Directus

データベース・ファーストのヘッドレスCMS。既存のデータベースを即座にAPIとして公開可能。

ヘッドレスCMSオープンソースデータベース駆動型API自動生成拡張可能
ライセンス
GPL v3
言語
TypeScript/Vue.js
料金
オープンソース版無料

ヘッドレスCMS

Directus

概要

Directusは、既存のSQLデータベースに接続して即座にREST APIとGraphQL APIを生成する、オープンソースのヘッドレスCMSです。技術者でなくても使いやすい管理画面(Data Studio)と、強力なAPIを組み合わせ、あらゆる種類のプロジェクトに対応できる柔軟性を提供します。データモデルの設計からコンテンツ管理、API配信まで、すべてを一つのプラットフォームで実現します。

詳細

Directusは「Database First」アプローチを採用し、新規または既存のSQLデータベースをそのまま活用します。PostgreSQL、MySQL、SQLite、OracleDB、CockroachDB、MariaDB、MS-SQLなど、主要なデータベースをサポートし、マイグレーション不要で導入できます。TypeScriptで構築され、Node.jsとVue.jsをベースにした完全にオープンソースのプラットフォームです。

主な特徴:

  • インスタントAPI生成: 既存DBから90秒以内にREST/GraphQL APIを自動生成
  • Data Studio: 非技術者でも使いやすいVue.js製の管理画面
  • データモデリング: Many-to-Any (M2A)関係を含む高度なデータ構造
  • 拡張性: Extensions SDKによる完全なカスタマイズ
  • ビルトイン機能: 認証、キャッシュ、画像変換、集計、フィルタリング
  • Flows: イベント駆動型のデータ処理とタスク自動化
  • アクセス制御: 詳細なロールベースのパーミッション
  • リアルタイムサポート: WebSocketによるリアルタイム更新

メリット・デメリット

メリット

  • 既存のデータベースをそのまま活用可能
  • データベース構造に縛られない柔軟性
  • 技術者・非技術者両方に優しいUI
  • 完全なオープンソース(GPL-3.0)
  • 豊富なデータベースサポート
  • 強力な拡張機能システム
  • APIパフォーマンスの高さ
  • セルフホスティングとクラウドの両方に対応

デメリット

  • データベースの知識が必要
  • NoSQLデータベースは非対応
  • 初期設定の複雑さ
  • コンテンツタイプの事前定義が必要
  • CMSに特化した機能が少ない
  • 学習曲線がやや急

参考ページ

書き方の例

1. Hello World(基本的なセットアップ)

# Directusのインストール(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

# NPMでのインストール
npm init directus-project my-project
cd my-project
npx directus bootstrap
npx directus start
// Directus SDKの初期化
import { createDirectus, rest, authentication } from '@directus/sdk';

// クライアントの作成
const client = createDirectus('http://localhost:8055')
  .with(authentication())
  .with(rest());

// ログイン
await client.login('[email protected]', 'password');

// データの取得
const articles = await client.request(
  readItems('articles', {
    fields: ['id', 'title', 'content', 'author.name'],
    limit: 10
  })
);

2. コンテンツ管理

// コレクションとフィールドの作成(管理API使用)
import { createCollection, createField } from '@directus/sdk';

// 記事コレクションの作成
await client.request(
  createCollection({
    collection: 'articles',
    meta: {
      collection: 'articles',
      icon: 'article',
      display_template: '{{title}}'
    }
  })
);

// フィールドの追加
await client.request(
  createField('articles', {
    field: 'title',
    type: 'string',
    meta: {
      interface: 'input',
      display: 'formatted-value',
      required: true
    }
  })
);

// リレーションの作成
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操作

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

// 複数アイテムの取得(フィルタリング付き)
const publishedArticles = await client.request(
  readItems('articles', {
    filter: {
      status: {
        _eq: 'published'
      },
      date_published: {
        _lte: '$NOW'
      }
    },
    sort: ['-date_published'],
    limit: 20
  })
);

// 単一アイテムの取得
const article = await client.request(
  readItem('articles', 'article-id')
);

// アイテムの作成
const newArticle = await client.request(
  createItem('articles', {
    title: 'New Article',
    content: 'Article content...',
    status: 'draft'
  })
);

// アイテムの更新
await client.request(
  updateItem('articles', 'article-id', {
    status: 'published',
    date_published: new Date()
  })
);

// 集計とグループ化
const stats = await client.request(
  readItems('articles', {
    aggregate: {
      count: ['id'],
      avg: ['views']
    },
    groupBy: ['status']
  })
);

4. 認証設定

// 認証の設定
const client = createDirectus('http://localhost:8055')
  .with(authentication('cookie', { credentials: 'include' }))
  .with(rest());

// ログイン
await client.login('[email protected]', 'password');

// トークンの取得
const token = await client.getToken();

// 静的トークンの使用
import { staticToken } from '@directus/sdk';

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

// カスタムストレージの実装
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());

// 2要素認証の有効化
await client.request(enableTwoFactor('secret', 'otp'));

5. プラグイン・拡張機能

// カスタムエンドポイントの作成
// 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 });
    });
  }
};

// カスタムフックの作成
// 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()
    });
  });
};

// カスタムインターフェースの作成
// 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. デプロイ・本番環境設定

// 環境変数の設定
// .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での起動設定
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'directus',
    script: 'npx',
    args: 'directus start',
    env: {
      NODE_ENV: 'production',
      PORT: 8055
    }
  }]
};

// 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リバースプロキシ設定
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;
  }
}