| name | Directus Development Workflow |
| description | Complete development setup: scaffolding, TypeScript, testing, CI/CD, Docker, deployment, and best practices |
| version | 1.0.0 |
| author | Directus Development System |
| tags | directus, workflow, testing, docker, ci-cd, typescript, deployment, devops |
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 --from=builder /app/dist/extensions ./extensions
COPY --from=builder /app/package*.json ./
# Install production dependencies
RUN npm ci --only=production
# Create uploads directory
RUN mkdir -p uploads
# Set up healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
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