Install
openclaw skills install compound-eng-nodejs-backendNode.js backend patterns: layered architecture, TypeScript, validation, error handling, security, deployment. Use when building REST APIs, REST endpoints, middleware, Express/Fastify/Hono/NestJS/Koa servers, tRPC procedures, Bun servers, or server-side TypeScript.
openclaw skills install compound-eng-nodejs-backendVerify before implementing: For framework-specific APIs (Express 5, Fastify 5, Node.js 22+ built-ins), search current docs via search_docs before writing code. Training data may lag current releases.
| Context | Choose | Why |
|---|---|---|
| Edge/Serverless | Hono | Zero-dep, fastest cold starts |
| Performance API | Fastify | Higher throughput than Express, built-in schema validation |
| Enterprise/team | NestJS | DI, decorators, structured conventions |
| Legacy/ecosystem | Express | Most middleware, widest adoption |
Ask user: deployment target, cold start needs, team experience, existing codebase.
src/
├── routes/ # HTTP: parse request, call service, format response
├── middleware/ # Auth, validation, rate limiting, logging
├── services/ # Business logic (no HTTP types)
├── repositories/ # Data access only (queries, ORM)
├── config/ # Env, DB pool, constants
└── types/ # Shared TypeScript interfaces
import type { } for type-only imports -- eliminates runtime overheadinterface for object shapes (2-5x faster type resolution than intersections)unknown over any -- forces explicit narrowingz.infer<typeof Schema> as single source of truth -- never duplicate types and schemasas assertions -- use type guards insteaddeclare module 'pkg' { const v: unknown; export default v; } in types/ambient.d.tsZod (TypeScript inference) or TypeBox (Fastify native). Validate at boundaries only: request entry, before DB ops, env vars at startup. Use .extend(), .pick(), .omit(), .partial(), .merge() for DRY schemas.
Custom error hierarchy: AppError(message, statusCode, isOperational) → ValidationError(400), NotFoundError(404), UnauthorizedError(401), ForbiddenError(403), ConflictError(409)
Centralized handler middleware:
AppError → return { error: message } with statusCodeconst asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);Codes: 400 bad input | 401 no auth | 403 no permission | 404 missing | 409 conflict | 422 business rule | 429 rate limited | 500 server fault
Contract-first: define route schemas (Zod schemas, Fastify JSON Schema, or OpenAPI spec) before writing handler logic. The schema is the contract -- implementation follows. Generate OpenAPI/Swagger docs from these schemas for interactive API documentation.
{ error: { code, message, details? } } structure. Centralize in the error handler middleware. Callers build error handling once; inconsistent errors force per-endpoint special cases..parse() on request body/params, Fastify schema validation). Services and repositories trust that input was validated at entry -- no redundant checks scattered through business logic./users), max 2 nesting levels (/users/:id/orders)/api/v1/{ data, pagination?: { page, limit, total, totalPages } }?page=1&limit=20&status=active&sort=createdAt,descLocation header on 201. Use 204 for successful DELETE with no body.| Pattern | Use When |
|---|---|
async/await | Sequential operations |
Promise.all | Parallel independent ops |
Promise.allSettled | Parallel, some may fail |
Promise.race | Timeout or first-wins |
Never use readFileSync or other sync methods in production -- use fs.promises or stream equivalents. Offload CPU work to worker threads (Piscina). Stream large payloads.
const env = envSchema.parse(process.env)). If invalid, crash before serving traffic. Never discover a missing env var on the first request that needs it./health (shallow, always 200 if process is alive) and /ready (deep, verifies database, cache, and critical dependencies are reachable). Load balancers probe /ready for traffic routing; monitoring probes /health for process liveness. Don't conflate them.@fastify/under-pressure (or equivalent) -- monitor event loop delay, heap, RSS; return 503 when thresholds exceeded.fast-json-stringify for 2-3x faster serialization.opossum for outbound service calls. States: CLOSED (normal) -> OPEN (failing, return fallback) -> HALF_OPEN (probe). Prevents cascade failures when downstream services are down.as any, non-null assertions on untrusted data, // @ts-ignore), treat it as a design smell and find the typed solutiontsc --noEmit && npm test pass with zero warnings before declaring donetsc --noEmit passes with zero errorsnpm test passes with zero failuresas any, @ts-ignore) in new code