---
name: nodejs-backend-patterns
description: "Node.js 22/24 services in ESM: structure, validated config, error mapping, graceful shutdown, AsyncLocalStorage, streams, queues, node:test. Use when building or reviewing a Node.js backend."
metadata:
  risk: safe
  source: multi-agent-pipeline
  date_added: "2026-09-25"
---

# Node.js Backend Patterns

A Node service fails in a few characteristic ways: one blocking call stalls every
request, a forgotten `await` loses an error, a deploy kills requests mid-way,
or a missing environment variable surfaces as `undefined` three layers deep at
2 a.m. The patterns below exist to make those failures impossible or loud.

Assumes Node 22 or 24 LTS and ES modules. Type-level guidance lives in
`typescript-patterns`; schema and query work in `database-patterns`; test
strategy beyond the runner in `testing-backend`.

## 1. Core rules

Load `references/runtime-patterns.md` for the full code. The rules it implements:

- **Project shape**: group by feature, keep HTTP at the edge, and build the
  server from a side-effect-free `buildApp(config, deps)` factory; `main.ts`
  only loads config, builds, listens and handles signals.
- **Configuration**: parse `process.env` once with a schema (zod), freeze the
  result, exit on invalid input, and pass `config` down; no `process.env` reads
  elsewhere and no committed `.env` secrets.
- **Errors**: expected failures throw a typed `AppError` (stable code, HTTP
  status); unexpected ones reach one error handler that returns RFC 9457 problem
  details without leaking internals. Crash on `uncaughtException` and
  `unhandledRejection` and let the orchestrator restart. Lint with
  `no-floating-promises` and `no-misused-promises`.
- **Graceful shutdown**: on `SIGTERM`, fail readiness, stop accepting work,
  finish in-progress requests and jobs, close pools, exit within a bounded
  grace period. Liveness stays cheap and never touches dependencies.
- **Request context**: `AsyncLocalStorage` carries request id and principal for
  logging and tracing; business inputs stay explicit parameters.
- **Timeouts**: every outbound call gets `AbortSignal.timeout` (combined with
  the request's signal via `AbortSignal.any`); set statement, pool-acquire and
  server timeouts.
- **Streams**: stream large payloads through `stream/promises` `pipeline`,
  which propagates errors and destroys every stage; avoid manual `.pipe()`.

## 2. Keep the event loop free

One thread runs all JavaScript. Anything CPU-heavy or synchronous blocks every
request.

- Never use `*Sync` fs or crypto calls in request paths.
- Large `JSON.parse`/`JSON.stringify`, image processing, PDF rendering,
  compression of big payloads, and bcrypt/argon2 with high cost go to worker
  threads (`node:worker_threads`, or a pool like `piscina`) or a separate job.
- Monitor loop delay with `perf_hooks.monitorEventLoopDelay()` and export it as
  a metric; a p99 above ~100 ms means something is blocking.

## 3. Background work

- Anything that can fail independently of the response (emails, webhooks,
  thumbnails, third-party sync) goes on a durable queue: BullMQ on Redis,
  pg-boss on Postgres, or a cloud queue (SQS, Pub/Sub).
- Jobs must be idempotent and carry an id; queues deliver at least once.
- Configure retries with exponential backoff and a dead-letter destination.
- Write the domain change and the "job to enqueue" record in one transaction
  (outbox pattern) when losing the job would be a bug.
- Run workers as a separate process type from the HTTP server so they scale and
  deploy independently.

## 4. Testing and framework choice

Tests build the app from `buildApp(config, fakeDeps())` and call it in memory
(`app.inject()` on Fastify, `supertest` on Express) with the built-in
`node:test` runner. Worked examples, runner flags and a comparison of Fastify,
Express 5, NestJS and Hono, plus logging and observability defaults (pino,
redaction, OpenTelemetry), are in `references/testing-and-frameworks.md`.

## Review checklist

- [ ] Config parsed and validated once at startup; no scattered `process.env`.
- [ ] No floating promises; one central error handler returning problem details.
- [ ] `SIGTERM` drains requests, jobs and pools within a bounded grace period.
- [ ] Readiness and liveness are separate; liveness does not touch dependencies.
- [ ] Every outbound call and query has a timeout.
- [ ] No synchronous or CPU-heavy work on request paths.
- [ ] Large payloads stream through `pipeline`.
- [ ] Side effects that may fail run as idempotent queued jobs.
- [ ] Logs are structured, correlated by request id, and redact secrets.
- [ ] Tests build the app from a factory with injected dependencies.

## References

- `references/runtime-patterns.md`: layout, config, errors, shutdown, request context, timeouts and streams in full.
- `references/testing-and-frameworks.md`: `node:test` examples, framework comparison, logging and OpenTelemetry defaults.

## Related skills

- `api-patterns`, `rest-api-design`: the contract this service implements.
- `api-security-best-practices`: authentication, authorization, rate limits.
- `typescript-patterns`: types, Zod, strict mode.
- `database-patterns`: schema, migrations, query tuning.
- `testing-backend`: test pyramid and fixtures.
- `docker-expert`, `observability-engineer`: packaging and running it.
