Skip to content
cccskills
FreePublished on GitHub

backend-standards

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

The English summary is being prepared. Showing the original description.

Works withKubernetes
See how to install

Files included (1)

  • SKILL.md17.5 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

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

The English summary is being prepared. Showing the original description.

LiorCohen/sdd43Updated Feb 24, 2026

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

The English summary is being prepared. Showing the original description.

LiorCohen/sdd43Updated Feb 24, 2026

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

The English summary is being prepared. Showing the original description.

LiorCohen/sdd43Updated Feb 24, 2026

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

The English summary is being prepared. Showing the original description.

LiorCohen/sdd43Updated Feb 24, 2026

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

The English summary is being prepared. Showing the original description.

LiorCohen/sdd43Updated Feb 24, 2026

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

The English summary is being prepared. Showing the original description.

LiorCohen/sdd43Updated Feb 24, 2026

See all skills by LiorCohen

Report a problem with this skill