Node.js (NestJS)

NestJS services.

What this is

NestJS services. Its BLOCKER tier is almost all boundary and concurrency discipline specific to a singleton-based DI framework: unvalidated DTOs, request-scoped data leaking across users via singleton providers, the event loop blocked by sync APIs, errors swallowed into a generic 500. The HIGH tier is mostly architecture — logic that belongs in a service ending up in a controller, N+1 ORM queries, HTTP clients built without the shared interceptor stack.

How to onboard it

A repository picks up this stack by onboarding on a profile that includes it. redline init detects the profile from what is in the repository, so in most cases this is automatic:

terminal
$ npx redlinegate init  # detects the profile
$ npx redlinegate init --profile fullstack-node  # or name one

2 profiles pull these rules in: fullstack-node, service-node.

Once onboarded, your files match this stack when they fit any of these globs:

  • src/**/*.ts
  • apps/**/src/**/*.ts
  • libs/**/src/**/*.ts
  • packages/**/src/**/*.ts

How to use it — 20 rules

Nothing to run. Once your profile includes Node.js (NestJS), redline init renders these rules into your repository's AI tooling and the reviewer applies them on every pull request. When a review comment cites one of these ids, this table is where to look up what it catches and why.

Rule idSeverityCatches
nodejs/floating-promisesBLOCKERFloating promises. Every promise is `await`ed, returned, or explicitly handled with `.catch` — an unhandled rejection crashes the process on modern Node.
nodejs/blocking-event-loopBLOCKERBlocking the event loop. Sync APIs in request paths (`fs.*Sync`, `child_process.execSync`, `crypto.pbkdf2Sync`), or CPU-heavy loops/JSON parsing of large payloads without worker threads.
nodejs/missing-dto-validationBLOCKERMissing validation at the boundary. Request DTOs must use `class-validator` decorators with a global `ValidationPipe` (`whitelist: true`); no raw `@Body() body: any`.
nodejs/request-state-on-singletonBLOCKERRequest-scoped data in singleton providers. NestJS providers are singletons by default — storing per-request state on `this` bleeds data across users. Use `REQUEST` scope deliberately or AsyncLocalStorage.
nodejs/await-in-loopBLOCKER`await` in a loop for independent operations — require `Promise.all` / `Promise.allSettled` with a bounded batch size.
nodejs/config-outside-configserviceBLOCKERSecrets/config read via `process.env` scattered through code — access only through the typed `ConfigService`/config module, validated at startup (fail fast on missing).
nodejs/error-detail-swallowedBLOCKERErrors caught and converted to generic 500 with details swallowed — use exception filters; preserve cause in logs (structured), never in the response body.
nodejs/logic-in-controllerHIGHBusiness logic in controllers — controllers translate HTTP only; logic lives in providers/services.
nodejs/forwardref-circular-dependencyHIGHCircular module dependencies "solved" with `forwardRef` where extraction of a shared module is possible.
nodejs/raw-http-clientHIGHRaw `axios`/`fetch` calls without timeout, and without going through the service's shared HTTP client (interceptors carry auth, tracing, retries).
nodejs/missing-guardHIGHNew endpoint missing guard coverage (authn/authz) — verify global guard applies or explicit guard present.
nodejs/orm-n-plus-oneHIGHTypeORM/Prisma queries inside loops (N+1) — batch with `In()`/`findMany`.
nodejs/transaction-spans-remote-callHIGHTransactions spanning external HTTP calls or message publishes.
nodejs/json-parse-external-inputHIGH`JSON.parse` on external input without size limits or try/catch at the boundary.
nodejs/missing-listener-teardownHIGHEvent emitter / interval / stream listeners registered without teardown in `onModuleDestroy`.
nodejs/console-loggingHIGHLogging via `console.*` instead of the injected structured logger.
nodejs/dto-input-output-sharedSUGGESTIONDTO classes reused for both input and output — split; output shape is a contract.
nodejs/lazy-import-heavy-modulesSUGGESTIONHeavy modules imported at top level but used in one rarely-hit path — lazy import.
nodejs/magic-valuesSUGGESTIONMagic status codes/strings — use `HttpStatus` and shared enums.
nodejs/hot-loop-allocationSUGGESTIONRepeated `Date.now()` / `new Intl.*` in hot loops — hoist.

Expected output

A finding from this file, and every finding Redline produces, opens with a machine-readable first line — severity, then the rule id in brackets, then the problem in one line:

a finding from this file
Redline/BLOCKER [nodejs/floating-promises]: <one-line problem>

Ids are aggregated per rule, which is how the organisation finds out which rules earn their place and which only generate noise — so a finding without a valid id cannot be measured and counts as untagged. Of the 20 rules here, 7 BLOCKER, 9 HIGH and 4 SUGGESTION. Only a BLOCKER must not merge; a SUGGESTION may be dismissed without justification, and is never upgraded to get attention.

If a rule here fires constantly on code your team has deliberately decided to allow, that is the signal to raise with the standards owner — the rule id is what makes that conversation measurable — not to argue it away comment by comment.

How to edit it

Rules are edited in standards/stacks/nodejs.md and nowhere else. The rendered copies in AGENTS.md, .github/copilot-instructions.md and .github/instructions/ are generated and are overwritten by the next render. A change here propagates to every onboarded repository as a pull request, so treat it as a production change.

  1. Edit the markdownstandards/ is the only place a human edits a rule. Everything under AGENTS.md, .github/copilot-instructions.md and .github/instructions/ is rendered from it and is overwritten by the next render.
  2. node scripts/assign-rule-ids.mjsAssigns a permanent <stack>/<slug> id to any new rule bullet and rewrites the file in place. Do not invent an id by hand. CI runs the same script with --check and fails if a rule is missing one.
  3. node scripts/render-self.mjsRe-renders this repository's own artifacts from the edited source. CI runs it with --check, so stale checked-in output fails the build.
  4. Bump standards/manifest.json → versionRequired in the same pull request as the rule change. Sync pull requests quote the version, so a repository's rendered artifacts always name where they came from.
  5. Add a CHANGELOG.md entryAlso in the same pull request. A standards change with no measurement is an opinion — record the seed score alongside it.
  6. node scripts/validate.mjsThe bundle self-check CI runs: manifest integrity, well-formed rule ids, the severity output contract surviving your edit, glob portability.

Rule ids are permanent. Rewording a rule is fine and keeps its id; renaming or removing an id orphans every historical telemetry record that cited it.

The full file

standards/stacks/nodejs.md · 35 lines · 3.3 KB
# Node.js (NestJS) Service Review Rules

**Scope:** server-side TypeScript in a NestJS service. Do not apply these rules to
`.tsx` files, test specs, or front-end code; if you see React in the file, this rule
set does not apply.

## BLOCKER — request changes

- `nodejs/floating-promises` — **Floating promises.** Every promise is `await`ed, returned, or explicitly handled with `.catch` — an unhandled rejection crashes the process on modern Node.
- `nodejs/blocking-event-loop` — **Blocking the event loop.** Sync APIs in request paths (`fs.*Sync`, `child_process.execSync`, `crypto.pbkdf2Sync`), or CPU-heavy loops/JSON parsing of large payloads without worker threads.
- `nodejs/missing-dto-validation` — **Missing validation at the boundary.** Request DTOs must use `class-validator` decorators with a global `ValidationPipe` (`whitelist: true`); no raw `@Body() body: any`.
- `nodejs/request-state-on-singleton` — **Request-scoped data in singleton providers.** NestJS providers are singletons by default — storing per-request state on `this` bleeds data across users. Use `REQUEST` scope deliberately or AsyncLocalStorage.
- `nodejs/await-in-loop` — **`await` in a loop for independent operations** — require `Promise.all` / `Promise.allSettled` with a bounded batch size.
- `nodejs/config-outside-configservice` — **Secrets/config read via `process.env` scattered through code** — access only through the typed `ConfigService`/config module, validated at startup (fail fast on missing).
- `nodejs/error-detail-swallowed` — **Errors caught and converted to generic 500 with details swallowed** — use exception filters; preserve cause in logs (structured), never in the response body.

## HIGH

- `nodejs/logic-in-controller` — Business logic in controllers — controllers translate HTTP only; logic lives in providers/services.
- `nodejs/forwardref-circular-dependency` — Circular module dependencies "solved" with `forwardRef` where extraction of a shared module is possible.
- `nodejs/raw-http-client` — Raw `axios`/`fetch` calls without timeout, and without going through the service's shared HTTP client (interceptors carry auth, tracing, retries).
- `nodejs/missing-guard` — New endpoint missing guard coverage (authn/authz) — verify global guard applies or explicit guard present.
- `nodejs/orm-n-plus-one` — TypeORM/Prisma queries inside loops (N+1) — batch with `In()`/`findMany`.
- `nodejs/transaction-spans-remote-call` — Transactions spanning external HTTP calls or message publishes.
- `nodejs/json-parse-external-input` — `JSON.parse` on external input without size limits or try/catch at the boundary.
- `nodejs/missing-listener-teardown` — Event emitter / interval / stream listeners registered without teardown in `onModuleDestroy`.
- `nodejs/console-logging` — Logging via `console.*` instead of the injected structured logger.

## SUGGESTION

- `nodejs/dto-input-output-shared` — DTO classes reused for both input and output — split; output shape is a contract.
- `nodejs/lazy-import-heavy-modules` — Heavy modules imported at top level but used in one rarely-hit path — lazy import.
- `nodejs/magic-values` — Magic status codes/strings — use `HttpStatus` and shared enums.
- `nodejs/hot-loop-allocation` — Repeated `Date.now()` / `new Intl.*` in hot loops — hoist.