本文へ移動
cccskills
無料GitHub で公開

backend-standards

CMDO architecture standards for Node.js/TypeScript backends with strict layer separation.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md17.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Backend Standards Skill

CMDO ("Commando") architecture for Node.js/TypeScript backends with strict separation between infrastructure and domain concerns.


Architecture: CMDO (Controller Model DAL Operator)

Operator → Controller → Model Use Cases
   ↓            ↓              ↑
Config → [All layers] → Dependencies (injected by Controller)
                               ↓
                             DAL

Key Distinction: Infrastructure vs Domain

LayerKnows AboutExample
OperatorInfrastructure only"Here's a DB connection and generic HTTP client"
ConfigURLs and settingspaymentGatewayUrl: "https://api.stripe.com"
ControllerDomain concerns"Use httpClient + paymentGatewayUrl to charge a card"
ModelBusiness logic only"Calculate total, then call chargePayment()"

Key Principle: Operator provides raw I/O capabilities (generic HTTP client, cache client, DB connection) with NO domain knowledge. Controller combines I/O + config to create domain-specific operations.


Layer Overview

LayerPathResponsibility
Entry Pointsrc/index.tsBootstrap only (ONLY file with side effects)
Operatorsrc/operator/Raw I/O capabilities (DB, HTTP clients, cache), lifecycle management, state machine
Configsrc/config/Environment parsing, validation, type-safe config
Controllersrc/controller/Request/response handling, combines I/O + config for domain calls, creates Dependencies for Model
Modelsrc/model/Business logic (definitions + use-cases), receives Dependencies
DALsrc/dal/Data access, queries, mapping DB ↔ domain objects

Entry Point: src/index.ts

Rules:

  • src/index.ts is the ONLY file that runs code on import (exception to the "index.ts exports only" rule for application entry points)
  • All other files export functions/types with NO side effects when imported
  • NO logic beyond importing and starting the operator
  • NO configuration loading, validation, or setup logic
// src/index.ts - Entry point (exception to index.ts rule for entry points)
import { createOperator } from "./operator";
import { loadConfig } from "./config";

const main = async (): Promise<void> => {
    const config = loadConfig();
    const operator = createOperator({ config });
    await operator.start();
};

main().catch((error) => {
    console.error("Failed to start operator:", error);
    process.exit(1);
});

Layer 1: Operator

Provides raw I/O capabilities and orchestrates application lifecycle. NO domain knowledge - only infrastructure.

What Operator Does:

  • Creates database connection pool (raw I/O capability)
  • Creates generic HTTP client (for external calls - if needed)
  • Creates cache client (if needed)
  • Binds DAL functions with database client
  • Creates Controller, passing raw I/O + DAL + config
  • Manages lifecycle via state machine (IDLE → STARTING → RUNNING → STOPPING → STOPPED)
  • Manages HTTP server for API endpoints
  • Manages lifecycle probes on separate port (health/readiness for Kubernetes)
  • Handles Unix signals for graceful shutdown (SIGTERM, SIGINT, SIGHUP)
  • Initializes telemetry (logger, metrics) before other modules

What Operator Does NOT Do:

  • NO domain-specific calls (e.g., "call payment gateway", "fetch user from auth service")
  • NO business logic
  • NO knowledge of what the raw I/O will be used for

Lifecycle State Machine:

IDLE → STARTING:PROBES → STARTING:DATABASE → STARTING:HTTP_SERVER → RUNNING
                                                                        ↓
STOPPED ← STOPPING:PROBES ← STOPPING:DATABASE ← STOPPING:HTTP_SERVER ←─┘

Unix Signal Handling:

SignalSourceAction
SIGTERMKubernetes pod termination, kill commandGraceful shutdown
SIGINTCtrl+C from terminalGraceful shutdown
SIGHUPTerminal hangupGraceful shutdown

When a signal is received:

  1. Log the signal with info level
  2. Initiate graceful shutdown via stop()
  3. Wait for all connections to drain
  4. Exit with code 0 (success) or 1 (error)

Structure:

src/operator/
├── create_operator.ts      # Main factory, state machine, lifecycle
├── create_database.ts      # Database connection pool
├── create_http_server.ts   # HTTP server wrapper
├── lifecycle_probes.ts     # Health/readiness endpoints (separate port)
├── state_machine.ts        # Generic state machine implementation
├── logger.ts               # Pino logger with OpenTelemetry
├── metrics.ts              # OpenTelemetry metrics
└── index.ts                # Exports only

Layer 2: Config

Environment parsing, validation, type-safe config objects.

CRITICAL: Use dotenv for ALL environment variable access. Direct process.env access is FORBIDDEN outside the Config layer.

Environment Variable Rules:

  1. dotenv is mandatory: Always use dotenv.config() inside loadConfig() (not at module level)
  2. Config layer ONLY: process.env access is ONLY allowed in src/config/
  3. Type-safe access: All other layers receive typed Config object
  4. Validation required: Validate required vars and throw if missing
  5. Default values: Provide sensible defaults for optional vars
  6. NO direct access elsewhere: NEVER use process.env in Operator, Controller, Model, or DAL layers

What it does NOT contain: Business logic, database queries.

Structure:

src/config/
├── index.ts                # loadConfig() function, Config type
└── validation.ts           # Optional: validation helpers

Layer 3: Controller

Request/response handling, combines I/O + config for domain-specific operations, creates Dependencies object for Model.

Controller's Key Role:

  • Receives raw I/O capabilities from Operator (generic httpClient, cache, etc.)
  • Receives Config with URLs and settings
  • Combines I/O + config to create domain-specific operations (e.g., httpClient + config.paymentUrl → chargePayment)
  • Creates Model Dependencies by stitching together DAL + domain operations

HTTP Handlers: Each file in http_handlers/ corresponds to an API namespace (e.g., /users, /orders) and exports a router. The create_controller.ts imports these routers and wires them together with the Dependencies object for Model use-cases.

Handler Naming: Use operationId from OpenAPI spec with handle prefix (e.g., createUser → handleCreateUser).

Health Check Endpoints: Lifecycle probes (/health, /readiness) are handled by Operator on a separate port, NOT in the Controller.

What it does NOT contain: Database queries, business logic (delegates to Model).

Structure:

src/controller/
├── http_handlers/          # One file per API namespace
│   ├── users.ts            # Exports usersRouter
│   ├── orders.ts           # Exports ordersRouter
│   └── index.ts            # Re-exports all routers
├── create_controller.ts    # Assembles routers, creates Dependencies for Model
└── index.ts                # Exports only

Layer 4: Model

Business logic via definitions + use-cases. Model never imports from outside its module.

Definitions Rules:

  • Use TypeScript types only (type or interface)
  • NO Zod, Yup, io-ts, or similar validation libraries
  • Validation belongs in the Controller layer (input) or Operator layer (middleware)
  • Definitions are compile-time constructs, not runtime validators

Use Case Rules:

  • One use-case per file
  • Each use-case receives Dependencies as first argument
  • Return discriminated unions for success/failure
// ✅ GOOD: Discriminated union results
type CreateUserResult =
    | { readonly success: true; readonly user: User }
    | { readonly success: false; readonly error: 'email_exists' | 'invalid_email' };

const createUser = async (
    deps: Dependencies,
    args: CreateUserArgs
): Promise<CreateUserResult> => {
    // Business logic using only injected dependencies
};

What it does NOT contain: HTTP handling, direct database queries, external imports.

Structure:

src/model/
├── definitions/            # TypeScript types ONLY (no Zod/validation)
│   ├── user.ts
│   └── index.ts
├── use-cases/              # One function per file
│   ├── create_user.ts
│   ├── update_user.ts
│   └── index.ts
├── dependencies.ts         # Dependencies interface
└── index.ts                # Exports only

Layer 5: DAL

Data access functions that directly handle database queries. No repository pattern - use simple, focused functions instead.

DAL Rules:

  • One function per file, named after the function (e.g., find_user_by_id.ts)
  • Each function receives its dependencies as the first argument
  • No classes, no repository interfaces, no abstraction layers
  • Direct database queries with proper parameterization
  • index.ts re-exports all functions
  • No assumed grouping - add subdirectories only if explicitly instructed

What it does NOT contain: Business logic, HTTP handling, repository abstractions.

Structure:

src/dal/
├── find_user_by_id.ts
├── insert_user.ts
├── update_user.ts
└── index.ts                # Re-exports all functions

Telemetry (OpenTelemetry)

All observability follows OpenTelemetry standards. Telemetry is initialized by the Operator layer.

Logging

Use Pino with OpenTelemetry context injection.

Log Levels:

LevelWhen to use
debugDetailed debugging (disabled in production)
infoNormal operations, state changes, requests
warnRecoverable issues, deprecations
errorFailures requiring attention

When to Use Info Logging:

Log before and after every domain action or permanent state change:

  • Database write operations (create, update, delete)
  • User actions (login, logout, password change)
  • Outgoing calls (HTTP requests to external services)
  • State transitions (order placed, payment processed)
  • Business events (subscription activated, invoice generated)

Required Log Fields:

FieldRequiredDescription
levelYesLog level (debug, info, warn, error)
timeYesISO 8601 timestamp
componentYesSource component (operator, controller, dal)
msgYesHuman-readable message
traceIdAutoOpenTelemetry trace ID
spanIdAutoOpenTelemetry span ID
userIdContextUser ID if authenticated
requestIdContextRequest correlation ID
errorOn errorError object with stack

Logging by Layer: Loggers are created by the Operator layer and passed down. Each layer creates a child logger with its component name.

Security Rule: Never log sensitive data (passwords, tokens, credit cards, PII beyond IDs).

Metrics

Required Metrics:

MetricTypeLabelsLayer
http.server.request.durationHistogrammethod, route, statusOperator
http.server.request.countCountermethod, route, statusOperator
db.client.operation.durationHistogramoperation, tableDAL
db.client.connection.pool.usageUpDownCounterstate (active/idle)DAL
business.operation.countCounteroperation, resultModel

Metric Naming: Follow OpenTelemetry semantic conventions:

  • Use . as separator (e.g., http.server.request.duration)
  • Prefix with namespace (http, db, business)
  • Include unit in name if not obvious (e.g., duration, count, size)

Spans

Wrap business operations with spans using @opentelemetry/api.

Span Attributes: Use OpenTelemetry semantic conventions:

AttributeTypeExample
http.methodstringGET, POST
http.routestring/api/users/:id
http.status_codeint200, 404
db.systemstringpostgresql
db.operationstringSELECT, INSERT
db.statementstringSQL query (sanitized)
user.idstringUser identifier

Telemetry Rules

  1. Initialize in Operator: Telemetry is created in Operator before other modules
  2. Structured logs only: All logs must be JSON with required fields
  3. Include trace context: Use withTraceContext() for all logs
  4. No sensitive data: Never log passwords, tokens, or PII
  5. Appropriate levels: Use correct log level for each message
  6. Custom spans for business ops: Wrap use-cases with spans
  7. Standard metric names: Follow OpenTelemetry semantic conventions
  8. Layer-specific metrics: Each layer records its own metrics

File Naming

CRITICAL: Use lowercase_with_underscores for ALL filenames

✅ create_user.ts, find_user_by_id.ts, user_repository.ts
❌ createUser.ts (camelCase), CreateUser.ts (PascalCase), create-user.ts (kebab-case)

Intra-Module Imports

Inside a module, nothing should ever import from its own index.ts. All imports within a module must use relative paths. The barrel is the module's public API for external consumers only. For nested modules, the same barrel rules apply.

// Given this structure:
// controllers/
// ├── index.ts          ← barrel: re-exports all routers
// ├── users/
// │   ├── index.ts
// │   └── users_router.ts
// └── health/
//     ├── index.ts
//     └── health_router.ts

// In controllers/users/users_router.ts:

// GOOD: relative path to sibling sub-module barrel
import { healthCheck } from '../health';

// BAD: importing from own module's barrel
import { healthCheck } from '@/controllers';
import { healthCheck } from '@/controllers/health';

Implementation Order

When implementing a new feature, follow this order to minimize rework and ensure clean architecture:

Step 1: Contract First (API Design)

Start in components/contract/:

  1. Define the endpoint in openapi.yaml - request/response schemas
  2. Generate types via the system CLI (contract generate-types)
  3. Import types into server via workspace package

This ensures types flow from contract → server → frontend.

Step 2: Model (Business Logic)

Start in src/model/:

  1. Add definitions - Types specific to this feature in definitions/
  2. Create use-case - One file per use-case in use-cases/
  3. Define Dependencies interface - What the use-case needs injected
  4. Write tests - Test use-case with mocked dependencies

Model is pure business logic with no external dependencies.

Step 3: DAL (Data Access)

In src/dal/:

  1. Add database functions - One function per file
  2. Map to domain types - Convert DB rows ↔ domain objects
  3. Use parameterized queries - Never string concatenation

Step 4: Controller (Wiring)

In src/controller/:

  1. Add HTTP handler - In appropriate http_handlers/ file
  2. Create Dependencies - Wire DAL + external services
  3. Call use-case - Pass Dependencies and request data
  4. Map response - Convert use-case result to HTTP response

Step 5: Operator (If Needed)

In src/operator/:

Only modify if the feature requires:

  • New external service clients
  • New lifecycle events
  • New infrastructure capabilities

Implementation Checklist

StepLocationWhat to Do
1contract/openapi.yamlDefine endpoint, request/response schemas
2model/definitions/Add feature-specific types
3model/use-cases/Implement business logic
4model/dependencies.tsExtend Dependencies if needed
5dal/Add database functions
6controller/http_handlers/Add HTTP handler
7controller/create_controller.tsWire Dependencies
8TestsTest each layer independently

Anti-Patterns to Avoid

  • Starting with HTTP handler - Leads to business logic in controller
  • Skipping contract - Types become inconsistent between layers
  • DAL before Model - Data structure drives design instead of business needs
  • Modifying Operator for features - Operator is infrastructure, not features

Config Schema

Server components require configuration from components/config/. The backend-scaffolding skill generates the initial server structure including a minimal config schema with port, host, and logLevel fields.


Summary Checklist

Before committing backend code, verify:

  • Entry point (src/index.ts) only imports and starts operator
  • Operator provides raw I/O only - no domain knowledge
  • Operator handles Unix signals (SIGTERM, SIGINT, SIGHUP) for graceful shutdown
  • Config is the only layer accessing process.env
  • Controller combines I/O + config for domain operations
  • Model receives Dependencies, never imports external modules
  • DAL has one function per file, no repository pattern
  • All filenames use lowercase_with_underscores
  • Telemetry initialized in Operator before other modules
  • All layers use structured logging with required fields
  • No sensitive data in logs
  • Implementation followed correct order: Contract → Model → DAL → Controller

Input / Output

This skill defines no input parameters or structured output.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Standards for authoring SDD plugin agents — frontmatter, self-containment, skill references, and no-user-interaction rules.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Scaffolds Node.js/TypeScript backend components with CMDO architecture, driven by component settings.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Intent-to-command mappings for fullstack-typescript tech pack features.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Create change specification and implementation plan with dynamic phase generation. Supports feature, bugfix, refactor, and epic types.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Orchestrates the full change lifecycle — routes actions to phase-specific sub-files.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Scaffolds CI/CD pipeline definitions using GitHub Actions for build, test, lint, and deploy workflows.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

LiorCohen のスキルをすべて見る

このスキルの問題を報告する