NestJS
House conventions for NestJS backends on v11 / Express 5. Apply them to code you are writing or changing — don't restructure untouched modules unless asked.
Examples throughout use an invented shipment domain purely to make the shape concrete. Read the structure, not the names.
Layering
Two layers, and the dependency arrow points one way only.
core/ domain + infrastructure. No controllers, no HTTP. → core only
app/ features. Controllers, DTOs, guards, middleware, CQRS. → app + core
A core module that needs to reach into a feature is telling you the concept it owns sits in the wrong layer — move the concept down rather than importing upward. Keeping the arrow honest is what lets a core module be driven by an HTTP request, a cron job, a CLI, or a queue consumer without dragging a controller along.
Always apply
These hold for nearly every change, so they live here rather than behind a reference:
- A module owns one domain, and its root barrel exports only the
.module.ts. A provider that isn't exported can be refactored freely; one that is becomes public API.
- Controllers translate HTTP and delegate; services hold the logic. A controller that branches on business rules can't be reused by a job, a CLI, or a queue consumer, and its tests need an HTTP layer to say anything.
- Every request body, query, and param goes through a DTO with
class-validator, behind a ValidationPipe with whitelist: true, forbidNonWhitelisted: true, transform: true. Without whitelisting, an unexpected property rides through into your persistence layer.
- Response DTOs are what leaves the process. Returning an entity leaks whatever a future migration adds to the table — password hashes, internal flags, soft-delete columns — with no code change to notice.
- Domain errors extend one base class and are rendered by one global filter. A bare
Error becomes a 500, so a legitimate "not found" pages someone.
- Global enhancers are registered with
APP_GUARD / APP_PIPE / APP_INTERCEPTOR / APP_FILTER. app.useGlobal*() binds outside any module, so those instances cannot inject anything.
- No domain service imports a vendor SDK. The payment gateway, database client, and mail provider each get a wrapper module; a major SDK bump becomes one module's problem.
Wrong by default
Six behaviours that contradict what the API name suggests, so they get stated here rather than in a reference you would only open if you already suspected a problem. Each one compiles cleanly and fails at runtime.
- Express 5 wildcards must be named.
@Get("files/*") and forRoutes("*") no longer match — write @Get("files/*splat") and forRoutes("{*splat}"). :param? is gone too.
- Exception filters resolve route → controller → global, the inverse of guards and pipes, and interceptors unwind first-in-last-out. A route-scoped filter shadows the global one.
@Res() opts the handler out of the framework's response handling — @HttpCode, @Header, serialization, and every response interceptor stop applying. Add { passthrough: true } unless you are sending the response yourself.
ClassSerializerInterceptor only transforms class instances. Returning { user: new UserEntity() } skips @Exclude() silently and ships the excluded field.
ttl is milliseconds in @nestjs/cache-manager and @nestjs/throttler, and a cache ttl: 0 means never expire, not "don't cache".
ClientProxy.send() returns a cold Observable and HttpService returns one too — nothing happens until something subscribes.
The first and the fifth are version-specific. Check @nestjs/core in package.json before applying anything in this skill marked v11, and read references/nestjs-11-migration.md if the project is older.
Reference map
Read the file that matches the change; don't preload all of them.
| Working on |
Read |
| Directory layout, filenames, barrels, CQRS vs service, persistence |
references/architecture.md |
Providers, tokens, scopes, ModuleRef, circular deps, lifecycle hooks |
references/dependency-injection.md |
| Guards, interceptors, pipes, middleware, decorators, responses, versioning |
references/request-lifecycle.md |
| DTOs, class-validator, serialization, Swagger/OpenAPI |
references/dto-validation-openapi.md |
| Exceptions, exception filters, error messages |
references/errors.md |
ConfigModule, ConfigService, logging |
references/configuration-and-logging.md |
@nestjs/axios, event-emitter, bullmq, throttler, cache-manager, schedule, microservices |
references/ecosystem-packages.md |
| Upgrading from v10, or code that predates Express 5 |
references/nestjs-11-migration.md |
| Tests |
the testing skill's NestJS Jest references |
When you are reviewing rather than writing, references/request-lifecycle.md and references/nestjs-11-migration.md catch the most defects per line read: enhancer ordering and Express 5 route syntax both fail in ways that type-check cleanly.
1---2name: nestjs3description: Opinionated house conventions for NestJS backends — module boundaries and layering, provider wiring and injection scopes, guards/interceptors/pipes/filters, DTOs and validation, domain exceptions, config, and the Nest 11 / Express 5 behaviour that compiles cleanly and fails at runtime. Use whenever the code is NestJS (`@nestjs/*` in package.json, or `@Module`/`@Injectable`/`@Controller`/`@Cron` in the file), including when the question sounds like plain TypeScript, an OOP refactor, a code review, or an ops problem — the Nest-specific answer differs from the generic one. Typical asks: scaffolding a module, adding a controller, provider, or endpoint, wiring an enhancer, a boot-time DI or circular-import failure, a response leaking entity fields despite `@Exclude()`, a cron job firing on every replica, upgrading NestJS, or reviewing a Nest diff. Not for plain Express or Fastify, Angular, frontend, SQL, or CI.4---56# NestJS78House conventions for NestJS backends on v11 / Express 5. Apply them to code you are writing or changing — don't restructure untouched modules unless asked.910Examples throughout use an invented shipment domain purely to make the shape concrete. Read the structure, not the names.1112## Layering1314Two layers, and the dependency arrow points one way only.1516```text17core/ domain + infrastructure. No controllers, no HTTP. → core only18app/ features. Controllers, DTOs, guards, middleware, CQRS. → app + core19```2021A core module that needs to reach into a feature is telling you the concept it owns sits in the wrong layer — move the concept down rather than importing upward. Keeping the arrow honest is what lets a core module be driven by an HTTP request, a cron job, a CLI, or a queue consumer without dragging a controller along.2223## Always apply2425These hold for nearly every change, so they live here rather than behind a reference:2627- **A module owns one domain, and its root barrel exports only the `.module.ts`.** A provider that isn't exported can be refactored freely; one that is becomes public API.28- **Controllers translate HTTP and delegate; services hold the logic.** A controller that branches on business rules can't be reused by a job, a CLI, or a queue consumer, and its tests need an HTTP layer to say anything.29- **Every request body, query, and param goes through a DTO with `class-validator`**, behind a `ValidationPipe` with `whitelist: true`, `forbidNonWhitelisted: true`, `transform: true`. Without whitelisting, an unexpected property rides through into your persistence layer.30- **Response DTOs are what leaves the process.** Returning an entity leaks whatever a future migration adds to the table — password hashes, internal flags, soft-delete columns — with no code change to notice.31- **Domain errors extend one base class and are rendered by one global filter.** A bare `Error` becomes a 500, so a legitimate "not found" pages someone.32- **Global enhancers are registered with `APP_GUARD` / `APP_PIPE` / `APP_INTERCEPTOR` / `APP_FILTER`.** `app.useGlobal*()` binds outside any module, so those instances cannot inject anything.33- **No domain service imports a vendor SDK.** The payment gateway, database client, and mail provider each get a wrapper module; a major SDK bump becomes one module's problem.3435## Wrong by default3637Six behaviours that contradict what the API name suggests, so they get stated here rather than in a reference you would only open if you already suspected a problem. Each one compiles cleanly and fails at runtime.3839- **Express 5 wildcards must be named.** `@Get("files/*")` and `forRoutes("*")` no longer match — write `@Get("files/*splat")` and `forRoutes("{*splat}")`. `:param?` is gone too.40- **Exception filters resolve route → controller → global**, the inverse of guards and pipes, and interceptors unwind first-in-last-out. A route-scoped filter shadows the global one.41- **`@Res()` opts the handler out of the framework's response handling** — `@HttpCode`, `@Header`, serialization, and every response interceptor stop applying. Add `{ passthrough: true }` unless you are sending the response yourself.42- **`ClassSerializerInterceptor` only transforms class instances.** Returning `{ user: new UserEntity() }` skips `@Exclude()` silently and ships the excluded field.43- **`ttl` is milliseconds** in `@nestjs/cache-manager` and `@nestjs/throttler`, and a cache `ttl: 0` means never expire, not "don't cache".44- **`ClientProxy.send()` returns a cold Observable** and `HttpService` returns one too — nothing happens until something subscribes.4546The first and the fifth are version-specific. Check `@nestjs/core` in `package.json` before applying anything in this skill marked v11, and read `references/nestjs-11-migration.md` if the project is older.4748## Reference map4950Read the file that matches the change; don't preload all of them.5152| Working on | Read |53| --- | --- |54| Directory layout, filenames, barrels, CQRS vs service, persistence | `references/architecture.md` |55| Providers, tokens, scopes, `ModuleRef`, circular deps, lifecycle hooks | `references/dependency-injection.md` |56| Guards, interceptors, pipes, middleware, decorators, responses, versioning | `references/request-lifecycle.md` |57| DTOs, class-validator, serialization, Swagger/OpenAPI | `references/dto-validation-openapi.md` |58| Exceptions, exception filters, error messages | `references/errors.md` |59| `ConfigModule`, `ConfigService`, logging | `references/configuration-and-logging.md` |60| `@nestjs/axios`, `event-emitter`, `bullmq`, `throttler`, `cache-manager`, `schedule`, microservices | `references/ecosystem-packages.md` |61| Upgrading from v10, or code that predates Express 5 | `references/nestjs-11-migration.md` |62| Tests | the `testing` skill's NestJS Jest references |6364When you are reviewing rather than writing, `references/request-lifecycle.md` and `references/nestjs-11-migration.md` catch the most defects per line read: enhancer ordering and Express 5 route syntax both fail in ways that type-check cleanly.