Directus Development Workflow
SKILL.md
Directus Development Workflow
Overview
This skill provides comprehensive guidance for setting up and maintaining professional Directus development workflows. Master project scaffolding, TypeScript configuration, testing strategies, continuous integration/deployment, Docker containerization, multi-environment management, and development best practices for building scalable Directus applications.
When to Use This Skill
- Setting up new Directus projects
- Configuring TypeScript for type safety
- Implementing testing strategies
- Setting up CI/CD pipelines
- Containerizing with Docker
- Managing multiple environments
- Implementing database migrations
- Setting up development tools
- Optimizing build processes
- Deploying to production
Project Setup
Step 1: Initialize Directus Project
# Create project directory
mkdir my-directus-project && cd my-directus-project
# Initialize package.json
npm init -y
# Install Directus
npm install directus
# Initialize Directus
npx directus init
# Project structure
my-directus-project/
├── .env # Environment variables
├── .gitignore # Git ignore rules
├── docker-compose.yml # Docker configuration
├── package.json # Dependencies
├── tsconfig.json # TypeScript config
├── uploads/ # File uploads directory
├── extensions/ # Custom extensions
│ ├── endpoints/
│ ├── hooks/
│ ├── interfaces/
│ ├── displays/
│ ├── layouts/
│ ├── modules/
│ ├── operations/
│ └── panels/
├── migrations/ # Database migrations
├── seeders/ # Database seeders
├── tests/ # Test files
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── scripts/ # Utility scripts
├── docs/ # Documentation
└── .github/ # GitHub workflows
└── workflows/
Step 2: Environment Configuration
# .env.example
# Database
DB_CLIENT="pg"
DB_HOST="localhost"
DB_PORT="5432"
DB_DATABASE="directus"
DB_USER="directus"
DB_PASSWORD="directus"
# Security
KEY="your-random-secret-key"
SECRET="your-random-secret"
# Admin
ADMIN_EMAIL="admin@example.com"
ADMIN_PASSWORD="admin"
# Server
PUBLIC_URL="http://localhost:8055"
PORT=8055
# Storage
STORAGE_LOCATIONS="local,s3"
STORAGE_LOCAL_DRIVER="local"
STORAGE_LOCAL_ROOT="./uploads"
# S3 Storage (optional)
STORAGE_S3_DRIVER="s3"
STORAGE_S3_KEY="your-s3-key"
STORAGE_S3_SECRET="your-s3-secret"
STORAGE_S3_BUCKET="your-bucket"
STORAGE_S3_REGION="us-east-1"
# Email
EMAIL_TRANSPORT="sendgrid"
EMAIL_SENDGRID_API_KEY="your-sendgrid-key"
EMAIL_FROM="no-reply@example.com"
# Cache
CACHE_ENABLED="true"
CACHE_STORE="redis"
CACHE_REDIS="redis://localhost:6379"
CACHE_AUTO_PURGE="true"
# Rate Limiting
RATE_LIMITER_ENABLED="true"
RATE_LIMITER_STORE="redis"
RATE_LIMITER_POINTS="100"
RATE_LIMITER_DURATION="60"
# Extensions
EXTENSIONS_AUTO_RELOAD="true"
# Telemetry
TELEMETRY="false"
# AI Integration (custom)
OPENAI_API_KEY="your-openai-key"
ANTHROPIC_API_KEY="your-anthropic-key"
PINECONE_API_KEY="your-pinecone-key"
Step 3: TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"types": ["node", "jest"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@extensions/*": ["extensions/*"],
"@utils/*": ["src/utils/*"],
"@services/*": ["src/services/*"],
"@types/*": ["src/types/*"]
}
},
"include": [
"src/**/*",
"extensions/**/*",
"tests/**/*"
],
"exclude": [
"node_modules",
"dist",
"uploads"
]
}
Step 4: Extension Development Setup
# Create extension scaffolding script
cat > scripts/create-extension.sh << 'EOF'
#!/bin/bash
echo "Select extension type:"
echo "1) Endpoint"
echo "2) Hook"
echo "3) Panel"
echo "4) Interface"
echo "5) Display"
echo "6) Layout"
echo "7) Module"
echo "8) Operation"
read -p "Enter choice [1-8]: " choice
read -p "Enter extension name: " name
case $choice in
1) type="endpoint" ;;
2) type="hook" ;;
3) type="panel" ;;
4) type="interface" ;;
5) type="display" ;;
6) type="layout" ;;
7) type="module" ;;
8) type="operation" ;;
*) echo "Invalid choice"; exit 1 ;;
esac
npx create-directus-extension@latest \
--type=$type \
--name=$name \
--language=typescript
echo "Extension created at extensions/$type-$name"
EOF
chmod +x scripts/create-extension.sh
Docker Configuration
Development Docker Setup
# docker-compose.yml
version: '3.8'
services:
database:
image: postgis/postgis:15-alpine
container_name: directus_database
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: directus
POSTGRES_PASSWORD: directus
POSTGRES_DB: directus
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U directus"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
container_name: directus_cache
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
directus:
build:
context: .
dockerfile: Dockerfile.dev
container_name: directus_app
ports:
- "8055:8055"
volumes:
- ./uploads:/directus/uploads
- ./extensions:/directus/extensions
- ./migrations:/directus/migrations
- ./.env:/directus/.env
environment:
DB_CLIENT: pg
DB_HOST: database
DB_PORT: 5432
DB_DATABASE: directus
DB_USER: directus
DB_PASSWORD: directus
CACHE_ENABLED: "true"
CACHE_STORE: redis
CACHE_REDIS: redis://cache:6379
EXTENSIONS_AUTO_RELOAD: "true"
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
command: >
sh -c "
npx directus database install &&
npx directus database migrate:latest &&
npx directus start
"
# Development tools
adminer:
image: adminer
container_name: directus_adminer
ports:
- "8080:8080"
environment:
ADMINER_DEFAULT_SERVER: database
depends_on:
- database
mailhog:
image: mailhog/mailhog
container_name: directus_mailhog
ports:
- "1025:1025" # SMTP server
- "8025:8025" # Web UI
volumes:
postgres_data:
redis_data:
networks:
default:
name: directus_network
Production Docker Setup
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY extensions ./extensions
COPY src ./src
# Build TypeScript
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /directus
# Install Directus
RUN npm install -g directus
# Copy built extensions
COPY /app/dist/extensions ./extensions
COPY /app/package*.json ./
# Install production dependencies
RUN npm ci --only=production
# Create uploads directory
RUN mkdir -p uploads
# Set up healthcheck
HEALTHCHECK \
CMD node -e "require('http').get('http://localhost:8055/server/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})"
# Expose port
EXPOSE 8055
# Start Directus
CMD ["npx", "directus", "start"]
Testing Strategy
Unit Testing with Vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'*.config.ts',
'tests/',
],
},
setupFiles: ['./tests/setup.ts'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@extensions': path.resolve(__dirname, './extensions'),
'@utils': path.resolve(__dirname, './src/utils'),
'@services': path.resolve(__dirname, './src/services'),
},
},
});
// tests/setup.ts
import { beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import { createTestDatabase, dropTestDatabase } from './helpers/database';
import { mockServices } from './mocks/services';
beforeAll(async () => {
await createTestDatabase();
global.testServices = mockServices();
});
afterAll(async () => {
await dropTestDatabase();
});
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
});
afterEach(() => {
// Clean up test data
});
Integration Testing
// tests/integration/api.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { createDirectus, rest, authentication, createItems, readItems } from '@directus/sdk';
describe('API Integration Tests', () => {
let client: any;
beforeAll(async () => {
client = createDirectus('http://localhost:8055')
.with(authentication())
.with(rest());
await client.login('admin@example.com', 'admin');
});
describe('Collections API', () => {
it('should create and read items', async () => {
// Create item
const created = await client.request(
createItems('articles', {
title: 'Test Article',
content: 'Test content',
status: 'published',
})
);
expect(created).toHaveProperty('id');
// Read items
const items = await client.request(
readItems('articles', {
filter: {
id: { _eq: created.id },
},
})
);
expect(items).toHaveLength(1);
expect(items[0].title).toBe('Test Article');
});
});
describe('Custom Endpoints', () => {
it('should handle custom analytics endpoint', async () => {
const response = await fetch('http://localhost:8055/custom/analytics', {
headers: {
Authorization: `Bearer ${client.token}`,
},
});
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveProperty('data');
expect(data.data).toHaveProperty('daily');
});
});
});
End-to-End Testing with Playwright
// tests/e2e/admin-panel.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Directus Admin Panel', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:8055/admin');
// Login
await page.fill('input[type="email"]', 'admin@example.com');
await page.fill('input[type="password"]', 'admin');
await page.click('button[type="submit"]');
await page.waitForURL('**/content');
});
test('should create new article', async ({ page }) => {
// Navigate to articles
await page.click('text=Articles');
// Create new item
await page.click('button:has-text("Create Item")');
// Fill form
await page.fill('input[name="title"]', 'Test Article from E2E');
await page.fill('textarea[name="content"]', 'This is test content');
// Save
await page.click('button:has-text("Save")');
// Verify
await expect(page.locator('text=Item created')).toBeVisible();
});
test('should use custom panel', async ({ page }) => {
// Navigate to insights
await page.click('text=Insights');
// Check custom panel
await expect(page.locator('.analytics-panel')).toBeVisible();
// Verify data loads
await expect(page.locator('.metric-card')).toHaveCount(3);
});
});
CI/CD Pipeline
GitHub Actions Workflow
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
release:
types: [created]
env:
NODE_VERSION: '18'
DOCKER_REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# Linting and Type Checking
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Type check
run: npm run type-check
# Unit and Integration Tests
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: directus
POSTGRES_PASSWORD: directus
POSTGRES_DB: directus_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run migrations
env:
DB_CLIENT: pg
DB_HOST: localhost
DB_PORT: 5432
DB_DATABASE: directus_test
DB_USER: directus
DB_PASSWORD: directus
run: |
npx directus database install
npx directus database migrate:latest
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
flags: unittests
name: codecov-umbrella
# E2E Tests
e2e:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: |
npm ci
npx playwright install --with-deps
- name: Start services
run: docker-compose up -d
- name: Wait for services
run: |
timeout 60 sh -c 'until curl -f http://localhost:8055/server/health; do sleep 1; done'
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
# Security Scanning
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Run npm audit
run: npm audit --audit-level=high
# Build and Push Docker Image
build:
needs: [lint, test]
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'release'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# Deploy to Staging
deploy-staging:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment:
name: staging
url: https://staging.example.com
steps:
- name: Deploy to Kubernetes
run: |
echo "Deploying to staging..."
# kubectl apply -f k8s/staging/
- name: Run smoke tests
run: |
curl -f https://staging.example.com/server/health
# Deploy to Production
deploy-production:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment:
name: production
url: https://example.com
steps:
- name: Deploy to Kubernetes
run: |
echo "Deploying to production..."
# kubectl apply -f k8s/production/
- name: Run smoke tests
run: |
curl -f https://example.com/server/health
- name: Notify deployment
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Production deployment completed!'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Database Migration Management
Migration Scripts
// migrations/001_create_custom_tables.ts
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
// Create custom analytics table
await knex.schema.createTable('custom_analytics', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('event_type', 50).notNullable();
table.string('event_category', 50);
table.jsonb('event_data');
table.uuid('user_id').references('id').inTable('directus_users');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.index(['event_type', 'created_at']);
table.index('user_id');
});
// Create custom settings table
await knex.schema.createTable('custom_settings', (table) => {
table.string('key', 100).primary();
table.jsonb('value').notNullable();
table.string('type', 20).defaultTo('string');
table.text('description');
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists('custom_settings');
await knex.schema.dropTableIfExists('custom_analytics');
}
Migration Runner Script
// scripts/migrate.ts
import { Knex } from 'knex';
import { config } from 'dotenv';
import path from 'path';
config();
const knexConfig: Knex.Config = {
client: process.env.DB_CLIENT || 'pg',
connection: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_DATABASE,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
migrations: {
directory: path.join(__dirname, '../migrations'),
extension: 'ts',
tableName: 'knex_migrations',
},
};
const knex = require('knex')(knexConfig);
async function runMigrations() {
try {
console.log('Running migrations...');
const [batch, migrations] = await knex.migrate.latest();
if (migrations.length === 0) {
console.log('Database is already up to date');
} else {
console.log(`Batch ${batch} run: ${migrations.length} migrations`);
migrations.forEach(migration => {
console.log(` - ${migration}`);
});
}
process.exit(0);
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
async function rollbackMigrations() {
try {
console.log('Rolling back migrations...');
const [batch, migrations] = await knex.migrate.rollback();
if (migrations.length === 0) {
console.log('No migrations to rollback');
} else {
console.log(`Batch ${batch} rolled back: ${migrations.length} migrations`);
migrations.forEach(migration => {
console.log(` - ${migration}`);
});
}
process.exit(0);
} catch (error) {
console.error('Rollback failed:', error);
process.exit(1);
}
}
// Parse command line arguments
const command = process.argv[2];
switch (command) {
case 'up':
case 'latest':
runMigrations();
break;
case 'down':
case 'rollback':
rollbackMigrations();
break;
default:
console.log('Usage: npm run migrate [up|down]');
process.exit(1);
}
Development Tools Setup
VS Code Configuration
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"files.exclude": {
"node_modules": true,
"dist": true,
".turbo": true,
"uploads": true
},
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/uploads": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"directus.api.url": "http://localhost:8055",
"directus.api.staticToken": "${env:DIRECTUS_STATIC_TOKEN}"
}
VS Code Launch Configuration
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Directus",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/node_modules/.bin/directus",
"args": ["start"],
"env": {
"NODE_ENV": "development",
"EXTENSIONS_AUTO_RELOAD": "true"
},
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Debug Extension",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/extensions/${input:extensionName}/src/index.ts",
"preLaunchTask": "npm: build:extensions",
"outFiles": ["${workspaceFolder}/extensions/${input:extensionName}/dist/**/*.js"],
"env": {
"NODE_ENV": "development"
}
},
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/node_modules/.bin/vitest",
"args": ["--run", "${file}"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
],
"inputs": [
{
"id": "extensionName",
"type": "promptString",
"description": "Enter the extension name to debug"
}
]
}
Performance Monitoring
Application Performance Monitoring
// src/monitoring/apm.ts
import * as Sentry from '@sentry/node';
import { ProfilingIntegration } from '@sentry/profiling-node';
import { performance } from 'perf_hooks';
export class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map();
constructor() {
// Initialize Sentry
if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
new ProfilingIntegration(),
],
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
profilesSampleRate: 1.0,
});
}
}
startTimer(operation: string): () => void {
const start = performance.now();
return () => {
const duration = performance.now() - start;
this.recordMetric(operation, duration);
// Log slow operations
if (duration > 1000) {
console.warn(`Slow operation: ${operation} took ${duration.toFixed(2)}ms`);
}
};
}
recordMetric(name: string, value: number): void {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
const values = this.metrics.get(name)!;
values.push(value);
// Keep only last 100 values
if (values.length > 100) {
values.shift();
}
}
getStats(name: string): {
avg: number;
min: number;
max: number;
p95: number;
} | null {
const values = this.metrics.get(name);
if (!values || values.length === 0) return null;
const sorted = [...values].sort((a, b) => a - b);
const sum = sorted.reduce((a, b) => a + b, 0);
return {
avg: sum / sorted.length,
min: sorted[0],
max: sorted[sorted.length - 1],
p95: sorted[Math.floor(sorted.length * 0.95)],
};
}
async measureDatabaseQuery<T>(
queryName: string,
query: () => Promise<T>
): Promise<T> {
const transaction = Sentry.startTransaction({
op: 'db.query',
name: queryName,
});
const endTimer = this.startTimer(`db.${queryName}`);
try {
const result = await query();
transaction.setStatus('ok');
return result;
} catch (error) {
transaction.setStatus('internal_error');
throw error;
} finally {
endTimer();
transaction.finish();
}
}
reportMetrics(): void {
const report: any = {};
this.metrics.forEach((values, name) => {
report[name] = this.getStats(name);
});
console.log('Performance Report:', JSON.stringify(report, null, 2));
// Send to monitoring service
if (process.env.MONITORING_ENDPOINT) {
fetch(process.env.MONITORING_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timestamp: new Date().toISOString(),
metrics: report,
}),
}).catch(console.error);
}
}
}
Deployment Strategies
Kubernetes Deployment
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: directus
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: directus
template:
metadata:
labels:
app: directus
spec:
containers:
- name: directus
image: ghcr.io/myorg/directus:latest
ports:
- containerPort: 8055
env:
- name: DB_CLIENT
value: "pg"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: directus-secrets
key: db-host
- name: DB_DATABASE
value: "directus"
- name: DB_USER
valueFrom:
secretKeyRef:
name: directus-secrets
key: db-user
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: directus-secrets
key: db-password
- name: KEY
valueFrom:
secretKeyRef:
name: directus-secrets
key: app-key
- name: SECRET
valueFrom:
secretKeyRef:
name: directus-secrets
key: app-secret
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /server/health
port: 8055
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /server/ping
port: 8055
initialDelaySeconds: 10
periodSeconds: 5
volumeMounts:
- name: uploads
mountPath: /directus/uploads
- name: extensions
mountPath: /directus/extensions
volumes:
- name: uploads
persistentVolumeClaim:
claimName: directus-uploads
- name: extensions
configMap:
name: directus-extensions
---
apiVersion: v1
kind: Service
metadata:
name: directus
namespace: production
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8055
selector:
app: directus
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: directus-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: directus
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Terraform Infrastructure
# terraform/main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "terraform-state-directus"
key = "production/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = var.aws_region
}
# RDS Database
resource "aws_db_instance" "directus" {
identifier = "directus-production"
engine = "postgres"
engine_version = "15.3"
instance_class = "db.t3.medium"
allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true
db_name = "directus"
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.database.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 30
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
skip_final_snapshot = false
final_snapshot_identifier = "directus-final-snapshot-${timestamp()}"
tags = {
Name = "directus-production"
Environment = "production"
}
}
# ElastiCache Redis
resource "aws_elasticache_cluster" "directus" {
cluster_id = "directus-cache"
engine = "redis"
node_type = "cache.t3.micro"
num_cache_nodes = 1
parameter_group_name = "default.redis7"
port = 6379
subnet_group_name = aws_elasticache_subnet_group.main.name
security_group_ids = [aws_security_group.cache.id]
tags = {
Name = "directus-cache"
Environment = "production"
}
}
# S3 Bucket for uploads
resource "aws_s3_bucket" "uploads" {
bucket = "directus-uploads-${var.environment}"
tags = {
Name = "directus-uploads"
Environment = var.environment
}
}
resource "aws_s3_bucket_versioning" "uploads" {
bucket = aws_s3_bucket.uploads.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "uploads" {
bucket = aws_s3_bucket.uploads.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "directus-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = "directus-cluster"
Environment = var.environment
}
}
# ECS Task Definition
resource "aws_ecs_task_definition" "directus" {
family = "directus"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "1024"
memory = "2048"
container_definitions = jsonencode([{
name = "directus"
image = "${var.ecr_repository_url}:${var.image_tag}"
portMappings = [{
containerPort = 8055
protocol = "tcp"
}]
environment = [
{
name = "DB_CLIENT"
value = "pg"
},
{
name = "DB_HOST"
value = aws_db_instance.directus.address
},
{
name = "DB_DATABASE"
value = "directus"
},
{
name = "CACHE_ENABLED"
value = "true"
},
{
name = "CACHE_STORE"
value = "redis"
},
{
name = "CACHE_REDIS"
value = "redis://${aws_elasticache_cluster.directus.cache_nodes[0].address}:6379"
},
{
name = "STORAGE_LOCATIONS"
value = "s3"
},
{
name = "STORAGE_S3_BUCKET"
value = aws_s3_bucket.uploads.id
}
]
secrets = [
{
name = "DB_USER"
valueFrom = aws_secretsmanager_secret.db_credentials.arn
},
{
name = "DB_PASSWORD"
valueFrom = aws_secretsmanager_secret.db_credentials.arn
},
{
name = "KEY"
valueFrom = aws_secretsmanager_secret.app_secrets.arn
},
{
name = "SECRET"
valueFrom = aws_secretsmanager_secret.app_secrets.arn
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.directus.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "directus"
}
}
}])
tags = {
Name = "directus-task"
Environment = var.environment
}
}
# Output values
output "database_endpoint" {
value = aws_db_instance.directus.endpoint
description = "RDS database endpoint"
}
output "cache_endpoint" {
value = aws_elasticache_cluster.directus.cache_nodes[0].address
description = "Redis cache endpoint"
}
output "s3_bucket" {
value = aws_s3_bucket.uploads.id
description = "S3 bucket for uploads"
}
Monitoring & Logging
Structured Logging
// src/utils/logger.ts
import winston from 'winston';
import { LoggingWinston } from '@google-cloud/logging-winston';
const logFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
);
const transports: winston.transport[] = [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}),
];
// Add cloud logging in production
if (process.env.NODE_ENV === 'production') {
if (process.env.GOOGLE_CLOUD_PROJECT) {
transports.push(new LoggingWinston());
}
}
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: {
service: 'directus',
environment: process.env.NODE_ENV,
},
transports,
});
// Request logging middleware
export function requestLogger(req: any, res: any, next: any) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration,
ip: req.ip,
userAgent: req.get('user-agent'),
userId: req.accountability?.user,
});
});
next();
}
Best Practices
Code Quality Standards
// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
project: './tsconfig.json',
},
plugins: [
'@typescript-eslint',
'import',
'prettier',
'security',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:security/recommended',
'prettier',
],
rules: {
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
'import/order': ['error', {
'groups': ['builtin', 'external', 'internal'],
'newlines-between': 'always',
'alphabetize': { 'order': 'asc' },
}],
'no-console': ['warn', { allow: ['warn', 'error'] }],
'security/detect-object-injection': 'warn',
},
ignorePatterns: ['dist/', 'node_modules/', 'coverage/'],
};
Security Best Practices
// src/security/security-middleware.ts
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import mongoSanitize from 'express-mongo-sanitize';
import { v4 as uuidv4 } from 'uuid';
export function setupSecurity(app: any): void {
// Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Stricter rate limiting for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
});
app.use('/auth/', authLimiter);
// Request sanitization
app.use(mongoSanitize());
// Request ID for tracing
app.use((req: any, res: any, next: any) => {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
// CORS configuration
app.use((req: any, res: any, next: any) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
}
Success Metrics
- ✅ Development environment setup < 5 minutes
- ✅ TypeScript compilation with zero errors
- ✅ Test coverage > 80%
- ✅ CI/CD pipeline execution < 10 minutes
- ✅ Docker build size < 500MB
- ✅ Zero-downtime deployments
- ✅ Database migrations rollback capability
- ✅ Monitoring alerts < 1 minute response time
- ✅ Security scanning passes all checks
- ✅ Performance benchmarks meet SLA requirements
Resources
- Directus Documentation
- Docker Best Practices
- GitHub Actions
- Kubernetes Documentation
- Terraform AWS Provider
- Vitest Testing Framework
- Playwright E2E Testing
- TypeScript Handbook
Version History
- 1.0.0 - Initial release with comprehensive development workflow patterns