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:
$ 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/**/*.tsapps/**/src/**/*.tslibs/**/src/**/*.tspackages/**/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 id | Severity | Catches |
|---|---|---|
nodejs/floating-promises | BLOCKER | 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 | BLOCKER | 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 | BLOCKER | 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 | BLOCKER | 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 | BLOCKER | `await` in a loop for independent operations — require `Promise.all` / `Promise.allSettled` with a bounded batch size. |
nodejs/config-outside-configservice | BLOCKER | 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 | BLOCKER | Errors 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-controller | HIGH | Business logic in controllers — controllers translate HTTP only; logic lives in providers/services. |
nodejs/forwardref-circular-dependency | HIGH | Circular module dependencies "solved" with `forwardRef` where extraction of a shared module is possible. |
nodejs/raw-http-client | HIGH | Raw `axios`/`fetch` calls without timeout, and without going through the service's shared HTTP client (interceptors carry auth, tracing, retries). |
nodejs/missing-guard | HIGH | New endpoint missing guard coverage (authn/authz) — verify global guard applies or explicit guard present. |
nodejs/orm-n-plus-one | HIGH | TypeORM/Prisma queries inside loops (N+1) — batch with `In()`/`findMany`. |
nodejs/transaction-spans-remote-call | HIGH | Transactions spanning external HTTP calls or message publishes. |
nodejs/json-parse-external-input | HIGH | `JSON.parse` on external input without size limits or try/catch at the boundary. |
nodejs/missing-listener-teardown | HIGH | Event emitter / interval / stream listeners registered without teardown in `onModuleDestroy`. |
nodejs/console-logging | HIGH | Logging via `console.*` instead of the injected structured logger. |
nodejs/dto-input-output-shared | SUGGESTION | DTO classes reused for both input and output — split; output shape is a contract. |
nodejs/lazy-import-heavy-modules | SUGGESTION | Heavy modules imported at top level but used in one rarely-hit path — lazy import. |
nodejs/magic-values | SUGGESTION | Magic status codes/strings — use `HttpStatus` and shared enums. |
nodejs/hot-loop-allocation | SUGGESTION | Repeated `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:
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
# 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.