Skip to content
Back to skills

Backend Standards

ASecurity

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

  • 43 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added February 8, 2026
developmenttypescriptgosqlnodekubernetesdebuggingapidatabasefrontendbackend

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add LiorCohen/sdd --skill backend-standards --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Backend Standards?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Backend Standards
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/liorcohen-backend-standards/badge)](https://www.skillsdirectory.com/skills/liorcohen-backend-standards)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: backend-standards
description: CMDO architecture standards for Node.js/TypeScript backends with strict layer separation.
---


# 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)

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

### Key Distinction: Infrastructure vs Domain

| Layer | Knows About | Example |
|-------|-------------|---------|
| **Operator** | Infrastructure only | "Here's a DB connection and generic HTTP client" |
| **Config** | URLs and settings | `paymentGatewayUrl: "https://api.stripe.com"` |
| **Controller** | Domain concerns | "Use httpClient + paymentGatewayUrl to charge a card" |
| **Model** | Business 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

| Layer | Path | Responsibility |
|-------|------|----------------|
| **Entry Point** | `src/index.ts` | Bootstrap only (ONLY file with side effects) |
| **Operator** | `src/operator/` | Raw I/O capabilities (DB, HTTP clients, cache), lifecycle management, state machine |
| **Config** | `src/config/` | Environment parsing, validation, type-safe config |
| **Controller** | `src/controller/` | Request/response handling, combines I/O + config for domain calls, creates Dependencies for Model |
| **Model** | `src/model/` | Business logic (definitions + use-cases), receives Dependencies |
| **DAL** | `src/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

```typescript
// 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:**
```text
IDLE → STARTING:PROBES → STARTING:DATABASE → STARTING:HTTP_SERVER → RUNNING
                                                                        ↓
STOPPED ← STOPPING:PROBES ← STOPPING:DATABASE ← STOPPING:HTTP_SERVER ←─┘
```

**Unix Signal Handling:**

| Signal | Source | Action |
|--------|--------|--------|
| `SIGTERM` | Kubernetes pod termination, `kill` command | Graceful shutdown |
| `SIGINT` | Ctrl+C from terminal | Graceful shutdown |
| `SIGHUP` | Terminal hangup | Graceful 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:**
```text
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:**
```text
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:**
```text
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

```typescript
// ✅ 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:**
```text
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:**
```text
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:**

| Level | When to use |
|-------|-------------|
| `debug` | Detailed debugging (disabled in production) |
| `info` | Normal operations, state changes, requests |
| `warn` | Recoverable issues, deprecations |
| `error` | Failures 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:**

| Field | Required | Description |
|-------|----------|-------------|
| `level` | Yes | Log level (debug, info, warn, error) |
| `time` | Yes | ISO 8601 timestamp |
| `component` | Yes | Source component (operator, controller, dal) |
| `msg` | Yes | Human-readable message |
| `traceId` | Auto | OpenTelemetry trace ID |
| `spanId` | Auto | OpenTelemetry span ID |
| `userId` | Context | User ID if authenticated |
| `requestId` | Context | Request correlation ID |
| `error` | On error | Error 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:**

| Metric | Type | Labels | Layer |
|--------|------|--------|-------|
| `http.server.request.duration` | Histogram | method, route, status | Operator |
| `http.server.request.count` | Counter | method, route, status | Operator |
| `db.client.operation.duration` | Histogram | operation, table | DAL |
| `db.client.connection.pool.usage` | UpDownCounter | state (active/idle) | DAL |
| `business.operation.count` | Counter | operation, result | Model |

**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:

| Attribute | Type | Example |
|-----------|------|---------|
| `http.method` | string | `GET`, `POST` |
| `http.route` | string | `/api/users/:id` |
| `http.status_code` | int | `200`, `404` |
| `db.system` | string | `postgresql` |
| `db.operation` | string | `SELECT`, `INSERT` |
| `db.statement` | string | SQL query (sanitized) |
| `user.id` | string | User 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**

```text
✅ 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.

```typescript
// 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

| Step | Location | What to Do |
|------|----------|------------|
| 1 | `contract/openapi.yaml` | Define endpoint, request/response schemas |
| 2 | `model/definitions/` | Add feature-specific types |
| 3 | `model/use-cases/` | Implement business logic |
| 4 | `model/dependencies.ts` | Extend Dependencies if needed |
| 5 | `dal/` | Add database functions |
| 6 | `controller/http_handlers/` | Add HTTP handler |
| 7 | `controller/create_controller.ts` | Wire Dependencies |
| 8 | Tests | Test 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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.