Node.js Service Scaffold
When to use
Invoke when starting a new Node.js backend service, standardizing an existing service baseline, modernizing a service that lacks validated config, structured logging, health probes, or layered error handling, or preparing a Node.js service for container deployment.
Do not use for: auth, session, or OWASP review work (use nodejs-auth-and-security-review), observability vendor wiring — OpenTelemetry, prom-client, SLO/alerts (use nodejs-observability-readiness), queue, broker, or event integration (use nodejs-queue-and-event-integration), or performance and resilience gating — backpressure, circuit breakers, load-test gates (use nodejs-performance-and-resilience).
Inputs
Required:
- Service name and the business capability it serves.
- Approved
backend-architecture.md, or explicit confirmation that backend architecture is intentionally deferred.
Optional:
- Approved
architecture/securitydecisions on auth provider, session model, and secret handling. - Framework choice if
backend-architecture.mdis silent (Fastify default for new services; Express for ecosystem-constrained services; NestJS if a module/DI framework is mandated). - DI container preference if silent (the framework's built-in for NestJS;
awilixdefault otherwise). - Data layer named by architecture (Prisma / Drizzle / TypeORM) — registered as a seam only, not implemented here.
- Target Node.js LTS (20+ default), package manager, and monorepo vs single-service repo placement.
Operating rules
- Never generate tutorial-grade scaffolding. Assume multiple environments, structured logging, container deployment, graceful shutdown, and operational ownership.
- Consume
backend-architecture.md; do not invent decisions. Framework, domain boundaries, data layer, and contracts belong tobackend-architecture.md; auth provider and secret handling belong toarchitecture/security. If either is silent on a decision this scaffold needs, pause and raise an ADR candidate rather than guessing. - Owns the DI and principal-provider baseline only — the shell, not the flow. Token verification, session, and protected-route logic belong to
nodejs-auth-and-security-review. Scaffold the seam; do not implement the flow. - Secure by default: configuration is validated at boot and fails fast; no secrets in source, committed config, the image, or
.env; the container runs as a non-root user from a digest-pinned base image. - Configuration is validated, not read ad hoc: a single typed, frozen config object built from a schema (zod or equivalent) at startup. Missing or malformed required variables abort boot with a clear message — never a runtime
undefined. - Error handling is layered in a specific order:
process.on('uncaughtException')andprocess.on('unhandledRejection')(log fatal, flush, exit non-zero) → the framework error handler (Express error middleware / FastifysetErrorHandler/ Nest exception filter; structured body, no stack or internals in non-dev) → graceful shutdown onSIGTERM/SIGINT(stop accepting, drain in-flight, close server and registered resources within a bounded timeout, then exit). All four are required. - Observability is a seam, not an implementation: a structured pino logger bound to an AsyncLocalStorage request context (request id correlated) is mandatory in the baseline; OpenTelemetry, metrics, and alerting are explicitly deferred to
nodejs-observability-readinessbehind a no-op tracer/metrics interface. - Health probes are mandatory: a liveness endpoint (process is up) and a readiness endpoint (registered dependency checks pass) are separate and wired before any feature route.
- A scaffold that does not build is not done. Run typecheck, lint, the test run, and a boot smoke check before declaring completion; fix and re-run on failure.
- Confirm the target directory before writing files. Recommend
services/<service-name>/in a monorepo or repo root for a single-service repo. Refuse to write into a plugin or skill repository without explicit user override.
Output contract
The generated service shell MUST conform to:
- deployment-standards — env-agnostic image; runtime config injected, not baked; one artifact per environment; non-root container.
- observability-standards — structured logging with request-id correlation and an environment tag; tracer/metrics seam present.
- security-standards — no secrets in source, committed config, or image; config fails fast on missing required secrets.
- naming-conventions — service, module, and file naming.
Upstream contract: backend-architecture.md is the source of truth for framework, domain boundaries, data layer, and contracts; architecture/security is the source of truth for auth provider, session model, and secret handling. If either is silent on a decision this scaffold needs, pause and raise an ADR candidate rather than guessing.
Progressive references
- Read
references/nodejs-scaffold-playbook.mdwhen implementing any owned area or checking the anti-pattern list. - Read
references/nodejs-scaffold-quality-rubric.mdbefore declaring the scaffold complete. - Use
assets/nodejs-service-scaffold.template.mdas the directory-layout, config, and entry-point reference.
Process
- Gather context: service name, business capability, target directory, target Node LTS, package manager, and the auth provider decision from
architecture/security. Loadbackend-architecture.mdand extract framework, domain boundaries, data layer, and contracts. If a needed decision is missing, raise an ADR candidate before proceeding. Confirm the target directory is not a plugin or skill repository. - Generate the project layout:
package.json(deps pinned exactly — no^or~,engines.nodeset),tsconfig.json(strict: true),eslint/lint config,.gitignore, and the directory structure (feature/domain layout perbackend-architecture.md). Referenceassets/nodejs-service-scaffold.template.mdfor the canonical layout. - Generate validated configuration: a schema (zod or equivalent) in
src/config/index.tsparsingprocess.envat boot, producing a single frozen typed config object; abort with a clear message on any missing or malformed required variable. Create.env.exampledocumenting every variable with placeholder values only. - Generate the structured logging and request-context seam: a pino logger in
src/observability/logger.ts; an AsyncLocalStorage request context insrc/observability/context.ts; framework middleware/hook that assigns or propagates a request id and binds a child logger. Add a no-op tracer/metrics interface insrc/observability/telemetry.tswith explicit TODO comments namingnodejs-observability-readinessas the owner. - Generate the layered error handling: process-level
uncaughtExceptionandunhandledRejectionhandlers (log fatal, flush logs, exit non-zero) insrc/server/process-handlers.ts; the framework error handler (Express error middleware / FastifysetErrorHandler/ Nest exception filter) returning a structured body with no stack or internals in non-dev; graceful shutdown onSIGTERM/SIGINTinsrc/server/shutdown.tsthat stops accepting connections, drains in-flight requests, closes the server and registered resource hooks within a bounded timeout, then exits. - Generate health probes: a liveness endpoint (
/healthz) returning 200 when the process is up, and a readiness endpoint (/readyz) iterating registered dependency-check hooks and returning 503 until all pass. Wire both before any feature route. - Generate the DI and principal-provider baseline: the container from
backend-architecture.md(Nest built-in, orawilix/tsyringe) insrc/container/index.ts. Register the principal/auth-context provider as a typed shell. Add explicit TODO comments namingnodejs-auth-and-security-reviewas the owner of token verification, session, and protected-route logic, and the data-layer client as a seam owned by the data implementation. - Generate container packaging: a multi-stage
Dockerfile(build stage, slim runtime stage) running as a non-root user from a digest-pinned base image, a.dockerignore, and a documentedHEALTHCHECKhitting/healthz. No secrets baked; runtime config arrives via environment. - Generate local-run documentation in the service README: how to run each environment, every required environment variable (mirrored from
.env.example), the runtime-config contract, and the explicit table of seams downstream archetypes fill (auth, observability vendor, queue/event, performance gates, data layer). - Build verification (mandatory): run
tsc --noEmit, the lint command, the test command (vitest/jest), and a boot smoke check (start the server, assertGET /healthzreturns 200, shut down cleanly). Fix and re-run on failure. Then validate against the standards in the Output contract; revise until all pass or explicitly document any unresolved gap in the README.
Outputs
Required:
- TypeScript service source tree with
package.json(pinned deps,engines.node),tsconfig.json(strict), and lint config. - Validated, frozen typed configuration with fail-fast boot and
.env.example. - Structured pino logger + AsyncLocalStorage request context + request-id correlation; no-op tracer/metrics seam.
- Layered error handling: process handlers, framework error handler, graceful shutdown (all wired).
- Separate liveness (
/healthz) and readiness (/readyz) probes. - DI container with a typed principal-provider shell (auth flow explicitly deferred).
- Multi-stage non-root
Dockerfilefrom a digest-pinned base,.dockerignore, documentedHEALTHCHECK. - Service README listing all seams downstream archetypes fill.
Output rules:
- Generated files are functional and the service boots, not placeholder-heavy.
- No secrets in source, committed config,
.env.example, or the image. - The principal provider is a seam, not a flow — token verification, session, and route guards are explicitly deferred to
nodejs-auth-and-security-review. - The same image runs in every environment; configuration arrives at runtime via environment variables.
Quality checks
-
tsc --noEmitreports zero errors and the lint command passes. - The test command runs and the boot smoke check (
GET /healthz→ 200, clean shutdown) passes, or a skip is documented with reason. -
package.jsonpins every dependency (no^/~) and setsengines.node. - Configuration is schema-validated at boot and aborts on a missing required variable — verified by removing one required var and observing a clear non-zero exit.
- No secrets appear in source, committed config,
.env.example, or the built image. - All four error layers are wired:
uncaughtException,unhandledRejection, framework error handler, graceful shutdown onSIGTERM/SIGINT. - The framework error handler returns no stack trace or internal details in non-dev environments.
-
/healthzand/readyzare separate;/readyzreturns 503 until registered dependency checks pass. - The pino logger binds a request id from AsyncLocalStorage; the tracer/metrics seam is a no-op with a TODO naming
nodejs-observability-readiness. - The DI principal provider is a typed shell tied to an
architecture/securitydecision; token/session logic is explicitly deferred with TODO comments. - The
Dockerfileis multi-stage, runs as non-root, uses a digest-pinned base, and bakes no secrets. - The service README lists all seams downstream archetypes are expected to fill.
References
- Upstream:
architecture/backend-architecture,architecture/security. - Related Node.js archetype skills (extend this baseline):
nodejs-auth-and-security-review,nodejs-observability-readiness,nodejs-queue-and-event-integration,nodejs-performance-and-resilience. - Standards:
deployment-standards,observability-standards,security-standards,naming-conventions.