Claude-skill-registry create-container-module
Scaffolds new container modules following ModuleImplementationGuide.md Section 3.1 standard structure. Creates directory layout, required files (Dockerfile, package.json, tsconfig.json, README.md, CHANGELOG.md), config files with schema validation, server.ts entry point, and OpenAPI spec. Use when creating a new microservice container, scaffolding module structure, or setting up a new service following the standard architecture.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/create-container-module" ~/.claude/skills/majiayu000-claude-skill-registry-create-container-module && rm -rf "$T"
skills/data/create-container-module/SKILL.mdCreate Container Module
Scaffolds new container modules following the standard structure from ModuleImplementationGuide.md Section 3.1.
Quick Start
When creating a new container module:
- Create standard directory structure
- Generate required files (Dockerfile, package.json, tsconfig.json, README.md, CHANGELOG.md)
- Set up configuration (config/default.yaml, config/schema.json)
- Create src/server.ts entry point
- Generate OpenAPI spec template
Standard Directory Layout
Reference: ModuleImplementationGuide.md Section 3.1
containers/[module-name]/ ├── Dockerfile ├── package.json ├── tsconfig.json ├── README.md ├── CHANGELOG.md ├── openapi.yaml ├── config/ │ ├── default.yaml │ └── schema.json ├── src/ │ ├── server.ts │ ├── config/ │ │ ├── index.ts │ │ └── types.ts │ ├── routes/ │ │ ├── index.ts │ │ └── [resource].ts │ ├── services/ │ │ └── [Service].ts │ ├── types/ │ │ └── index.ts │ └── utils/ │ └── logger.ts └── tests/ ├── unit/ ├── integration/ └── fixtures/
Required Files
1. Dockerfile
FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ COPY containers/shared/package*.json ./containers/shared/ RUN npm install COPY containers/[module-name] ./containers/[module-name] COPY containers/shared ./containers/shared WORKDIR /app/containers/shared RUN npm run build WORKDIR /app/containers/[module-name] RUN npm run build FROM node:20-alpine WORKDIR /app COPY --from=builder /app/containers/[module-name]/dist ./dist COPY --from=builder /app/containers/[module-name]/node_modules ./node_modules COPY --from=builder /app/containers/[module-name]/package.json ./ EXPOSE 3XXX CMD ["node", "dist/server.js"]
2. package.json
{ "name": "@coder/[module-name]", "version": "1.0.0", "description": "[Module Description]", "main": "dist/server.js", "type": "module", "scripts": { "dev": "tsx watch src/server.ts", "build": "tsc", "start": "node dist/server.js", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "lint": "eslint src --ext .ts" }, "dependencies": { "@coder/shared": "file:../shared", "@fastify/swagger": "^9.1.0", "@fastify/swagger-ui": "^5.0.0", "fastify": "^5.1.0", "js-yaml": "^4.1.0", "zod": "^3.24.1" }, "devDependencies": { "@types/node": "^25.0.5", "@vitest/coverage-v8": "^2.1.0", "tsx": "^4.19.2", "typescript": "^5.9.3", "vitest": "^4.0.0" } }
3. tsconfig.json
{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "baseUrl": "./src", "paths": { "@/*": ["./*"] } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "tests"] }
4. config/default.yaml
Reference: ModuleImplementationGuide.md Section 4.2
module: name: [module-name] version: 1.0.0 server: port: ${PORT:-3XXX} host: ${HOST:-0.0.0.0} cosmos_db: endpoint: ${COSMOS_DB_ENDPOINT} key: ${COSMOS_DB_KEY} database_id: ${COSMOS_DB_DATABASE_ID:-castiel} containers: main: [module-name]_data services: auth: url: ${AUTH_URL:-http://localhost:3021} logging: url: ${LOGGING_URL:-http://localhost:3014} rabbitmq: url: ${RABBITMQ_URL} exchange: coder_events queue: [module-name]_service bindings: [] features: feature_flag: ${FEATURE_FLAG:-true}
5. config/schema.json
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["server", "cosmos_db"], "properties": { "server": { "type": "object", "required": ["port"], "properties": { "port": { "type": "number" }, "host": { "type": "string" } } }, "cosmos_db": { "type": "object", "required": ["endpoint", "key", "database_id"], "properties": { "endpoint": { "type": "string" }, "key": { "type": "string" }, "database_id": { "type": "string" } } } } }
6. src/server.ts
Reference: containers/auth/src/server.ts
import { randomUUID } from 'crypto'; import Fastify, { FastifyInstance } from 'fastify'; import { initializeDatabase, getDatabaseClient, setupJWT } from '@coder/shared'; import swagger from '@fastify/swagger'; import swaggerUI from '@fastify/swagger-ui'; import { loadConfig } from './config'; import { log } from './utils/logger'; let app: FastifyInstance | null = null; export async function buildApp(): Promise<FastifyInstance> { const config = loadConfig(); const fastify = Fastify({ logger: false, requestIdHeader: 'x-request-id', genReqId: () => randomUUID(), bodyLimit: 1048576, requestTimeout: 30000, }); await fastify.register(swagger, { openapi: { openapi: '3.0.3', info: { title: '[Module Name] API', version: '1.0.0', }, servers: [{ url: '/api/v1' }], }, }); await fastify.register(swaggerUI, { routePrefix: '/docs', }); await setupJWT(fastify, { secret: config.jwt?.secret || process.env.JWT_SECRET || 'change-me', }); // Initialize database initializeDatabase({ endpoint: config.cosmos_db.endpoint, key: config.cosmos_db.key, database: config.cosmos_db.database_id, containers: config.cosmos_db.containers, }); const db = getDatabaseClient(); // Note: Tenant enforcement is handled per-route via tenantEnforcementMiddleware() // No need to register globally - use in route preHandler arrays // Register global error handler fastify.setErrorHandler((error: Error & { validation?: unknown; statusCode?: number }, request, reply) => { log.error('Request error', error, { requestId: request.id, path: request.url, method: request.method, service: '[module-name]', }); // Handle validation errors if (error.validation) { return reply.status(400).send({ error: { code: 'VALIDATION_ERROR', message: 'Invalid request', details: error.validation, }, }); } // Generic error response return reply.status(error.statusCode || 500).send({ error: { code: 'INTERNAL_ERROR', message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : error.message, }, }); }); // Register request/response logging hooks fastify.addHook('onRequest', async (request) => { log.debug('Request received', { requestId: request.id, method: request.method, path: request.url, service: '[module-name]', }); }); fastify.addHook('onResponse', async (request, reply) => { log.debug('Request completed', { requestId: request.id, method: request.method, path: request.url, statusCode: reply.statusCode, responseTime: reply.elapsedTime, service: '[module-name]', }); }); // Register routes const { registerRoutes } = await import('./routes'); await registerRoutes(fastify, { db, config }); // Health check endpoints (no auth required) fastify.get('/health', async () => { return { status: 'healthy', timestamp: new Date().toISOString(), service: '[module-name]', }; }); fastify.get('/ready', async () => { const db = getDatabaseClient(); let dbStatus = 'unknown'; try { // Test database connection const container = db.getContainer(config.cosmos_db.containers.main); await container.read(); dbStatus = 'ok'; } catch (error) { dbStatus = 'error'; } const allOk = dbStatus === 'ok'; return { status: allOk ? 'ready' : 'not_ready', checks: { database: { status: dbStatus }, }, timestamp: new Date().toISOString(), service: '[module-name]', }; }); app = fastify; return fastify; } export async function start(): Promise<void> { try { const config = loadConfig(); const app = await buildApp(); await app.listen({ port: config.server.port, host: config.server.host, }); log.info(`[Module Name] service listening on http://${config.server.host}:${config.server.port}`, { port: config.server.port, host: config.server.host, service: '[module-name]', }); } catch (error) { log.error('Failed to start [module-name] service', error, { service: '[module-name]' }); process.exit(1); } } // Graceful shutdown handler async function gracefulShutdown(signal: string): Promise<void> { log.info(`${signal} received, shutting down gracefully`, { service: '[module-name]' }); if (app) { await app.close(); } process.exit(0); } // Graceful shutdown process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Handle uncaught exceptions process.on('uncaughtException', (error: Error) => { log.error('Uncaught exception', error, { service: '[module-name]' }); gracefulShutdown('uncaughtException').catch(() => { process.exit(1); }); }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason: any, promise: Promise<any>) => { log.error('Unhandled promise rejection', reason as Error, { service: '[module-name]', promise: promise.toString(), }); }); // Start server if this file is run directly if (require.main === module) { start().catch((error) => { console.error('Fatal error starting server:', error); process.exit(1); }); }
7. src/routes/index.ts
Reference: containers/auth/src/routes/index.ts, containers/context-service/src/routes/index.ts
/** * Route Registration * Per ModuleImplementationGuide Section 7 */ import { FastifyInstance } from 'fastify'; import { ModuleConfig } from '../config/types'; export async function registerRoutes( fastify: FastifyInstance, deps: { db: any; config: ModuleConfig } ): Promise<void> { // Register resource routes const { setupResourceRoutes } = await import('./resource'); await setupResourceRoutes(fastify, deps); }
8. src/routes/[resource].ts (Example Route)
Reference: containers/context-service/src/routes/index.ts
import { FastifyInstance } from 'fastify'; import { authenticateRequest, tenantEnforcementMiddleware } from '@coder/shared'; import { AppError } from '@coder/shared'; import { ResourceService } from '../services/ResourceService'; export async function setupResourceRoutes( fastify: FastifyInstance, deps: { db: any; config: any } ): Promise<void> { const service = new ResourceService(deps.db); // GET /api/v1/resources/:id fastify.get<{ Params: { id: string } }>( '/api/v1/resources/:id', { preHandler: [authenticateRequest(), tenantEnforcementMiddleware()], schema: { description: 'Get resource by ID', tags: ['Resources'], params: { type: 'object', properties: { id: { type: 'string', format: 'uuid' }, }, }, response: { 200: { type: 'object', description: 'Resource details', }, }, }, }, async (request, reply) => { // ✅ tenantId is available from tenantEnforcementMiddleware via request.user // The middleware extracts X-Tenant-ID header and validates it const tenantId = request.user!.tenantId; const userId = request.user!.id; // Also available from authenticateRequest const resource = await service.getResource(tenantId, request.params.id); if (!resource) { throw new AppError('Resource not found', 404, 'NOT_FOUND'); } return reply.send({ data: resource }); } ); // POST /api/v1/resources fastify.post<{ Body: CreateResourceInput }>( '/api/v1/resources', { preHandler: [authenticateRequest(), tenantEnforcementMiddleware()], schema: { description: 'Create a new resource', tags: ['Resources'], body: { type: 'object', required: ['name'], properties: { name: { type: 'string' }, }, }, response: { 201: { type: 'object', description: 'Resource created successfully', }, }, }, }, async (request, reply) => { // ✅ tenantId and userId available from middleware const tenantId = request.user!.tenantId; const userId = request.user!.id; const resource = await service.createResource(tenantId, request.body); return reply.status(201).send({ data: resource }); } ); }
Note:
validates JWT token and attaches user toauthenticateRequest()request.user
validatestenantEnforcementMiddleware()
header and attachesX-Tenant-ID
totenantIdrequest.user.tenantId- Both are required in
array for protected routespreHandler - The gateway injects
header (immutable, from JWT)X-Tenant-ID
9. openapi.yaml
Reference: ModuleImplementationGuide.md Section 7.4
openapi: 3.0.3 info: title: [Module Name] API version: 1.0.0 servers: - url: /api/v1 paths: /health: get: summary: Health check responses: '200': description: Service is healthy
10. README.md
# [Module Name] Module [Description] ## Features - Feature 1 - Feature 2 ## Quick Start ### Prerequisites - Node.js 20+ - Azure Cosmos DB NoSQL account ### Installation \`\`\`bash npm install \`\`\` ### Configuration \`\`\`bash cp config/default.yaml config/local.yaml \`\`\` ### Running \`\`\`bash npm run dev \`\`\` ## API Reference See [OpenAPI Spec](./openapi.yaml) ## Events ### Published Events - \`[module].event.name\` ### Consumed Events - \`other.event.name\`
11. src/utils/logger.ts
Reference: containers/auth/src/utils/logger.ts
/** * Structured Logger * Per ModuleImplementationGuide Section 15.2 */ const LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3, } as const; type LogLevel = keyof typeof LOG_LEVELS; const currentLevel: LogLevel = (process.env.LOG_LEVEL as LogLevel) || 'info'; function shouldLog(level: LogLevel): boolean { return LOG_LEVELS[level] <= LOG_LEVELS[currentLevel]; } function formatLog(level: LogLevel, message: string, meta?: Record<string, any>): string { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level: level.toUpperCase(), service: '[module-name]', message, ...meta, }; return JSON.stringify(logEntry); } export const log = { error(message: string, error?: Error | unknown, meta?: Record<string, any>): void { if (shouldLog('error')) { const errorMeta = error instanceof Error ? { error: error.message, stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined } : error ? { error: String(error) } : {}; console.error(formatLog('error', message, { ...errorMeta, ...meta })); } }, warn(message: string, meta?: Record<string, any>): void { if (shouldLog('warn')) { console.warn(formatLog('warn', message, meta)); } }, info(message: string, meta?: Record<string, any>): void { if (shouldLog('info')) { console.info(formatLog('info', message, meta)); } }, debug(message: string, meta?: Record<string, any>): void { if (shouldLog('debug')) { console.debug(formatLog('debug', message, meta)); } }, };
12. CHANGELOG.md
# Changelog ## [1.0.0] - YYYY-MM-DD ### Added - Initial module creation - Core functionality
Validation Checklist
- All required files created
- Directory structure matches Section 3.1
- Config uses environment variables (no hardcoded values)
- Server.ts follows auth container pattern
- OpenAPI spec created
- TypeScript config extends root tsconfig.json
- Logger utility created in src/utils/logger.ts
- All imports use @coder/shared (no direct module imports)
- Health check endpoints implemented (/health, /ready)