Directus

Database-first headless CMS. Instantly turns existing databases into APIs.

Headless CMSOpen SourceDatabase-DrivenAuto-Generated APIExtensible
License
GPL v3
Language
TypeScript/Vue.js
Pricing
オープンソース版無料
Official Site
Visit Official Site

Headless CMS

Directus

Overview

Directus is an open-source headless CMS that connects to existing SQL databases and instantly generates REST and GraphQL APIs. It combines an intuitive admin interface (Data Studio) that's accessible to non-technical users with powerful APIs, providing the flexibility to handle any type of project. From data model design to content management and API delivery, everything is achieved on a single platform.

Details

Directus adopts a "Database First" approach, leveraging new or existing SQL databases as-is. It supports major databases including PostgreSQL, MySQL, SQLite, OracleDB, CockroachDB, MariaDB, and MS-SQL, allowing deployment without migration. Built with TypeScript and based on Node.js and Vue.js, it's a completely open-source platform.

Key Features:

  • Instant API Generation: Auto-generates REST/GraphQL APIs from existing DB in under 90 seconds
  • Data Studio: User-friendly Vue.js admin interface for non-technical users
  • Data Modeling: Advanced data structures including Many-to-Any (M2A) relationships
  • Extensibility: Complete customization through Extensions SDK
  • Built-in Features: Authentication, caching, image transformation, aggregation, filtering
  • Flows: Event-driven data processing and task automation
  • Access Control: Granular role-based permissions
  • Real-time Support: Real-time updates via WebSocket

Advantages and Disadvantages

Advantages

  • Can leverage existing databases as-is
  • Flexibility not constrained by database structure
  • User-friendly UI for both technical and non-technical users
  • Completely open source (GPL-3.0)
  • Extensive database support
  • Powerful extension system
  • High API performance
  • Supports both self-hosting and cloud deployment

Disadvantages

  • Requires database knowledge
  • No support for NoSQL databases
  • Complex initial setup
  • Requires pre-defining content types
  • Limited CMS-specific features
  • Somewhat steep learning curve

Reference Pages

Code Examples

1. Hello World (Basic Setup)

# Install Directus (using Docker)
docker run -p 8055:8055 \
  -e ADMIN_EMAIL="[email protected]" \
  -e ADMIN_PASSWORD="password" \
  -e KEY="your-random-key" \
  -e SECRET="your-random-secret" \
  directus/directus

# Install with NPM
npm init directus-project my-project
cd my-project
npx directus bootstrap
npx directus start
// Initialize Directus SDK
import { createDirectus, rest, authentication } from '@directus/sdk';

// Create client
const client = createDirectus('http://localhost:8055')
  .with(authentication())
  .with(rest());

// Login
await client.login('[email protected]', 'password');

// Fetch data
const articles = await client.request(
  readItems('articles', {
    fields: ['id', 'title', 'content', 'author.name'],
    limit: 10
  })
);

2. Content Management

// Create collections and fields (using Admin API)
import { createCollection, createField } from '@directus/sdk';

// Create articles collection
await client.request(
  createCollection({
    collection: 'articles',
    meta: {
      collection: 'articles',
      icon: 'article',
      display_template: '{{title}}'
    }
  })
);

// Add fields
await client.request(
  createField('articles', {
    field: 'title',
    type: 'string',
    meta: {
      interface: 'input',
      display: 'formatted-value',
      required: true
    }
  })
);

// Create relation
await client.request(
  createField('articles', {
    field: 'author',
    type: 'uuid',
    meta: {
      interface: 'select-dropdown-m2o',
      display: 'related-values',
      display_options: {
        template: '{{first_name}} {{last_name}}'
      }
    },
    schema: {
      foreign_key_table: 'directus_users',
      foreign_key_column: 'id'
    }
  })
);

3. API Operations

// CRUD operations
import { 
  readItems, 
  readItem, 
  createItem, 
  updateItem, 
  deleteItem 
} from '@directus/sdk';

// Fetch multiple items with filtering
const publishedArticles = await client.request(
  readItems('articles', {
    filter: {
      status: {
        _eq: 'published'
      },
      date_published: {
        _lte: '$NOW'
      }
    },
    sort: ['-date_published'],
    limit: 20
  })
);

// Fetch single item
const article = await client.request(
  readItem('articles', 'article-id')
);

// Create item
const newArticle = await client.request(
  createItem('articles', {
    title: 'New Article',
    content: 'Article content...',
    status: 'draft'
  })
);

// Update item
await client.request(
  updateItem('articles', 'article-id', {
    status: 'published',
    date_published: new Date()
  })
);

// Aggregation and grouping
const stats = await client.request(
  readItems('articles', {
    aggregate: {
      count: ['id'],
      avg: ['views']
    },
    groupBy: ['status']
  })
);

4. Authentication Setup

// Configure authentication
const client = createDirectus('http://localhost:8055')
  .with(authentication('cookie', { credentials: 'include' }))
  .with(rest());

// Login
await client.login('[email protected]', 'password');

// Get token
const token = await client.getToken();

// Use static token
import { staticToken } from '@directus/sdk';

const publicClient = createDirectus('http://localhost:8055')
  .with(staticToken('your-static-token'))
  .with(rest());

// Implement custom storage
class CustomStorage {
  get() {
    return JSON.parse(localStorage.getItem('directus-auth'));
  }
  set(data) {
    localStorage.setItem('directus-auth', JSON.stringify(data));
  }
}

const client = createDirectus('http://localhost:8055')
  .with(authentication('json', { storage: new CustomStorage() }))
  .with(rest());

// Enable two-factor authentication
await client.request(enableTwoFactor('secret', 'otp'));

5. Plugins and Extensions

// Create custom endpoint
// extensions/endpoints/custom/index.js
export default {
  id: 'custom',
  handler: (router, { services, env }) => {
    const { ItemsService } = services;
    
    router.get('/stats', async (req, res) => {
      const articlesService = new ItemsService('articles', {
        schema: req.schema,
        accountability: req.accountability
      });
      
      const count = await articlesService.readByQuery({
        aggregate: {
          count: ['id']
        }
      });
      
      res.json({ total_articles: count });
    });
  }
};

// Create custom hook
// extensions/hooks/audit/index.js
export default ({ filter, action }, { services, database }) => {
  const { ItemsService } = services;
  
  filter('items.create', async (input, { collection }) => {
    input.created_by = input.accountability?.user;
    return input;
  });
  
  action('items.create', async ({ payload, collection }) => {
    const auditService = new ItemsService('audit_log', {
      schema: await getSchema()
    });
    
    await auditService.createOne({
      collection,
      action: 'create',
      user: payload.accountability?.user,
      timestamp: new Date()
    });
  });
};

// Create custom interface
// extensions/interfaces/custom-input/index.js
import InterfaceComponent from './interface.vue';

export default {
  id: 'custom-input',
  name: 'Custom Input',
  icon: 'box',
  component: InterfaceComponent,
  options: [
    {
      field: 'placeholder',
      name: 'Placeholder',
      type: 'string'
    }
  ]
};

6. Deployment and Production Setup

// Environment variables configuration
// .env
KEY=your-random-key
SECRET=your-random-secret
DB_CLIENT=postgres
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=directus
DB_USER=postgres
DB_PASSWORD=password

ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=password

PUBLIC_URL=https://api.example.com
CORS_ENABLED=true
CORS_ORIGIN=https://app.example.com

// PM2 startup configuration
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'directus',
    script: 'npx',
    args: 'directus start',
    env: {
      NODE_ENV: 'production',
      PORT: 8055
    }
  }]
};

// Production setup with Docker
// docker-compose.yml
version: '3'
services:
  database:
    image: postgres:15
    environment:
      POSTGRES_USER: directus
      POSTGRES_PASSWORD: directus
      POSTGRES_DB: directus
    volumes:
      - ./data/database:/var/lib/postgresql/data

  directus:
    image: directus/directus:latest
    ports:
      - 8055:8055
    volumes:
      - ./uploads:/directus/uploads
      - ./extensions:/directus/extensions
    environment:
      KEY: ${KEY}
      SECRET: ${SECRET}
      DB_CLIENT: postgres
      DB_HOST: database
      DB_PORT: 5432
      DB_DATABASE: directus
      DB_USER: directus
      DB_PASSWORD: directus
      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
    depends_on:
      - database

// Nginx reverse proxy configuration
server {
  listen 80;
  server_name api.example.com;
  
  location / {
    proxy_pass http://localhost:8055;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}