# Learn NestJS > Free NestJS tutorials, plus how NestJS conventions make agent-generated backend code reviewable. Canonical: https://learn-nestjs.com/ Licence: content free to read and quote with attribution to Learn NestJS (https://learn-nestjs.com/). Maintainer: Code Learning Dojo. Last built 2026-09-06. ## Foundations The syntax and the mental model. Short, runnable, no fluff. - [Hello, World!](https://learn-nestjs.com/hello-world/): A running NestJS application in three commands, and what each generated file is actually for. - [Modules](https://learn-nestjs.com/modules/): The unit of organisation in Nest, and the thing that decides what can see what. - [Controllers and routing](https://learn-nestjs.com/controllers/): Parse the request, call a service, return a value. Everything else in a controller is a mistake. - [Providers and dependency injection](https://learn-nestjs.com/providers-and-di/): How Nest constructs your objects, and the injection patterns worth knowing beyond the constructor. - [DTOs and validation](https://learn-nestjs.com/dto-and-validation/): The boundary where untrusted input becomes a typed object. Get this right and most of your input-handling bugs disappear. - [Guards, authentication and authorisation](https://learn-nestjs.com/guards-and-auth/): Guards answer one question: may this request proceed? Getting the difference between authentication and authorisation right is where most back-end vulnerabilities live. - [Database access and testing](https://learn-nestjs.com/database-and-testing/): Wiring a database into a Nest module, and building the test setup that lets an agent iterate without a container. - [Interceptors](https://learn-nestjs.com/interceptors/): Code that wraps a handler — before and after. Logging, response shaping, caching, timeouts and cleanup all live here. - [Exception Filters](https://learn-nestjs.com/exception-filters/): Turning thrown errors into HTTP responses — in one place, with full detail in the log and none of it leaking to the client. - [Middleware and Lifecycle](https://learn-nestjs.com/middleware-and-lifecycle/): The layer beneath Nest's abstractions, and the module hooks that decide whether your redeploys leak database connections. - [Configuration and Environment](https://learn-nestjs.com/configuration/): An application that refuses to start on a missing secret is far better than one that starts and fails on the first request that needs it. ## AI-Native Configuring agents, harnesses and feedback loops for this language. Updated as the tooling moves. - [Nest conventions make agent-written code reviewable](https://learn-nestjs.com/ai/conventions-for-agents/): The framework's opinionated structure turns out to be exactly what makes generated back-end code fast to review. Here is how to lean on it. - [Writing an AGENTS.md for NestJS](https://learn-nestjs.com/ai/agents-md/): Nest already decides where things go, so this file has one real job: the security configuration the framework will happily let you get wrong. - [Per-tenant token budgets in NestJS](https://learn-nestjs.com/ai/tokenomics/): Nest's interceptors and guards are exactly the right shape for cost control: measure in one place, enforce before the handler runs, and no feature can be added without both. ## Review & Verify How generated code fails in this language, and the checks that catch it before your users do. - [The NestJS mistakes language models actually make](https://learn-nestjs.com/review/failure-modes/): TypeScript catches the type errors. What gets through is authorisation, validation configuration, and logic drifting out of services — and those are the ones that matter. - [Dependency hygiene for NestJS projects](https://learn-nestjs.com/review/dependencies/): npm's problems, plus a framework whose packages must all be on the same major version, plus a decorator ecosystem where a version mismatch produces a runtime error with no useful message. - [Security review checklist for NestJS applications](https://learn-nestjs.com/review/security/): The framework gives you the right places to put security controls. What it will not do is tell you when one is missing — and a missing global is invisible until someone finds it. - [The performance traps in generated NestJS code](https://learn-nestjs.com/review/performance/): Two Nest-specific problems dominate: N+1 queries through the ORM, and request-scoped providers that quietly make your entire dependency graph rebuild on every request. ## Reference pages - [About Learn NestJS, and how we make money](https://learn-nestjs.com/about/): Editorial policy, sourcing, corrections and affiliate disclosure for Learn NestJS, part of the Code Learning Dojo network. - [The NestJS stack we would set up today](https://learn-nestjs.com/tools/): An opinionated NestJS stack: ORM, validation, config, testing, observability, hosting, and the packages worth adding or avoiding. --- # Full text ## Hello, World! Source: https://learn-nestjs.com/hello-world/ NestJS is a Node.js framework that gives a back end the thing Express deliberately does not: structure. Modules, dependency injection, decorators, and a strong convention for where everything goes. That structure is why it is worth learning in 2026. A codebase where every piece has an obvious home is one you can review quickly — and reviewing is now most of the job. ## Create the project ```bash npm i -g @nestjs/cli nest new hello-nest cd hello-nest && npm run start:dev ``` Open `http://localhost:3000`. You will see `Hello World!`. ## What was generated ```text src/ main.ts entry point. creates the app, listens on a port. app.module.ts the root module. wires everything together. app.controller.ts handles HTTP requests. no business logic. app.service.ts the logic. injected into the controller. app.controller.spec.ts ``` That four-file split is the whole mental model, and it repeats at every level of a Nest application. ## The entry point ```ts src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap(); ``` `NestFactory.create` walks the module graph, constructs every provider, resolves every dependency, and hands you an application. ## The controller ```ts src/app.controller.ts import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } } ``` Two things are happening that are easy to miss: 1. `@Controller()` with no argument means this handles the root path. `@Controller('users')` would prefix every route in the class with `/users`. 2. `private readonly appService: AppService` in the constructor is **the whole dependency injection system**. You never write `new AppService()`. Nest reads the type, finds the provider, and passes it in. ## The service ```ts src/app.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } } ``` `@Injectable()` marks the class as something Nest can construct and inject. Business logic lives here — not in the controller. ## The module ```ts src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` A module is a declaration of what belongs together. `controllers` handle requests; `providers` can be injected; `exports` makes a provider available to other modules that import this one. :::tip Why the split matters more than it looks Controllers should parse and serialise. Services should hold logic. Keeping that boundary means your logic is testable without HTTP, and it means an agent asked to "add validation" has an obvious place to put it. Blurring it is the single most common way a Nest codebase goes bad. ::: ## Add a route ```ts src/app.controller.ts @Get('health') health(): { status: string; at: string } { return { status: 'ok', at: new Date().toISOString() }; } ``` `http://localhost:3000/health` now returns JSON. Nest serialises objects automatically and sets the content type. ## Common questions ### Is NestJS just Angular for the back end? The decorator syntax and the dependency injection are deliberately Angular-like, and that is where the resemblance ends. Nest runs on Express or Fastify underneath and is a server framework throughout. ### Do I need to know Express? No, but it helps when you need to drop down a level. Nest exposes the underlying request and response objects when you ask for them, and most Express middleware works unchanged. ### Express or Fastify? Express is the default and has the larger middleware ecosystem. Fastify is faster and the adapter is a one-line change. Start on Express; switch if benchmarks ever tell you to. ## Nest conventions make agent-written code reviewable Source: https://learn-nestjs.com/ai/conventions-for-agents/ Frameworks that impose structure were unfashionable for a decade. They are worth another look now, for a reason that did not exist then: **when a machine writes the code, the value of everything having an obvious place goes up sharply.** A generated Express application can put a database query anywhere. A generated Nest application puts it in a service, injected into a controller, registered in a module — because that is the only shape the framework accepts. So reviewing it is a matter of checking a small number of known locations rather than reading everything. ## What the structure buys you **The CLI generates the skeleton.** `nest g resource orders` produces module, controller, service, DTOs and a spec file, wired together. Ask an agent to use it rather than to write the files freehand, and every feature in your codebase looks the same. **Dependency injection makes tests trivial to write.** Which means an agent can actually produce useful tests, which means the loop closes. **Decorators put the contract next to the code.** `@Roles(Role.Admin)`, `@IsEmail()`, `@HttpCode(201)` are all greppable, and their absence is greppable too. That is a reviewable property. **Global pipes, guards and filters are declared in one place.** Security posture lives in `main.ts` and `app.module.ts` rather than being distributed across every route. ## The AGENTS.md that goes with it The instructions file is where you turn those properties into rules the agent follows every session — commands, the structure contract, and above all the security configuration Nest will happily let you get wrong. That file has a page of its own, with the full template: **[Writing an AGENTS.md for NestJS](/ai/agents-md/)**. The two lines in it that matter most, if you read nothing else: ```markdown - JwtAuthGuard is global via APP_GUARD. Opt out with @Public(), never opt in. - ValidationPipe: { whitelist: true, forbidNonWhitelisted: true, transform: true }. ``` The first fails closed; the per-route alternative fails open. The second is what stands between you and mass assignment. ## The review checks worth automating ```bash # a controller that talks to the database directly grep -rn "PrismaService\|Repository<" src --include="*.controller.ts" # ValidationPipe without whitelist grep -rn "new ValidationPipe" src | grep -v whitelist # routes with no auth decorator in a module that should have them grep -rn "@Public()" src # spreading a DTO straight into an update — mass assignment grep -rn "data: { \.\.\." src # entities with a secret field and no @Exclude grep -rln "passwordHash\|refreshToken" src | xargs grep -L "@Exclude" ``` Five greps, and they cover the failure modes that actually cause incidents in Nest codebases. Put them in a `pnpm check:security` script and run it in CI. ## Where generated Nest goes wrong - **Logic creeping into controllers.** The most common structural drift. Catch it with the grep above and with a line in review. - **`@UseGuards()` per route** instead of a global guard. Fails open. - **`ValidationPipe` with no options.** Mass assignment. - **Request-scoped providers used casually**, which quietly makes the whole injection chain request-scoped. - **Missing `@Type()` on nested DTOs**, so nested validation silently does nothing. - **`forwardRef` sprinkled around** instead of extracting the shared concept. ## Common questions ### Is Nest overkill for a small API? For something with three endpoints, yes — Fastify or Hono will be simpler and faster to start. Nest earns its structure at the point where several people (or several agent sessions) are changing the same codebase and consistency starts to matter more than brevity. ### Does the CLI matter that much? More than it used to. `nest g resource` produces a consistent skeleton every time, which means every feature is shaped identically, which means your review is pattern-matching rather than reading. Ask the agent to use it. ### Do decorators confuse language models? Not noticeably — there is a lot of Nest and Angular in the training data and the patterns are highly regular. The regularity is the point: generated Nest tends to look like existing Nest, which is exactly the property you want. ## The NestJS mistakes language models actually make Source: https://learn-nestjs.com/review/failure-modes/ Nest's structure means generated code lands in predictable places, which makes review fast. The bugs that survive are not type errors — they are configuration and authorisation, and both are invisible to the compiler. ## Security ### 1. Authentication without authorisation The most common real vulnerability in generated back-end code, in any framework. ```ts @Get(':id') findOne(@Param('id') id: string) { return this.invoices.findOne(id); // any logged-in user, any invoice } ``` The guard confirmed *a* valid user. Nothing confirmed it is *their* invoice. **Correct:** the ownership check goes in the service, beside the query. ```ts async findOne(id: string, requester: AuthUser) { const inv = await this.repo.findById(id); if (!inv || inv.ownerId !== requester.id) throw new NotFoundException(); return inv; } ``` Return 404, not 403, for a resource the caller may not see — a 403 confirms the id is real. **Catch it with:** one test per resource type. It is the highest value-per-line test in the codebase. ```ts it('does not leak another user’s invoice', () => request(app.getHttpServer()) .get(`/invoices/${bobsInvoice.id}`) .set('Authorization', aliceToken) .expect(404)); ``` ### 2. Guards applied per-route instead of globally ```ts @UseGuards(JwtAuthGuard) // on each controller that needs it ``` This fails open: the endpoint someone forgets is public. Register the guard with `APP_GUARD` and opt out with `@Public()`. ```ts providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }] ``` **Catch it with:** `grep -rn "@UseGuards" src` — in a correctly configured app there should be very few. ### 3. `ValidationPipe` with no options ```ts app.useGlobalPipes(new ValidationPipe()); // validates declared fields only ``` Undeclared properties pass straight through. Combined with the next item, that is privilege escalation. **Correct:** `new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })`. ### 4. Mass assignment ```ts await this.prisma.user.update({ where: { id }, data: { ...dto } }); ``` If anything undeclared survived validation, it is now written to the database. A client sending `{ "role": "admin" }` gets exactly what it asked for. **Correct:** pick fields explicitly, or rely on `whitelist: true` and still pick fields explicitly. **Catch it with:** `grep -rn "data: { \.\.\." src` ### 5. Entities serialised with their secrets ```ts return this.repo.findOne(id); // returns passwordHash, refreshToken, ... ``` **Correct:** `@Exclude()` on the field plus `ClassSerializerInterceptor` registered globally. **Catch it with:** `grep -rln "passwordHash\|refreshToken" src | xargs grep -L "@Exclude"` ## Validation ### 6. Nested DTOs that are never validated ```ts @ValidateNested({ each: true }) lines!: OrderLineDto[]; // no @Type() — items are not checked ``` `class-transformer` needs `@Type(() => OrderLineDto)` to know what to instantiate. Without it, validation of the array items is silently skipped. Silent is the problem: nothing fails, nothing warns. ### 7. Query and param types Query parameters are always strings. `@IsInt()` on a query DTO without `@Type(() => Number)` (or `enableImplicitConversion`) rejects every valid request. ### 8. Global pipes missing in tests `useGlobalPipes` lives in `main.ts`, which the testing module never runs. Generated e2e tests therefore pass on payloads production would reject. Register the same pipes in the test setup. ## Architecture ### 9. Logic in controllers ```ts @Post() async create(@Body() dto: CreateOrderDto) { const user = await this.prisma.user.findUnique(...); // in a controller if (!user) throw new NotFoundException(); const total = dto.lines.reduce(...); return this.prisma.order.create(...); } ``` Untestable without HTTP, unreusable, and it puts business rules somewhere nobody looks for them. **Catch it with:** `grep -rn "PrismaService\|Repository<" src --include="*.controller.ts"` — that should return nothing. ### 10. Request-scoped providers used casually ```ts @Injectable({ scope: Scope.REQUEST }) ``` Scope is contagious: everything that injects this becomes request-scoped too, all the way up to the controller. On a hot path that is a chain of object construction per request. **Correct:** `AsyncLocalStorage`, or `nestjs-cls`, for request context. ### 11. `forwardRef` as a habit One or two is a design signal. Several means a shared concept wants extracting into a third module. ### 12. Missing shutdown hooks ```ts app.enableShutdownHooks(); // omitted in most generated bootstraps ``` Without it, `onModuleDestroy` never runs and database connections leak on every redeploy. ## Async Generated Nest inherits every TypeScript and JavaScript async failure — floating promises are the big one, particularly in event handlers and background jobs. See [the TypeScript failure modes](https://learn-typescript.org/review/failure-modes/). Nest-specific: an interceptor that does not return the observable, or an `async` guard whose promise is not awaited by the code path you think. ## The review, as five commands ```bash grep -rn "PrismaService\|Repository<" src --include="*.controller.ts" grep -rn "new ValidationPipe" src | grep -v whitelist grep -rn "data: { \.\.\." src grep -rn "@UseGuards" src grep -rln "passwordHash\|refreshToken" src | xargs grep -L "@Exclude" ``` Put them in a `pnpm check:conventions` script and run it in CI. They cover the items on this page that actually cause incidents. :::verdict The short version Nest's conventions make everything else easy to review, so spend the attention on the two things the framework cannot check for you: does every resource endpoint verify ownership, and is `ValidationPipe` configured with `whitelist: true`. ::: :::promo digitalocean ::: ## Common questions ### Why is the ownership check in the service and not a guard? Because it depends on the record. A guard runs before the handler and would have to query the database itself, duplicating the work and drifting out of sync with the real query. Beside the query is where the check stays correct. ### Is `forbidNonWhitelisted` too strict? For an internal API, no — a surprise field means a client bug and you want to hear about it. For a public API where clients may send extra fields harmlessly, `whitelist: true` alone (strip rather than reject) is the friendlier choice. ### Do I need all five grep checks in CI? They take under a second and each one maps to a real incident class. The controller-database one and the ValidationPipe one are the two you should never skip. ## Dependency hygiene for NestJS projects Source: https://learn-nestjs.com/review/dependencies/ Everything in [npm dependency hygiene](https://learn-javascript.org/review/dependencies/) applies — install scripts, slopsquatting, lockfiles, transitive bloat — and that is the larger risk. This page is what is specific to Nest. ## `@nestjs/*` packages must move together Nest is not one package; it is fifteen or twenty that share internals. Mixing majors produces failures that are genuinely hard to diagnose. ```json "dependencies": { "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0", "@nestjs/platform-express": "^11.0.0", "@nestjs/config": "^4.0.0", // its own versioning — check the compat table "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.0", "@nestjs/swagger": "^11.0.0", "@nestjs/testing": "^11.0.0" } ``` The core packages (`common`, `core`, `platform-*`, `testing`, and most first-party modules) share a major version. Satellite packages like `@nestjs/config` and `@nestjs/schedule` have their own numbering, and each release documents which Nest major it supports. The failure when they drift is characteristic and unhelpful: ```text Nest can't resolve dependencies of the FooService (?). Please make sure that the argument Object at index [0] is available in the FooModule context. ``` That message appears for a genuinely missing provider *and* for a version mismatch where two copies of `@nestjs/common` exist and the metadata keys do not match. If the provider is obviously present and you get this, check for duplicates: ```bash npm ls @nestjs/common @nestjs/core # more than one version? that is your bug pnpm why @nestjs/common ``` ```bash # audit alignment npm ls --depth=0 2>/dev/null | grep '@nestjs/' npx npm-check-updates '/@nestjs/.*/' -u # upgrade the family together, never piecemeal ``` **Upgrade the whole family in one commit.** Bumping `@nestjs/core` alone is how you get the error above at 2am. ## The decorator stack is tightly coupled ```json "dependencies": { "reflect-metadata": "^0.2.2", "class-validator": "^0.14.0", "class-transformer": "^0.5.1", "rxjs": "^7.8.0" } ``` Four packages that are effectively part of the framework: **`reflect-metadata`** must be imported exactly once, at the very top of `main.ts`, and there must be exactly one copy in the tree. Two copies means two metadata registries and dependency injection silently stops working for half your providers. ```bash npm ls reflect-metadata # must be one version, deduped ``` **`class-validator` and `class-transformer`** are pre-1.0 and their minor releases have historically been breaking. Pin them exactly rather than with a caret: ```json "class-validator": "0.14.2", "class-transformer": "0.5.1" ``` This is one of the few places where exact pinning is clearly worth the maintenance cost — a surprise change in validation behaviour is a security issue, not just a bug. **`rxjs`** major versions matter because interceptors are built on it. A v7/v8 mismatch between your code and Nest's produces operator errors that look like your mistake. ## Peer dependencies are not optional here Nest uses peer dependencies extensively, and npm 7+ installs them automatically — which hides a problem rather than solving it. A peer warning about `@nestjs/common` is telling you a module was built against a different major. ```bash npm ls 2>&1 | grep -i 'peer dep\|invalid' ``` Treat every one as a bug to fix rather than noise to scroll past. In this ecosystem they are usually correct. ## Reduce the graph Nest applications accumulate dependencies quickly, partly because the documentation demonstrates a module for everything. | Generated reaches for | Often unnecessary | |---|---| | `@nestjs/passport` + a strategy for JWT only | `@nestjs/jwt` and a ~30-line guard | | `bcrypt` (native, needs a build toolchain) | `argon2`, or Node's `crypto.scrypt` | | `moment` | `Intl`, `Temporal` | | `lodash` | `Object.groupBy`, `structuredClone`, `?.`, `??` | | `uuid` | `crypto.randomUUID()` | | `dotenv` directly | `@nestjs/config`, which wraps it | | `axios` for one outbound call | global `fetch` (`@nestjs/axios` if you want DI) | | `cache-manager` + a store for one lookup | a `Map` with a TTL | The Passport one is worth expanding. `@nestjs/passport` plus `passport` plus `passport-jwt` plus their types is four packages to validate a JWT — which `@nestjs/jwt` does in three lines inside a guard you already need to write. Passport earns its place when you have several OAuth providers; for JWT alone it is machinery. `bcrypt` is worth flagging separately: it is a native module, so it needs a compiler in your build image, breaks on Node upgrades, and is a common cause of Docker build pain. `argon2` is the better algorithm anyway, and `@node-rs/argon2` avoids the native build. ## Docker images get large A default Nest Dockerfile produces something close to a gigabyte. That is a deploy-time cost and a security surface. ```dockerfile FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build && npm prune --omit=dev # drop devDependencies FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist USER node # do not run as root CMD ["node", "dist/main.js"] ``` Three things generated Dockerfiles miss: the multi-stage split (so build tools do not ship), `npm prune --omit=dev` or a production-only install, and `USER node`. The last one costs nothing and means a container escape starts from an unprivileged user. ```bash docker images | grep myapp # is it 200MB or 1.2GB? docker scout cves myapp:latest # or trivy image myapp:latest ``` ## Audit and update ```bash npm audit --omit=dev # production tree only npx depcheck # installed and never imported npm ls --depth=0 | grep '@nestjs/' ``` `depcheck` is worth running quarterly on a Nest project specifically, because modules get removed from `app.module.ts` far more often than their packages get removed from `package.json`. A `@nestjs/graphql` you stopped using two years ago is still in your tree, still in your image, still a surface. :::verdict The Nest-specific policy 1. `@nestjs/*` core packages share a major. Upgrade them together, in one commit. 2. `reflect-metadata` must be exactly one copy. Check when DI behaves oddly. 3. Pin `class-validator` and `class-transformer` exactly — pre-1.0, and validation is security. 4. Treat every peer dependency warning as a bug. 5. Multi-stage Docker build, prune dev dependencies, `USER node`. ::: ## Common questions ### Why does Nest say it cannot resolve a provider that is clearly there? Two causes with the same message: the provider is genuinely not in scope (not in `providers`, or in another module that does not `export` it), or there are two copies of `@nestjs/common` in the tree so the metadata keys differ. Check `npm ls @nestjs/common` before you go hunting through modules. ### Should I use Passport? For multiple OAuth providers, yes — that is what it is good at. For JWT validation alone it is four packages replacing a thirty-line guard, and the guard is easier to read and to debug than a strategy plus a module plus a serializer. ### Is pinning `class-validator` exactly worth the maintenance? Yes. It is pre-1.0, its minors have been breaking historically, and a change in validation behaviour is a potential security regression rather than an inconvenience. Upgrade deliberately and read the changelog. ### How do I keep the Docker image small? Multi-stage build, `npm ci --omit=dev` in the runtime stage or `npm prune` after building, an Alpine or distroless base, and a `.dockerignore` that excludes `node_modules`, `.git` and test files. That typically takes a default Nest image from around a gigabyte to a couple of hundred megabytes. ## Modules Source: https://learn-nestjs.com/modules/ A module groups related code and declares its boundary. Everything in a Nest application belongs to exactly one module, and modules decide what is visible outside themselves. ```ts src/users/users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ imports: [], // other modules whose exports we need controllers: [UsersController], providers: [UsersService], // constructible and injectable inside this module exports: [UsersService], // ...and available to modules that import us }) export class UsersModule {} ``` The key rule: **a provider is private to its module unless it is exported.** If `OrdersService` needs `UsersService`, then `UsersModule` must export it and `OrdersModule` must import `UsersModule`. There is no ambient global scope. That constraint is the point. It makes the dependency graph explicit and it means you can see, from one file, everything a feature depends on. ## Feature modules The standard layout is one module per domain concept: ```text src/ app.module.ts users/ users.module.ts users.controller.ts users.service.ts dto/ entities/ orders/ orders.module.ts ... ``` ```ts src/app.module.ts @Module({ imports: [UsersModule, OrdersModule], }) export class AppModule {} ``` ## Shared modules A module exporting a provider is a singleton across the whole application by default. Import it in two places and both get the same instance. ```ts src/database/database.module.ts @Module({ providers: [PrismaService], exports: [PrismaService], }) export class DatabaseModule {} ``` ## Dynamic modules When a module needs configuration, use the `forRoot` / `forRootAsync` convention. ```ts @Module({}) export class MailModule { static forRoot(options: MailOptions): DynamicModule { return { module: MailModule, providers: [ { provide: MAIL_OPTIONS, useValue: options }, MailService, ], exports: [MailService], }; } } ``` ```ts @Module({ imports: [MailModule.forRoot({ apiKey: process.env.MAIL_KEY! })], }) export class AppModule {} ``` ## Global modules `@Global()` makes a module's exports available everywhere without importing it. ```ts @Global() @Module({ providers: [ConfigService], exports: [ConfigService] }) export class ConfigModule {} ``` :::warn Use this sparingly `@Global()` removes exactly the property that makes modules useful — the explicit dependency edge. Config and logging are reasonable; anything domain-specific is not. A codebase where everything is global is an Express app with extra decorators. ::: ## Circular dependencies Two modules importing each other is usually a design smell, but when it is genuinely needed: ```ts @Module({ imports: [forwardRef(() => OrdersModule)] }) export class UsersModule {} ``` Before reaching for `forwardRef`, ask whether the shared piece belongs in a third module that both import. That is the right answer about eighty percent of the time. ## Common questions ### How granular should modules be? One per bounded domain concept, not one per file. `UsersModule` containing the controller, service, DTOs and entities is right. Splitting a service into its own module because it felt big is not. ### Why does my provider say it cannot be resolved? Almost always one of two things: the provider is not in the `providers` array of any module in scope, or it is in another module that does not `export` it. The error message names the missing token and the module it was requested from — read both halves. ### Is `forwardRef` bad? It is a signal, not a sin. It usually means two modules share a concept that wants extracting into a third. If you have more than one or two, look at the shape of your domain rather than adding more. ## Writing an AGENTS.md for NestJS Source: https://learn-nestjs.com/ai/agents-md/ `AGENTS.md` is a Markdown file in your repository root that coding agents read before they start. Claude Code reads `CLAUDE.md`; most other tools read `AGENTS.md`. Write one and symlink: ```bash ln -s AGENTS.md CLAUDE.md ``` Nest is unusual among the languages on this network because the framework has already answered most of the questions an instructions file normally answers. Where does a database query go? In a service. How is a dependency provided? Constructor injection. What shape is a feature? Module, controller, service, DTOs. So the file gets short, and it concentrates on the one thing Nest does *not* decide for you: **security configuration.** ## The two lines that matter most Everything else on this page is secondary to these: ```markdown - JwtAuthGuard is registered globally with APP_GUARD. Opt out with @Public(), never opt in with @UseGuards() per route. - ValidationPipe runs with { whitelist: true, transform: true }. ``` The first fails closed: an endpoint someone forgot to decorate is protected. The per-route alternative fails open, and the endpoint someone forgot is public. That difference is the whole ballgame. The second prevents mass assignment. `new ValidationPipe()` with no options validates the fields you declared and passes everything else straight through — so a client sending `{"role": "admin"}` gets it written to the database if any code path spreads the DTO into an update. Generated Nest bootstraps omit the options constantly. See [the Nest failure modes](/review/failure-modes/). ## The template ```markdown AGENTS.md NestJS 11, TypeScript strict, Prisma, pnpm, Express adapter. ## Commands - Dev: `pnpm start:dev` - Test: `pnpm test` (unit only — must stay under 10s) - E2E: `pnpm test:e2e` - Check: `pnpm check` (eslint + tsc --noEmit + unit tests). Must pass. - One: `pnpm test -- users.service.spec.ts` ## Generating code Use the CLI: `nest g resource `. Do not hand-write module, controller and service files — the skeleton should be identical across every feature. ## Structure - Controllers are ONE LINE per method: parse, delegate, return. - Business logic and database access live in services. - DTOs in `dto/`, one class per operation, validated with class-validator. - Never inject PrismaService or a repository into a controller. - Static route segments are declared BEFORE parameterised ones (`@Get('me')` above `@Get(':id')`) or the parameter swallows them. ## Security — non-negotiable - JwtAuthGuard is global via APP_GUARD. Opt out with @Public(), never opt in. - ValidationPipe: { whitelist: true, forbidNonWhitelisted: true, transform: true }. - Never spread a DTO into a Prisma update. Pick fields explicitly. - @Exclude() on every secret field, with ClassSerializerInterceptor global. - Ownership checks live in the SERVICE, beside the query — not in a guard. Return 404, not 403, for a resource the caller may not see. - Nested DTOs need @Type(() => Child) alongside @ValidateNested, or the items are silently not validated at all. ## Testing - Register the same global pipes in the test setup. main.ts does not run in tests, so without this your e2e tests pass on payloads production rejects. - Use overrideProvider for anything crossing the network. - Every resource endpoint has a test asserting another user gets a 404. ## Avoid - Scope.REQUEST providers — scope is contagious and makes the whole injection chain per-request. Use AsyncLocalStorage / nestjs-cls instead. - forwardRef. One or two is a smell; more means a shared concept wants extracting into a third module. - @Global() on anything domain-specific. Config and logging only. ## Landmines - src/billing/ posts to Stripe. Do not change it without asking. - Migrations: write them, never run them. A human runs migrations. - app.enableShutdownHooks() must stay in main.ts or connections leak on every redeploy. ``` Around sixty lines, and more than half of it is security. That ratio is right for Nest: TypeScript catches the type errors, the framework catches the structural ones, and what is left is the configuration that fails silently. :::tip The test line pays for itself immediately "Register the same global pipes in the test setup" catches a problem that bites nearly every Nest project once: `useGlobalPipes` lives in `main.ts`, the testing module never runs it, so your end-to-end tests validate nothing and pass on input production would reject. ::: ## Enforce the greppable half Several of those rules are checkable in a second, so make them a script rather than a hope: ```json package.json { "scripts": { "check:conventions": "bash scripts/check-conventions.sh" } } ``` ```bash scripts/check-conventions.sh #!/usr/bin/env bash set -Eeuo pipefail fail=0 check() { if grep -rqn "$1" src ${3:-}; then echo "✗ $2"; fail=1; fi; } grep -rn "PrismaService\|Repository<" src --include="*.controller.ts" \ && { echo "✗ database access in a controller"; fail=1; } grep -rn "new ValidationPipe" src | grep -qv whitelist \ && { echo "✗ ValidationPipe without whitelist"; fail=1; } grep -rn 'data: { \.\.\.' src \ && { echo "✗ DTO spread into an update (mass assignment)"; fail=1; } grep -rln "passwordHash\|refreshToken" src | xargs -r grep -L "@Exclude" \ | grep -q . && { echo "✗ secret field without @Exclude"; fail=1; } exit $fail ``` Put it in CI and in `pnpm check`. Four greps, under a second, and they cover the failures that actually cause incidents. ## In a monorepo ```text AGENTS.md commands, structure, security policy apps/api/AGENTS.md route conventions, which guards are global apps/worker/AGENTS.md queue semantics, idempotency requirements libs/shared/AGENTS.md "imported by everything — changes are breaking" ``` ## Common questions ### Does this replace the conventions page? No — [Nest conventions for agents](/ai/conventions-for-agents/) explains *why* the framework's structure makes generated code reviewable and what to grep for. This page is the file you actually put in the repo. ### Why is the ownership check not in a guard? Because it depends on the record. A guard runs before the handler and would have to query the database itself, duplicating the work and drifting out of sync with the real query. Beside the query is where it stays correct — and that is exactly the kind of decision worth writing down, because it is not obvious. ### Should I say which ORM? Yes, in the first line, and never allow a second one. A codebase with both Prisma and TypeORM is one where nobody knows which layer a given query goes through, and generated code will cheerfully use whichever it saw first. ## Controllers and routing Source: https://learn-nestjs.com/controllers/ A controller maps HTTP to method calls. The discipline that keeps a Nest codebase healthy is that it does nothing else. ```ts src/users/users.controller.ts import { Controller, Get, Post, Patch, Delete, Param, Query, Body, HttpCode, ParseIntPipe, } from '@nestjs/common'; @Controller('users') export class UsersController { constructor(private readonly users: UsersService) {} @Get() findAll(@Query('limit', ParseIntPipe) limit = 20) { return this.users.findAll(limit); } @Get(':id') findOne(@Param('id') id: string) { return this.users.findOne(id); } @Post() @HttpCode(201) create(@Body() dto: CreateUserDto) { return this.users.create(dto); } @Patch(':id') update(@Param('id') id: string, @Body() dto: UpdateUserDto) { return this.users.update(id, dto); } @Delete(':id') @HttpCode(204) remove(@Param('id') id: string) { return this.users.remove(id); } } ``` Every method is one line. That is the target. ## Return values Return an object and Nest serialises it as JSON with a 200 (or 201 for `@Post`). Return a promise and it awaits it. Return an Observable and it subscribes. You almost never need the raw response object. If you take `@Res()`, you opt out of Nest's serialisation entirely and become responsible for ending the response yourself — a common source of hung requests. ```ts // avoid unless you genuinely need streaming or a redirect @Get('download') download(@Res() res: Response) { res.setHeader('Content-Type', 'text/csv'); res.send(csv); // you must end it. Nest will not. } ``` Use `@Res({ passthrough: true })` when you only need to set a header or a cookie and still want Nest to serialise the return value. ## Route parameters and order ```ts @Get('me') // must come before ':id' me() {} @Get(':id') findOne(@Param('id') id: string) {} ``` Routes match in declaration order. `@Get(':id')` declared first will swallow `/users/me`. This is one of the two or three most common Nest bugs. ## Status codes and headers ```ts @Post() @HttpCode(201) @Header('Cache-Control', 'no-store') create(@Body() dto: CreateUserDto) {} @Get('old') @Redirect('/users', 301) old() {} ``` ## Sub-resources ```ts @Controller('users/:userId/orders') export class UserOrdersController { @Get() list(@Param('userId') userId: string) { return this.orders.forUser(userId); } } ``` :::warn Authorisation is not routing `/users/:userId/orders` says nothing about whether the caller may read *that* user's orders. Checking that the authenticated user matches `userId` is a guard's job, and forgetting it is the most common real vulnerability in generated back-end code. See [reviewing generated NestJS](/review/failure-modes/). ::: ## Common questions ### Should validation happen in the controller? Declare it — with a DTO class and decorators — and let the global `ValidationPipe` enforce it. That is the controller declaring its contract, not implementing logic. Business rules ("this email is already taken") belong in the service. ### Where does error handling go? Throw a Nest exception (`NotFoundException`, `ForbiddenException`) from the service, and let the built-in exception filter turn it into a response. Controllers should not contain `try/catch` in the normal case. ### Why is my `:id` route catching everything? Because a more specific literal route is declared after it. Move static segments (`me`, `search`, `export`) above parameterised ones. ## Per-tenant token budgets in NestJS Source: https://learn-nestjs.com/ai/tokenomics/ The economics are language-independent — [what tokens cost and where the money goes](https://codelearningdojo.com/token-economics/) is the model, and the [Node page](https://learn-javascript.org/ai/tokenomics/) covers the runtime mechanics. What Nest adds is placement. Cost control has two halves — *measure everything* and *refuse before spending* — and Nest already has a first-class home for each: an interceptor and a guard. Put them there and a new feature cannot ship unmeasured, because the plumbing is global. ## Attribution: AsyncLocalStorage, not a constructor parameter Every LLM call needs to be tagged with tenant, user and route. Threading that through service constructors is how you end up with a request-scoped provider, which is [contagious and slow](/review/failure-modes/). ```ts src/cost/cost-context.ts import { AsyncLocalStorage } from 'node:async_hooks'; export interface CostContext { tenantId: string; userId?: string; route: string; requestId: string; } export const costStore = new AsyncLocalStorage(); ``` ```ts src/cost/cost-context.middleware.ts @Injectable() export class CostContextMiddleware implements NestMiddleware { use(req: Request, _res: Response, next: NextFunction) { costStore.run( { tenantId: (req as any).user?.tenantId ?? 'anonymous', userId: (req as any).user?.id, route: req.route?.path ?? req.path, requestId: (req.headers['x-request-id'] as string) ?? randomUUID(), }, () => next(), ); } } ``` ```ts src/app.module.ts export class AppModule implements NestModule { configure(c: MiddlewareConsumer) { c.apply(CostContextMiddleware).forRoutes('*'); } } ``` Singleton services throughout, full attribution, no scope contagion. ## The quota guard: refuse before you spend A guard runs before the handler, which is precisely where a budget check belongs. An over-quota tenant should never reach the code that makes the call. ```ts src/cost/quota.guard.ts export const QUOTA = 'quota'; export const Quota = (feature: Feature) => SetMetadata(QUOTA, feature); @Injectable() export class QuotaGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly ledger: LedgerService, ) {} async canActivate(ctx: ExecutionContext): Promise { const feature = this.reflector.getAllAndOverride(QUOTA, [ ctx.getHandler(), ctx.getClass(), ]); if (!feature) return true; // not an LLM route const { tenantId } = costStore.getStore() ?? { tenantId: 'anonymous' }; const { spentMicros, capMicros } = await this.ledger.usage(tenantId); if (spentMicros >= capMicros) { throw new HttpException( { statusCode: 429, error: 'Quota exceeded', message: 'Monthly AI usage limit reached.', resetsAt: this.ledger.periodEnd(tenantId), }, HttpStatus.TOO_MANY_REQUESTS, ); } return true; } } ``` ```ts @Quota('reply_draft') @Post('drafts') create(@Body() dto: CreateDraftDto) { return this.drafts.create(dto); } ``` 429 with a `resetsAt` is the right response — it tells the client this is a quota problem and when it clears, rather than a generic failure they will retry into. :::warn Register the guard globally, opt in per route Same reasoning as the [auth guard](/guards-and-auth/): a global guard that checks for the `@Quota()` decorator and no-ops without it means adding the decorator is the only thing to remember. Registering `@UseGuards(QuotaGuard)` per controller fails open — the route someone forgets is unmetered. ```ts providers: [{ provide: APP_GUARD, useClass: QuotaGuard }] ``` ::: ## The interceptor: measure everything An interceptor wraps the whole handler, so it sees the outcome, the timing and — with a small collector — the usage from every model call made inside it. ```ts src/cost/cost.interceptor.ts @Injectable() export class CostInterceptor implements NestInterceptor { constructor(private readonly ledger: LedgerService) {} intercept(ctx: ExecutionContext, next: CallHandler): Observable { const started = Date.now(); const collector = new UsageCollector(); return usageStore.run(collector, () => next.handle().pipe( tap({ next: () => this.flush(collector, started, 'ok'), error: () => this.flush(collector, started, 'error'), }), finalize(() => { // fires on client disconnect too — the abandoned-stream case if (!collector.flushed) this.flush(collector, started, 'aborted'); }), ), ); } private flush(c: UsageCollector, started: number, outcome: Outcome) { c.flushed = true; const ctx = costStore.getStore(); void this.ledger.record({ ...ctx, calls: c.calls, inputTokens: c.input, cachedTokens: c.cached, outputTokens: c.output, costMicros: c.costMicros, latencyMs: Date.now() - started, outcome, }); } } ``` `finalize` is the important operator. It runs on success, on error **and on unsubscribe** — which is what happens when the client disconnects mid-stream. Without it, abandoned requests are the one category of spend that never reaches your ledger, and they are exactly the category you most want to see. Register it globally so no route can be added without accounting: ```ts providers: [{ provide: APP_INTERCEPTOR, useClass: CostInterceptor }] ``` ## Cancel upstream when the client leaves The interceptor records the abandonment. This stops it costing money in the first place. ```ts src/chat/chat.controller.ts @Quota('chat') @Sse('stream') stream(@Body() dto: ChatDto, @Req() req: Request): Observable { const ac = new AbortController(); req.on('close', () => ac.abort()); return new Observable((subscriber) => { void (async () => { try { for await (const chunk of this.llm.stream(dto, { signal: ac.signal })) { subscriber.next({ data: chunk.text }); } subscriber.complete(); } catch (err) { if ((err as Error).name !== 'AbortError') subscriber.error(err); else subscriber.complete(); } })(); return () => ac.abort(); // unsubscribe also cancels upstream }); } ``` Both paths abort: the raw `close` event and the Observable teardown. Belt and braces here is justified, because the failure is silent and shows up only on the bill. ## The ledger Two writes, deliberately: a durable row for reporting, and a fast counter for the guard to read. ```ts src/cost/ledger.service.ts @Injectable() export class LedgerService { constructor( private readonly prisma: PrismaService, @Inject(CACHE_MANAGER) private readonly cache: Cache, ) {} async record(entry: LedgerEntry): Promise { await this.prisma.llmSpend.create({ data: entry }); // durable, for reports const key = `spend:${entry.tenantId}:${period()}`; await this.cache.set(key, ((await this.cache.get(key)) ?? 0) + entry.costMicros); } async usage(tenantId: string) { const key = `spend:${tenantId}:${period()}`; const cached = await this.cache.get(key); if (cached !== undefined) return { spentMicros: cached, capMicros: await this.cap(tenantId) }; const agg = await this.prisma.llmSpend.aggregate({ where: { tenantId, createdAt: { gte: periodStart() } }, _sum: { costMicros: true }, }); const spent = agg._sum.costMicros ?? 0; await this.cache.set(key, spent); return { spentMicros: spent, capMicros: await this.cap(tenantId) }; } } ``` `costMicros` is an integer. Never a float — this is money in a billing path, and floats lose fractions across millions of rows. The guard reads the cached counter, so a quota check costs a Redis GET rather than an aggregate query on every request. The cache is a performance optimisation over a durable source of truth, which means a cache flush degrades into a slow request, not a wrong one. ## The report ```ts src/cost/cost.controller.ts @Roles(Role.Admin) @Get('admin/spend') async spend(@Query() q: SpendQueryDto) { return this.prisma.llmSpend.groupBy({ by: ['feature', 'model'], where: { createdAt: { gte: q.since } }, _sum: { costMicros: true, inputTokens: true, cachedTokens: true, outputTokens: true }, _count: true, orderBy: { _sum: { costMicros: 'desc' } }, }); } ``` Three numbers to derive from it and watch: - **Cache hit rate** = `cachedTokens / (inputTokens)`. High and flat. A drop means someone put something volatile at the top of a prompt. - **Cost per successful call**, not per call — the failed ones still cost input tokens. - **Abort rate.** Climbing means answers are too slow or too long, and both are money. :::verdict Why Nest is a good fit for this Because the two halves of cost control map onto framework primitives that are already global. A guard that refuses before the handler runs, and an interceptor that measures whatever happens — including the disconnect. Register both with `APP_GUARD` and `APP_INTERCEPTOR` and a new endpoint is metered and capped by default, with the developer having to opt *out* rather than remember to opt in. ::: ## Common questions ### Guard or interceptor for the budget check? Guard. It runs before the handler, so an over-quota request never reaches the code that spends money, and it can return a proper 429 without the handler knowing quotas exist. The interceptor's job is measurement, which has to wrap the handler to see the outcome. ### Why not a request-scoped provider to hold the cost context? Because `Scope.REQUEST` is contagious: everything that injects it becomes request-scoped too, all the way up to the controller, and you pay for constructing that chain on every request. `AsyncLocalStorage` gives you the same per-request value with singleton providers. ### Should the quota reset monthly or roll? A rolling window is fairer and harder to game; a calendar month is easier to explain on an invoice and to implement. Start with the calendar month, key your cache by period, and revisit if customers complain about the cliff at month end. ### How do I handle a tenant who legitimately needs more? Make the cap a column, not a constant, and let support raise it — that is the whole reason `cap()` is a lookup rather than a config value. A hard-coded limit means every exception is a deploy. ## Security review checklist for NestJS applications Source: https://learn-nestjs.com/review/security/ Nest's structure is a genuine security asset: guards, pipes, interceptors and filters give every control an obvious home, and `APP_GUARD`-style global registration means a control applies everywhere by default. The corresponding weakness is that **an absent global looks exactly like a correctly configured one**. There is no error, no warning, nothing in the diff. Most of this page is about verifying the things that are supposed to be everywhere. [The failure-mode catalogue](/review/failure-modes/) covers the recurring bugs; this is the security-specific pass, and the underlying [TypeScript](https://learn-typescript.org/review/security/) and [Node](https://learn-javascript.org/review/security/) checklists still apply. ## The bootstrap audit Read `main.ts` first. It is four or five lines and it determines the security posture of the entire application. ```ts src/main.ts async function bootstrap() { const app = await NestFactory.create(AppModule, { bufferLogs: true }); app.use(helmet()); // security headers app.enableCors({ // NOT { origin: true } origin: ['https://app.example.com'], credentials: true, }); app.useGlobalPipes(new ValidationPipe({ whitelist: true, // strip undeclared properties forbidNonWhitelisted: true, // or reject outright transform: true, transformOptions: { enableImplicitConversion: true }, })); app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); app.enableShutdownHooks(); await app.listen(process.env.PORT ?? 3000); } ``` Five things to verify, each of which is silently absent in generated bootstraps: | Missing | Consequence | |---|---| | `whitelist: true` | mass assignment | | `ClassSerializerInterceptor` | `@Exclude()` does nothing; secrets serialise | | `helmet()` | no CSP, no HSTS, no frame protection | | explicit CORS origin | `origin: true` reflects any origin, with credentials | | `enableShutdownHooks()` | connections leak on every redeploy | `enableCors({ origin: true })` deserves singling out. It reflects the requesting origin back, which combined with `credentials: true` means any website can make authenticated requests as your user. Generated code uses it because it makes local development work. ## Authorisation The most common real vulnerability in generated back-end code, and no linter finds it. Covered in [the failure modes](/review/failure-modes/); the essentials: **Guard globally, opt out per route.** ```ts providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }] ``` ```ts @Public() // explicit, greppable, deliberate @Post('auth/login') login(@Body() dto: LoginDto) {} ``` The alternative — `@UseGuards(JwtAuthGuard)` on each controller — fails open. The endpoint someone forgets is public. ```bash grep -rn "@Public()" src # in a correct app this list is short and reviewable grep -rn "@UseGuards" src # and this one is near-empty ``` **Ownership checks live in the service, beside the query.** ```ts async findOne(id: string, requester: AuthUser) { const inv = await this.repo.findById(id); if (!inv || inv.ownerId !== requester.id) throw new NotFoundException(); return inv; } ``` 404 rather than 403 for resources the caller may not see — a 403 confirms the id exists. **One test per resource type.** It is the highest value-per-line test in the codebase: ```ts it('does not leak another user’s invoice', () => request(app.getHttpServer()) .get(`/invoices/${bobsInvoice.id}`) .set('Authorization', aliceToken) .expect(404)); ``` ## Rate limiting Absent from essentially every generated Nest application, and it is the difference between a failed login attempt and a credential-stuffing run. ```ts imports: [ ThrottlerModule.forRoot([ { name: 'short', ttl: 1000, limit: 10 }, { name: 'long', ttl: 60_000, limit: 100 }, ]), ], providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }], ``` ```ts @Throttle({ short: { ttl: 60_000, limit: 5 } }) // much tighter on auth @Public() @Post('auth/login') login(@Body() dto: LoginDto) {} ``` Two things to get right: use a shared store (Redis) rather than in-memory if you run more than one instance, otherwise your limit is multiplied by your replica count. And configure `trust proxy` correctly, or every request appears to come from your load balancer and the limit applies globally rather than per client. ## Input handling ### Mass assignment ```ts await this.prisma.user.update({ where: { id }, data: { ...dto } }); // never ``` Even with `whitelist: true`, pick fields explicitly. Defence in depth, and it survives someone loosening the pipe later. ```bash grep -rn 'data: { \.\.\.' src grep -rn 'Object.assign(' src ``` ### Nested DTOs ```ts @ValidateNested({ each: true }) @Type(() => OrderLineDto) // without this, items are NOT validated lines!: OrderLineDto[]; ``` Silent — no error, validation simply does not happen on the array items. A parameterised test with a deliberately invalid nested object catches it. ### File uploads ```ts @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024, files: 1 }, fileFilter: (_req, file, cb) => { cb(null, ALLOWED_MIME.has(file.mimetype)); // and verify magic bytes after }, storage: diskStorage({ destination: UPLOAD_DIR, filename: (_req, _file, cb) => cb(null, randomUUID()), // never the client's name }), })) ``` Three failures in generated upload handlers: no size limit, trusting `file.mimetype` (client-supplied), and using `file.originalname` as the filename — which is path traversal and overwriting in one step. Generate the name yourself. ### Raw-body webhooks ```ts // Stripe and most webhook providers sign the RAW body. app.use('/webhooks/stripe', express.raw({ type: 'application/json' })); ``` If a JSON body parser runs first, the body is re-serialised and the signature no longer verifies — or worse, someone "fixes" it by skipping verification. This belongs in `AGENTS.md` as a landmine. ## Errors and disclosure ```ts @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; this.logger.error({ err: exception }, 'unhandled'); // full detail to the log ctx.getResponse().status(status).json({ // minimal detail to the client statusCode: status, message: status === 500 ? 'Internal server error' : (exception as HttpException).message, timestamp: new Date().toISOString(), }); } } ``` Nest's default filter is reasonable, but a Prisma or TypeORM error escaping to the client leaks table and column names. Catch database errors specifically and map them to generic messages. ## Secrets and configuration ```ts ConfigModule.forRoot({ isGlobal: true, validate: (raw) => EnvSchema.parse(raw), // fail at boot, not at 2am }) ``` ```ts const EnvSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']), DATABASE_URL: z.url(), JWT_SECRET: z.string().min(32), JWT_EXPIRES_IN: z.string().default('15m'), }); ``` An application that refuses to start on a weak or missing secret is far better than one that starts and fails on the first request needing it. Generated `ConfigModule` setups almost never validate. Also: `@Exclude()` on every secret field, verified mechanically. ```bash grep -rln 'passwordHash\|refreshToken\|apiKey' src | xargs -r grep -L '@Exclude' ``` ## GraphQL, if you use it ```ts GraphQLModule.forRoot({ playground: false, // not in production introspection: process.env.NODE_ENV !== 'production', validationRules: [depthLimit(7)], // depth attacks plugins: [ApolloServerPluginLandingPageDisabled()], }) ``` Query depth and complexity limits are the GraphQL-specific denial-of-service control, and generated configurations omit them. Field-level authorisation also needs checking — a guard on the resolver does not protect nested field resolvers. ## The review, as commands ```bash # the bootstrap audit grep -n 'ValidationPipe\|helmet\|enableCors\|ClassSerializer\|enableShutdownHooks' src/main.ts # authorisation grep -rn '@Public()' src grep -rn '@UseGuards' src grep -rn 'PrismaService\|Repository<' src --include='*.controller.ts' # input grep -rn 'data: { \.\.\.\|Object.assign(' src grep -rn '@ValidateNested' src | while read -r l; do echo "$l"; done # check each has @Type grep -rn 'originalname' src # secrets grep -rln 'passwordHash\|refreshToken' src | xargs -r grep -L '@Exclude' grep -rn 'ConfigModule.forRoot' src | grep -v validate npm audit --omit=dev ``` Under a minute, and it covers the mechanical half. What is left for attention is authorisation logic and anything touching money or personal data. :::verdict The five that matter most 1. `ValidationPipe` with `whitelist: true`, globally. 2. Auth guard via `APP_GUARD`, opt out with `@Public()` — never opt in. 3. Ownership checks in the service, returning 404. One test per resource type. 4. `ThrottlerGuard` globally, tighter on auth routes. 5. `ClassSerializerInterceptor` plus `@Exclude()` on every secret field. ::: ## Common questions ### Does Nest give me security by default? It gives you the right *places* — guards, pipes, interceptors, filters — and sensible behaviour once configured. It does not enable them for you. A generated Nest application typically has none of the five above, and each absence is invisible in review unless you go looking. ### Why is rate limiting the most commonly missing control? Because nothing fails without it. Validation errors are visible, auth errors are visible, and a missing rate limit produces no symptom at all until someone is running a credential-stuffing attack against your login endpoint. ### Should I trust `class-validator` for security? For shape and format, yes, with `whitelist: true`. It is not a substitute for authorisation or business-rule checks: it verifies that a field is a valid UUID, not that the caller is allowed to reference that UUID. Those checks belong in the service. ### Is helmet enough for headers? It sets sensible defaults and is a good baseline. The Content-Security-Policy needs tailoring to your application — helmet's default is strict enough to break most front ends, so people disable it entirely rather than configuring it, which is the outcome to avoid. ## Providers and dependency injection Source: https://learn-nestjs.com/providers-and-di/ Dependency injection is Nest's core mechanism. You declare what a class needs; Nest works out how to build it. ```ts @Injectable() export class OrdersService { constructor( private readonly users: UsersService, private readonly prisma: PrismaService, ) {} } ``` Nest reads the parameter types at runtime (via `emitDecoratorMetadata`), looks up a provider for each, constructs them if they do not exist yet, and passes them in. There is no `new` anywhere. The payoff is testing: ```ts const module = await Test.createTestingModule({ providers: [ OrdersService, { provide: UsersService, useValue: { findOne: jest.fn() } }, { provide: PrismaService, useValue: prismaMock }, ], }).compile(); ``` Swapping a real dependency for a fake is a one-line change, because nothing ever hardcoded the dependency. ## The four provider forms ```ts // 1. class — the common case providers: [UsersService] // 2. useClass — swap the implementation behind a token providers: [{ provide: MailerService, useClass: process.env.NODE_ENV === 'test' ? FakeMailer : SesMailer }] // 3. useValue — a constant or a mock providers: [{ provide: 'CONFIG', useValue: { retries: 3 } }] // 4. useFactory — computed, possibly async, may inject providers: [{ provide: 'DB', useFactory: async (config: ConfigService) => createPool(config.get('DATABASE_URL')), inject: [ConfigService], }] ``` ## String tokens and how to avoid them Interfaces do not exist at runtime, so you cannot inject by interface. The workaround is a token — but use a typed constant, not a bare string. ```ts src/mail/mail.tokens.ts export const MAILER = Symbol('MAILER'); export interface Mailer { send(to: string, subject: string, body: string): Promise; } ``` ```ts providers: [{ provide: MAILER, useClass: SesMailer }] constructor(@Inject(MAILER) private readonly mailer: Mailer) {} ``` A `Symbol` cannot collide and cannot be typo'd into a different provider. ## Scopes Providers are singletons by default, and that is almost always what you want. ```ts @Injectable({ scope: Scope.REQUEST }) // new instance per request export class RequestContext {} ``` :::warn Request scope is contagious and slow Anything that injects a request-scoped provider becomes request-scoped too, all the way up the graph — including your controller. On a hot path that means constructing a chain of objects per request. Prefer `AsyncLocalStorage` for request context, or `nestjs-cls`. ::: ## Optional and circular ```ts constructor(@Optional() private readonly cache?: CacheService) {} // two services that need each other constructor(@Inject(forwardRef(() => OrdersService)) private orders: OrdersService) {} ``` As with modules, `forwardRef` between *services* usually means a third thing wants extracting. ## Common questions ### Why can I not inject an interface? TypeScript interfaces are erased at compile time, so there is nothing at runtime for Nest to look up. Use a `Symbol` token with `@Inject()`, and keep the interface for the type annotation — you get both the abstraction and working injection. ### When should I use `useFactory`? When constructing the provider needs async work or configuration — a database pool, an SDK client built from environment values. For a plain class with injectable dependencies, the class form is simpler and does the same thing. ### Does dependency injection slow things down? Only at startup, and imperceptibly: the graph is constructed once. The exception is request-scoped providers, which construct per request and can be measurably expensive on a hot path. ## The performance traps in generated NestJS code Source: https://learn-nestjs.com/review/performance/ Nest runs on Node, so [everything in the JavaScript performance page](https://learn-javascript.org/review/performance/) applies — event loop blocking, unbounded concurrency, memory leaks, sequential awaits. Read that one first; it is the larger category. What is specific to Nest is two things that generated code produces routinely and that no linter flags. ## 1. Request-scoped provider contagion The Nest performance problem people discover last and regret most. ```ts @Injectable({ scope: Scope.REQUEST }) // looks harmless export class RequestContext { constructor(@Inject(REQUEST) private readonly req: Request) {} get tenantId() { return this.req.user?.tenantId; } } ``` Scope propagates **upward**. Anything that injects a request-scoped provider becomes request-scoped, and so does anything injecting *that*, all the way to the controller. One innocuous provider can make your entire dependency graph reconstruct on every request. ```text RequestContext (REQUEST) -> AuditService becomes REQUEST -> OrdersService becomes REQUEST -> OrdersController becomes REQUEST ``` You are now constructing four objects per request instead of zero, and you have lost the ability to use those services in a scheduled job or a queue consumer, because there is no request to inject. **The fix is `AsyncLocalStorage`**, which gives you per-request values with singleton providers: ```ts src/context/request-context.ts import { AsyncLocalStorage } from 'node:async_hooks'; export interface RequestContext { tenantId: string; requestId: string; userId?: string; } export const requestContext = new AsyncLocalStorage(); ``` ```ts src/context/context.middleware.ts @Injectable() export class ContextMiddleware implements NestMiddleware { use(req: Request, _res: Response, next: NextFunction) { requestContext.run( { tenantId: (req as any).user?.tenantId, requestId: randomUUID() }, () => next(), ); } } ``` Now any singleton can call `requestContext.getStore()` and get the current request's values. `nestjs-cls` wraps this pattern if you want it as a module. **Find it with:** ```bash grep -rn 'Scope.REQUEST\|Scope.TRANSIENT' src ``` In a healthy Nest application that returns nothing, or one deliberate case with a comment. ## 2. N+1 queries The most expensive item in practice, in any ORM. ```ts const orders = await this.prisma.order.findMany({ where: { userId } }); for (const order of orders) { order.items = await this.prisma.orderItem.findMany({ where: { orderId: order.id } }); } ``` Correct. Passes every test, because the test has three orders. In production with a hundred orders per user that is 101 round trips. ```ts const orders = await this.prisma.order.findMany({ where: { userId }, include: { items: true }, // one query, or two with a join strategy }); ``` TypeORM equivalents are `relations: ['items']` or an explicit `leftJoinAndSelect`. The trap in TypeORM is **lazy relations** — a property that issues a query when accessed, which makes an N+1 invisible in the code because there is no `await` on the line that causes it. **Catch it with a query-count test.** It is the only reliable defence: ```ts it('lists orders in at most two queries', async () => { const spy = countQueries(prisma); await request(app.getHttpServer()).get('/orders').set(auth).expect(200); expect(spy.count).toBeLessThanOrEqual(2); }); ``` Prisma exposes `$on('query')`; TypeORM has a logger you can count through. Twenty lines of test helper, and it catches the single most expensive class of regression in a Nest service. ## Serialization ### 3. `ClassSerializerInterceptor` on large payloads `class-transformer` uses reflection and is not cheap. On a list endpoint returning ten thousand rows, `plainToInstance` can dominate the response time. For large collections, project the shape in the query instead of transforming after: ```ts return this.prisma.order.findMany({ where: { userId }, select: { id: true, total: true, createdAt: true }, // only what you serialise }); ``` That is faster on both sides: less data from the database, no transformation pass. Keep the serializer for endpoints where `@Exclude()` is doing security work — see [the security page](/review/security/) — and be deliberate about the ones where it is not needed. ### 4. Returning entities directly Returning an ORM entity means serialising every loaded relation, including ones you did not intend to expose. It is a payload-size problem and a disclosure problem at once. Return an explicit shape. ## Database plumbing ### 5. Connection pool sizing ```text ?connection_limit=10 # Prisma extra: { max: 10 } # TypeORM ``` The default is often wrong in both directions. Too small and requests queue behind a pool that is idle-waiting; too large and you exhaust the database's own connection limit — which fails at the database rather than in your app, and looks like an outage rather than a config problem. The arithmetic that matters: `replicas × pool_size` must stay under your database's `max_connections`, with headroom for migrations and admin tools. Four replicas at a pool of 25 against a Postgres configured for 100 leaves you nothing. Use a connection pooler (PgBouncer, or your provider's) for anything serverless, where instance count is unbounded by design. ### 6. Missing indexes on foreign keys and filters Not Nest-specific, but generated schemas frequently declare relations without indexing the foreign key. Every `findMany({ where: { orderId } })` then becomes a sequential scan. ```prisma model OrderItem { orderId String order Order @relation(fields: [orderId], references: [id]) @@index([orderId]) // generated schemas often omit this } ``` Run `EXPLAIN ANALYZE` on your three most common queries once. It is twenty minutes and it usually finds something. ## Lifecycle ### 7. Missing shutdown hooks ```ts app.enableShutdownHooks(); ``` Without it, `onModuleDestroy` never runs: connections are not closed, in-flight requests are cut off, and every redeploy leaks database connections until the pool is exhausted. It presents as a slow degradation over days, which makes it hard to attribute. ### 8. Work in the constructor ```ts @Injectable() export class ReportService { constructor(private readonly prisma: PrismaService) { this.warmCache(); // fires during module construction, unawaited } } ``` Constructors cannot be async, so generated warm-up code either blocks module initialisation or floats an unhandled promise. Use `onModuleInit`, which Nest awaits. ### 9. No health check, or one that queries the database A health endpoint that hits the database on every probe adds load proportional to your probe frequency times your replica count. Use `@nestjs/terminus` and separate liveness (is the process alive) from readiness (can it serve), with only readiness touching dependencies. ## Caching ```ts @UseInterceptors(CacheInterceptor) @CacheTTL(30) @Get('popular') popular() { return this.products.popular(); } ``` Two things to get right, both of which generated code gets wrong: - **Use a shared store** (Redis) if you run more than one instance. The default in-memory cache means each replica has its own, so your hit rate divides by your replica count and users see inconsistent data. - **Never cache an authenticated response with the default key**, which is derived from the URL. Two users hitting `/me` get each other's data. Override `trackBy` to include the user, or do not cache the endpoint. That second one is a security bug wearing a performance costume, and it is a genuinely easy mistake to make. ## Measuring ```bash npx clinic doctor -- node dist/main.js # which category of problem? node --cpu-prof dist/main.js autocannon -c 100 -d 30 http://localhost:3000/orders ``` Add an interceptor that logs slow requests with the query count, and you will find most of this page without a profiler: ```ts @Injectable() export class SlowRequestInterceptor implements NestInterceptor { intercept(ctx: ExecutionContext, next: CallHandler) { const started = Date.now(); return next.handle().pipe(tap(() => { const ms = Date.now() - started; if (ms > 500) this.logger.warn({ url: ctx.switchToHttp().getRequest().url, ms }, 'slow'); })); } } ``` :::verdict The two Nest-specific things `grep -rn 'Scope.REQUEST' src` should return nothing, and every list endpoint should have a query-count test. Those two checks cover the problems that are specific to this framework; everything else is [Node performance](https://learn-javascript.org/review/performance/). ::: ## Common questions ### Is request scope ever the right answer? Rarely. It is defensible for something that genuinely must be constructed per request and cannot use `AsyncLocalStorage` — a per-tenant database connection, say. Even then, keep it at a leaf of the graph so the contagion does not reach your controllers, and comment why. ### Prisma or TypeORM for performance? Close enough that it is not the deciding factor. Prisma's generated queries are predictable and its `include` behaviour is explicit; TypeORM's lazy relations make accidental N+1 easier to write. If you use TypeORM, avoid lazy relations. ### How do I count queries in a test? Prisma exposes a `query` event you can subscribe to; TypeORM lets you supply a custom logger. Either way it is about twenty lines of test helper, and it is the highest-value performance test in a Nest codebase because N+1 is both the most common and the most expensive regression. ### Does the caching interceptor work across replicas? Only with a shared store configured. The default is per-process memory, so with four replicas you get a quarter of the hit rate and users see different cached values depending on which instance they reach. ## DTOs and validation Source: https://learn-nestjs.com/dto-and-validation/ A DTO — data transfer object — is a class describing the shape of a request. Decorate it, register the global `ValidationPipe`, and Nest rejects anything that does not match before your controller runs. ```ts src/users/dto/create-user.dto.ts import { IsEmail, IsInt, IsOptional, IsString, Length, Max, Min } from 'class-validator'; export class CreateUserDto { @IsEmail() email!: string; @IsString() @Length(2, 60) name!: string; @IsOptional() @IsInt() @Min(13) @Max(130) age?: number; } ``` ```ts src/main.ts app.useGlobalPipes(new ValidationPipe({ whitelist: true, // strip properties with no decorator forbidNonWhitelisted: true, // ...or reject the request outright transform: true, // instantiate the DTO class, coerce types transformOptions: { enableImplicitConversion: true }, })); ``` ## Why `whitelist` is the important one Without it, a client can send fields you never declared and they arrive in your DTO. If any code path spreads that object into a database update, you have mass assignment: ```ts // without whitelist, this is a privilege escalation await this.prisma.user.update({ where: { id }, data: { ...dto } }); // client sent { name: "x", role: "admin" } ``` `whitelist: true` strips undeclared properties. `forbidNonWhitelisted: true` rejects the request instead, which is better in an internal API where a surprise field means a client bug. :::warn This is the single most valuable line in your bootstrap Generated NestJS code frequently registers `ValidationPipe` with no options, which validates the fields you declared and silently passes through everything else. Always pass `whitelist: true`. ::: ## Transformation With `transform: true`, Nest instantiates the DTO class rather than handing you a plain object. That means `instanceof` works, defaults apply, and `class-transformer` decorators run. ```ts export class ListUsersDto { @Type(() => Number) // query params are always strings @IsInt() @Min(1) @Max(100) limit = 20; @Transform(({ value }) => value?.trim().toLowerCase()) @IsOptional() @IsString() search?: string; } ``` ## Nested objects and arrays Nested validation does not recurse unless you ask. ```ts export class CreateOrderDto { @ValidateNested({ each: true }) @Type(() => OrderLineDto) // required, or nested validation silently passes @ArrayMinSize(1) lines!: OrderLineDto[]; } ``` Omitting `@Type()` is the most common validation bug in Nest: the array is accepted, the items are never checked. ## Partial updates ```ts import { PartialType } from '@nestjs/mapped-types'; export class UpdateUserDto extends PartialType(CreateUserDto) {} ``` Every field becomes optional, decorators are preserved. `PickType`, `OmitType` and `IntersectionType` compose similarly. ## Response shaping Validation guards the way in. `class-transformer` guards the way out. ```ts src/users/entities/user.entity.ts export class UserEntity { id!: string; email!: string; @Exclude() passwordHash!: string; } ``` ```ts @UseInterceptors(ClassSerializerInterceptor) @Controller('users') export class UsersController {} ``` Now `passwordHash` cannot leak, even if a service returns the raw database row. Worth doing on every entity that has a secret in it — it converts a class of accidental disclosure into an impossibility. ## An alternative: schema-first If you prefer schemas to decorators, `nestjs-zod` gives you a Zod schema as the single source of truth for both runtime validation and the TypeScript type, which removes the drift between the two. ```ts const CreateUser = z.object({ email: z.email(), name: z.string().min(2) }); export class CreateUserDto extends createZodDto(CreateUser) {} ``` ## Common questions ### Do I need DTOs if I already have TypeScript types? Yes. Types are erased at compile time — they constrain your code, not the incoming JSON. A DTO with validators is the only thing actually checking what arrived over the wire. ### Where do business rules go? In the service. The DTO checks shape and format ("is this a valid email"); the service checks state ("is this email already registered"), because that needs the database. ### Why is my nested array not being validated? Almost certainly a missing `@Type(() => Child)` alongside `@ValidateNested({ each: true })`. Without it, `class-transformer` does not know what class to instantiate and validation of the items is skipped silently. ## Guards, authentication and authorisation Source: https://learn-nestjs.com/guards-and-auth/ A guard runs before the route handler and returns a boolean. `false` produces a 403; throwing gives you control of the response. ```ts src/auth/jwt-auth.guard.ts @Injectable() export class JwtAuthGuard implements CanActivate { constructor(private readonly jwt: JwtService, private readonly reflector: Reflector) {} async canActivate(ctx: ExecutionContext): Promise { const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC, [ ctx.getHandler(), ctx.getClass(), ]); if (isPublic) return true; const req = ctx.switchToHttp().getRequest(); const token = req.headers.authorization?.replace(/^Bearer /, ''); if (!token) throw new UnauthorizedException(); try { req.user = await this.jwt.verifyAsync(token); return true; } catch { throw new UnauthorizedException(); } } } ``` ## Default deny Register the guard globally and opt routes *out*, never the reverse. ```ts src/app.module.ts providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }] ``` ```ts export const IS_PUBLIC = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC, true); @Public() @Post('login') login(@Body() dto: LoginDto) {} ``` :::warn This is the whole ballgame An allowlist (`@UseGuards()` on the routes that need protection) fails open: the endpoint someone forgets to decorate is public. A global guard with `@Public()` opt-outs fails closed: the endpoint someone forgets is protected. Always the second one. ::: ## Roles ```ts export const Roles = (...roles: Role[]) => SetMetadata('roles', roles); @Injectable() export class RolesGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} canActivate(ctx: ExecutionContext): boolean { const required = this.reflector.getAllAndOverride('roles', [ ctx.getHandler(), ctx.getClass(), ]); if (!required?.length) return true; const { user } = ctx.switchToHttp().getRequest(); return required.some((r) => user?.roles?.includes(r)); } } ``` ```ts @Roles(Role.Admin) @Delete(':id') remove(@Param('id') id: string) {} ``` ## The check that gets forgotten Roles answer *what kind of user is this*. They do not answer *is this their resource*, and that second question is where the real vulnerabilities are. ```ts @Get(':id') findOne(@Param('id') id: string) { return this.invoices.findOne(id); // any authenticated user, any invoice } ``` Ownership depends on data, so it belongs in the service, where the query happens: ```ts async findOne(id: string, requester: AuthUser): Promise { const invoice = await this.repo.findById(id); if (!invoice || invoice.ownerId !== requester.id) { throw new NotFoundException(); // not Forbidden: do not confirm it exists } return invoice; } ``` Return 404 rather than 403 for resources the caller may not see. A 403 tells an attacker the id is real. And write the test, once per resource type: ```ts it('does not let a user read another user’s invoice', async () => { await request(app.getHttpServer()) .get(`/invoices/${bobsInvoice.id}`) .set('Authorization', aliceToken) .expect(404); }); ``` ## Guards, interceptors, pipes, filters The execution order, which is worth memorising: ```text middleware -> guards -> interceptors (before) -> pipes -> handler -> interceptors (after) -> exception filters ``` - **Guard** — may this proceed? Auth, roles, rate limits. - **Pipe** — transform and validate input. DTO validation. - **Interceptor** — wrap the call. Logging, caching, response shaping, timeouts. - **Filter** — turn a thrown exception into a response. Putting logic in the wrong one is a common source of surprise: a pipe cannot reject on authorisation grounds cleanly, and a guard cannot see the parsed body. ## Common questions ### Guard or middleware? Guards know about the route — they get the handler, the class and its metadata, so they can read decorators like `@Roles()`. Middleware runs before routing and knows only the request. Use guards for anything decision-based. ### Where do I check that a user owns a record? In the service, where the record is fetched. A guard runs before the handler and would have to query the database itself, duplicating work and drifting out of sync with the real query. ### 403 or 404 for a resource the user cannot access? 404, for anything with a guessable or enumerable id. A 403 confirms the resource exists, which is an information leak. Use 403 when the caller is allowed to know it exists but not to act. ## Database access and testing Source: https://learn-nestjs.com/database-and-testing/ ## A database provider ```ts src/database/prisma.service.ts import { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect(); } } ``` ```ts src/database/database.module.ts @Global() @Module({ providers: [PrismaService], exports: [PrismaService] }) export class DatabaseModule {} ``` `onModuleInit` is one of Nest's lifecycle hooks. `onModuleDestroy` is the matching one for cleanup, and Nest calls it on shutdown if you enable shutdown hooks (`app.enableShutdownHooks()`), which you should — otherwise connections leak on redeploy. ## Keep queries out of the controller ```ts src/users/users.service.ts @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} findAll(limit: number) { return this.prisma.user.findMany({ take: limit, orderBy: { createdAt: 'desc' } }); } async findOne(id: string) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new NotFoundException(`User ${id} not found`); return user; } } ``` Throwing `NotFoundException` from the service is idiomatic: Nest's exception filter turns it into a 404 with a JSON body, and the controller stays a one-liner. ## Unit tests The whole value of dependency injection shows up here. ```ts src/users/users.service.spec.ts describe('UsersService', () => { let service: UsersService; const prisma = { user: { findUnique: jest.fn(), findMany: jest.fn() } }; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [UsersService, { provide: PrismaService, useValue: prisma }], }).compile(); service = module.get(UsersService); }); it('throws when the user does not exist', async () => { prisma.user.findUnique.mockResolvedValue(null); await expect(service.findOne('missing')).rejects.toThrow(NotFoundException); }); }); ``` No database, no HTTP, milliseconds. This is the suite your agent should be able to run after every edit — see [the verification loop](https://learn-python.com/ai/feedback-loops/) for why speed here matters so much. ## End-to-end tests ```ts test/users.e2e-spec.ts describe('Users (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(MailerService).useValue({ send: jest.fn() }) .compile(); app = moduleRef.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); await app.init(); }); afterAll(() => app.close()); it('rejects an invalid email', () => request(app.getHttpServer()) .post('/users') .send({ email: 'nope', name: 'Ada' }) .expect(400)); }); ``` :::warn Replicate your global pipes in tests `useGlobalPipes` is called in `main.ts`, which the testing module does not run. If you forget to add it in the test setup, validation is off and your tests pass on payloads production would reject. This catches almost everyone once. ::: `overrideProvider` is the mechanism that makes end-to-end tests practical: swap the mailer, the payment gateway and the queue for fakes, keep everything else real. ## Transactions in tests The fastest reliable pattern for integration tests is a transaction per test, rolled back afterwards. It gives you a real database with no cleanup and no cross-test pollution — and it is fast enough to stay in the loop. ## Common questions ### Prisma or TypeORM? Prisma has better type inference and a clearer migration story; TypeORM fits better if you want the ActiveRecord/decorator style throughout or need something Prisma does not support. Either works well with Nest. Do not use both. ### Should I add a repository layer on top of the ORM? Only if you have a real reason — swapping the data store, or a domain model that differs substantially from your tables. Otherwise it is indirection with no payoff, and the ORM's types are better than the ones you will hand-write. ### How do I keep end-to-end tests fast? Reuse one application instance across the file, run each test in a transaction that is rolled back, and mock anything crossing the network. Most slow Nest suites are slow because they build a new application per test. ## Interceptors Source: https://learn-nestjs.com/interceptors/ An interceptor wraps the route handler. It sees the request on the way in, the response on the way out, and — crucially — it runs even when the handler throws or the client disconnects. ```typescript src/common/logging.interceptor.ts import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Logger } from '@nestjs/common'; import { Observable, tap } from 'rxjs'; @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(ctx: ExecutionContext, next: CallHandler): Observable { const req = ctx.switchToHttp().getRequest(); const started = Date.now(); return next.handle().pipe( tap({ next: () => this.logger.log(`${req.method} ${req.url} ${Date.now() - started}ms`), error: (err) => this.logger.error(`${req.method} ${req.url} failed: ${err.message}`), }), ); } } ``` `next.handle()` returns an Observable of whatever the handler returns. Everything before that call runs **before** the handler; everything in the `.pipe()` runs **after**. ## Where interceptors sit ```text middleware → guards → interceptors (before) → pipes → HANDLER → interceptors (after) → exception filters ``` That position is what makes them the right place for anything cross-cutting that needs to see the outcome. A guard can only say yes or no before the fact; an interceptor sees what happened. ## Registering ```typescript @UseInterceptors(LoggingInterceptor) // one handler or one controller @Get() findAll() {} ``` ```typescript src/app.module.ts providers: [{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }] // global, with DI ``` ```typescript src/main.ts app.useGlobalInterceptors(new LoggingInterceptor()); // global, no DI ``` Prefer `APP_INTERCEPTOR` for anything that needs injected dependencies — the `useGlobalInterceptors` form constructs the instance itself, so nothing can be injected into it. ## Transforming the response ```typescript export interface Envelope { data: T; timestamp: string } @Injectable() export class EnvelopeInterceptor implements NestInterceptor> { intercept(_ctx: ExecutionContext, next: CallHandler): Observable> { return next.handle().pipe( map((data) => ({ data, timestamp: new Date().toISOString() })), ); } } ``` Every handler now returns `{ data: ..., timestamp: ... }` without any handler knowing about it. Useful — and worth deciding once, early, because changing your response envelope later is a breaking change for every client. ## The serializer The one interceptor you should almost always register globally: ```typescript app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); ``` ```typescript export class UserEntity { id!: string; email!: string; @Exclude() passwordHash!: string; // now stripped from every response } ``` Without the interceptor, `@Exclude()` does nothing and your password hashes serialise. This pairing is a security control, not a formatting nicety — see [the security review](/review/security/). ## Timeouts ```typescript @Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(_ctx: ExecutionContext, next: CallHandler): Observable { return next.handle().pipe( timeout(5000), catchError((err) => err instanceof TimeoutError ? throwError(() => new RequestTimeoutException()) : throwError(() => err), ), ); } } ``` ## Caching ```typescript @Injectable() export class CacheInterceptor implements NestInterceptor { constructor(@Inject(CACHE_MANAGER) private cache: Cache) {} async intercept(ctx: ExecutionContext, next: CallHandler): Promise> { const req = ctx.switchToHttp().getRequest(); if (req.method !== 'GET') return next.handle(); const key = `${req.user?.id ?? 'anon'}:${req.url}`; // include the user! const hit = await this.cache.get(key); if (hit !== undefined) return of(hit); return next.handle().pipe(tap((body) => this.cache.set(key, body, 30_000))); } } ``` :::danger Never key a cache on the URL alone Nest's built-in `CacheInterceptor` keys on the URL by default. On an authenticated endpoint like `/me`, that means the first user's response is served to everyone else. Always include the user in the key, or do not cache authenticated routes at all. ::: ## `finalize` — the operator worth knowing `tap` fires on success or error. `finalize` also fires on **unsubscribe**, which is what happens when the client disconnects mid-response. ```typescript return next.handle().pipe( finalize(() => { // runs on success, on error, AND on client disconnect this.metrics.record(feature, Date.now() - started); }), ); ``` If you are measuring anything — latency, cost, token usage — `finalize` is the operator that stops abandoned requests going unrecorded. That matters more than it sounds: abandoned work is exactly the category you most want visibility into. ## A minimal RxJS vocabulary You do not need to learn RxJS to write interceptors. These five cover nearly everything: ```typescript map(fn) // transform the response tap({ next, error }) // side effect, no transformation catchError(fn) // handle an error timeout(ms) // fail if it takes too long finalize(fn) // cleanup, always runs of(value) // an Observable of one value — short-circuit the handler ``` ## Exercise ```typescript // Write a `RequestIdInterceptor` that: // - reads an `x-request-id` header, generating one if absent // - attaches it to the request object // - sets it on the response header // - logs method, url, request id and duration on completion, // including when the client disconnects ``` ## Common questions ### Interceptor, guard or middleware? Guard for "may this proceed" — it runs first and can reject cleanly. Middleware for anything that only needs the raw request and does not care about the route. Interceptor for anything that needs to see the *result*: logging, transforming, timing, caching. ### Do I need to learn RxJS? Only the five operators above. Nest wraps the handler's return value in an Observable for you, and interceptors are the main place it surfaces. Nothing else in a typical Nest application requires it. ### Why is my global interceptor not getting its dependencies? Because `useGlobalInterceptors` in `main.ts` constructs the instance outside the DI container. Register it as an `APP_INTERCEPTOR` provider in a module instead, and injection works normally. ## Exception Filters Source: https://learn-nestjs.com/exception-filters/ Nest's default behaviour is already reasonable: throw an `HttpException` and it becomes the matching HTTP response. ```typescript throw new NotFoundException(`User ${id} not found`); ``` ```json { "statusCode": 404, "message": "User 1 not found", "error": "Not Found" } ``` Anything that is *not* an `HttpException` becomes a generic 500, with the detail logged rather than sent — which is the correct default. ## The built-in exceptions ```typescript new BadRequestException() // 400 new UnauthorizedException() // 401 — not authenticated new ForbiddenException() // 403 — authenticated, not allowed new NotFoundException() // 404 new ConflictException() // 409 — duplicate, version conflict new UnprocessableEntityException() // 422 — valid shape, invalid meaning new TooManyRequestsException() // 429 new InternalServerErrorException() // 500 new ServiceUnavailableException() // 503 ``` Throw them from the **service**, not the controller. The service knows why the operation failed; the controller just passes the result along. ```typescript async findOne(id: string, requester: AuthUser): Promise { const invoice = await this.repo.findById(id); if (!invoice || invoice.ownerId !== requester.id) { throw new NotFoundException(); // 404, not 403 — do not confirm it exists } return invoice; } ``` That 404-not-403 choice matters: a 403 tells an attacker the id is real. See [the security review](/review/security/). ## Domain exceptions Better still, throw exceptions that belong to your domain and map them to HTTP at the edge. Your service then has no idea it is being used over HTTP, which makes it reusable from a queue consumer or a CLI. ```typescript src/core/errors.ts export class DomainError extends Error {} export class NotFound extends DomainError { constructor(readonly resource: string, readonly id: string) { super(`${resource} ${id} not found`); } } export class InsufficientStock extends DomainError { constructor(readonly sku: string, readonly available: number) { super(`insufficient stock for ${sku}`); } } ``` ## A global filter ```typescript src/common/all-exceptions.filter.ts @Catch() export class AllExceptionsFilter implements ExceptionFilter { private readonly logger = new Logger(AllExceptionsFilter.name); catch(exception: unknown, host: ArgumentsHost): void { const ctx = host.switchToHttp(); const res = ctx.getResponse(); const req = ctx.getRequest(); const { status, message } = this.map(exception); // full detail to the log this.logger.error({ err: exception, path: req.url, method: req.method, requestId: (req as any).id, }, 'request failed'); // minimal detail to the client res.status(status).json({ statusCode: status, message, timestamp: new Date().toISOString(), path: req.url, }); } private map(e: unknown): { status: number; message: string } { if (e instanceof HttpException) { const r = e.getResponse(); return { status: e.getStatus(), message: typeof r === 'string' ? r : ((r as any).message ?? e.message), }; } if (e instanceof NotFound) return { status: 404, message: e.message }; if (e instanceof InsufficientStock) return { status: 409, message: e.message }; // Prisma / TypeORM errors must NOT reach the client — they name your tables if (this.isDatabaseError(e)) { return { status: 500, message: 'Internal server error' }; } return { status: 500, message: 'Internal server error' }; } private isDatabaseError(e: unknown): boolean { return typeof e === 'object' && e !== null && 'code' in e && typeof (e as any).code === 'string' && (e as any).code.startsWith('P'); } } ``` ```typescript providers: [{ provide: APP_FILTER, useClass: AllExceptionsFilter }] ``` The shape to copy is the two-audience split: **everything to the log, almost nothing to the client.** A database error escaping to a response leaks your schema; a stack trace leaks your file layout. ## Scoped filters `@Catch()` with no argument catches everything. With arguments it catches only those types: ```typescript @Catch(NotFound, InsufficientStock) export class DomainErrorFilter implements ExceptionFilter { /* … */ } ``` Filters are matched most-specific-first, so a scoped filter and a catch-all can coexist — the scoped one handles what it declares, the catch-all takes the rest. ## Validation errors `ValidationPipe` throws a `BadRequestException` whose payload is an array of messages. To reshape it for a client that expects field-level errors: ```typescript new ValidationPipe({ whitelist: true, transform: true, exceptionFactory: (errors) => new BadRequestException({ statusCode: 400, message: 'Validation failed', fields: Object.fromEntries( errors.map((e) => [e.property, Object.values(e.constraints ?? {})]), ), }), }) ``` ## What not to do ```typescript @Get(':id') async findOne(@Param('id') id: string) { try { return await this.service.findOne(id); } catch (err) { throw new InternalServerErrorException(); // destroys the real error } } ``` Three problems: the original error and its stack are gone, a genuine 404 has become a 500, and the same `try/catch` now has to be repeated in every handler. Let the exception propagate and let the filter map it — that is what the filter is for. ## Exercise ```typescript // 1. Define domain errors: `PaymentDeclined` (has a `reason`) and // `DuplicateOrder` (has an `existingId`). // 2. Write a @Catch filter mapping PaymentDeclined -> 402 and // DuplicateOrder -> 409 with the existing id in the body. // 3. Ensure anything unrecognised becomes a 500 with no detail leaked, // while the full error is logged. ``` ## Common questions ### Should services throw HTTP exceptions? It is common and pragmatic in a Nest-only codebase. Domain exceptions mapped in a filter are cleaner, because the service then works unchanged from a queue consumer or a CLI where HTTP status codes are meaningless. Pick one convention and state it in your instructions file. ### Where do I log the error — the filter or the service? The filter, once. Logging in the service and re-throwing means one failure appears several times in your logs at different levels of detail. Add context by wrapping the error, and log at the boundary. ### Why is my filter not catching anything? Usually ordering or scope: a more specific filter is handling it first, or a `try/catch` in the handler swallowed the exception before it reached the filter. Also check the filter is registered with `APP_FILTER` and not just declared. ## Middleware and Lifecycle Source: https://learn-nestjs.com/middleware-and-lifecycle/ Middleware runs **before** everything Nest-specific — before guards, before interceptors, before the router has matched a handler. It is the raw Express or Fastify layer. ```typescript src/common/request-id.middleware.ts @Injectable() export class RequestIdMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const id = (req.headers['x-request-id'] as string) ?? randomUUID(); (req as any).id = id; res.setHeader('x-request-id', id); next(); // forget this and the request hangs forever } } ``` ```typescript src/app.module.ts export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(RequestIdMiddleware) .forRoutes('*'); consumer .apply(RawBodyMiddleware) .forRoutes({ path: 'webhooks/*', method: RequestMethod.POST }); consumer .apply(AuditMiddleware) .exclude('health', 'metrics') .forRoutes(OrdersController); } } ``` Functional middleware works too and is simpler when you need no dependencies: ```typescript export function requestId(req: Request, res: Response, next: NextFunction) { (req as any).id = randomUUID(); next(); } ``` ## The full pipeline Worth memorising, because putting logic in the wrong layer is a common source of confusion: ```text incoming request → middleware (raw req/res; no route metadata yet) → guards (may this proceed? sees decorators) → interceptors (before) → pipes (validate and transform arguments) → HANDLER → interceptors (after) → exception filters (on throw, at any stage) ``` The practical consequences: - **Middleware cannot read route decorators.** It runs before routing, so `@Roles()` and `@Public()` are invisible to it. Anything decorator-driven must be a guard. - **Guards cannot see the validated body.** Pipes run after guards, so a guard sees the raw payload. - **Only interceptors and filters see the outcome.** ## Where middleware is genuinely right Three cases, and they are all "this needs the raw request": **Request context**, set once and read everywhere without threading it through: ```typescript @Injectable() export class ContextMiddleware implements NestMiddleware { use(req: Request, _res: Response, next: NextFunction) { requestContext.run( { requestId: (req as any).id, tenantId: (req as any).user?.tenantId }, () => next(), ); } } ``` `AsyncLocalStorage` here rather than a request-scoped provider — scope is [contagious and expensive](/review/performance/). **Raw body for signed webhooks:** ```typescript consumer.apply(express.raw({ type: 'application/json' })) .forRoutes({ path: 'webhooks/stripe', method: RequestMethod.POST }); ``` Stripe and most webhook providers sign the raw bytes. If a JSON parser runs first, the body is re-serialised and the signature no longer verifies. **Third-party Express middleware** — `helmet`, `compression`, `cookie-parser`. These are written for Express and slot in unchanged. ```typescript src/main.ts app.use(helmet()); app.use(compression()); ``` ## Module lifecycle hooks Nest calls these as modules are initialised and destroyed: ```typescript @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { async onModuleInit() { await this.$connect(); } async onModuleDestroy() { await this.$disconnect(); } } ``` | Hook | When | |---|---| | `onModuleInit` | once the host module's dependencies are resolved | | `onApplicationBootstrap` | once every module has initialised | | `onModuleDestroy` | when a termination signal is received | | `beforeApplicationShutdown` | after all `onModuleDestroy` complete | | `onApplicationShutdown` | last, with the signal | Nest **awaits** async hooks, which is why they are the right place for setup that a constructor cannot do: ```typescript @Injectable() export class ReportService { constructor(private readonly prisma: PrismaService) { this.warmCache(); // WRONG — constructors cannot be async; } // this floats an unhandled promise } @Injectable() export class ReportService implements OnModuleInit { async onModuleInit() { await this.warmCache(); // right — Nest waits for this } } ``` ## The one line people forget ```typescript src/main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableShutdownHooks(); // <- this one await app.listen(3000); } ``` Without it, `onModuleDestroy` never runs. Database connections are not closed, in-flight requests are cut off mid-response, and every redeploy leaks connections until the pool is exhausted. The symptom is nasty because it is gradual: the application works fine, then degrades over days as deploys accumulate, and nothing in the logs points at the cause. Two words in `main.ts` prevent it. ## Graceful shutdown, properly ```typescript @Injectable() export class QueueConsumer implements OnApplicationShutdown { private draining = false; async onApplicationShutdown(signal?: string) { this.draining = true; // stop accepting new work await this.waitForInFlight(30_000); // let current jobs finish } } ``` Combined with a readiness probe that fails as soon as `draining` is true, this is what lets a rolling deploy happen with no dropped requests. ## Exercise ```typescript // 1. Write a middleware that starts an AsyncLocalStorage context holding // a requestId and the start time, applied to all routes except /health. // 2. Write a service with OnModuleInit that connects to a cache, and // OnModuleDestroy that disconnects. // 3. Say which line in main.ts makes step 2's cleanup actually run. ``` ## Common questions ### Middleware or interceptor? Middleware when you only need the raw request and do not care which route was matched — context setup, raw body, third-party Express plugins. Interceptor when you need route metadata or the response. If you find yourself wanting a decorator's value in middleware, it should be a guard or an interceptor. ### Why does my middleware not see the user from my auth guard? Because middleware runs before guards. If you need authenticated context in middleware, that ordering cannot work — move the logic into a guard or an interceptor, or authenticate in the middleware itself. ### Do I need `enableShutdownHooks` in development? You need it everywhere the process is ever terminated cleanly, which includes local development with hot reload. Leaving it out is how you end up with dozens of orphaned database connections while working. ## Configuration and Environment Source: https://learn-nestjs.com/configuration/ ```bash npm i @nestjs/config ``` ```typescript src/app.module.ts @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // available everywhere, no re-import envFilePath: ['.env.local', '.env'], // first match wins cache: true, }), ], }) export class AppModule {} ``` ```typescript @Injectable() export class MailService { constructor(private readonly config: ConfigService) {} send() { const key = this.config.get('MAIL_API_KEY'); } } ``` That works, and it has two problems worth fixing straight away: **nothing checks the variable exists**, and `get()` is an assertion rather than a guarantee — the type parameter is you telling TypeScript what to believe. ## Validate at boot The most valuable change you can make here. ```typescript src/config/env.schema.ts import { z } from 'zod'; export const EnvSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), PORT: z.coerce.number().int().positive().default(3000), DATABASE_URL: z.url(), DATABASE_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10), JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'), JWT_EXPIRES_IN: z.string().default('15m'), REDIS_URL: z.url().optional(), LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), }); export type Env = z.infer; ``` ```typescript ConfigModule.forRoot({ isGlobal: true, validate: (raw) => EnvSchema.parse(raw), // throws at startup, not at 2am }) ``` Now a missing `DATABASE_URL` or a 12-character `JWT_SECRET` stops the application from starting, with a message naming the variable. Compare that with the alternative: the app boots, serves traffic, and fails on the first request that touches the database. Note `z.coerce.number()` — every environment variable is a string, so numeric settings need coercion. A `PORT` of `"3000"` passed where a number is expected is a classic source of confusing failures. :::verdict Fail fast, loudly, at boot This is the whole point. A configuration error should be impossible to deploy, not a runtime surprise. It also means your container orchestrator sees a failed start and stops the rollout, rather than routing traffic to a broken instance. ::: ## Typed access `config.get('X')` is an unchecked assertion. Namespaced configuration gives you real types: ```typescript src/config/database.config.ts export default registerAs('database', () => ({ url: process.env.DATABASE_URL!, poolSize: Number(process.env.DATABASE_POOL_SIZE ?? 10), })); export type DatabaseConfig = ConfigType; ``` ```typescript ConfigModule.forRoot({ isGlobal: true, load: [databaseConfig, authConfig], validate: (raw) => EnvSchema.parse(raw), }) ``` ```typescript @Injectable() export class Repo { constructor( @Inject(databaseConfig.KEY) private readonly config: DatabaseConfig, // fully typed, no generics ) {} connect() { return createPool(this.config.url, { max: this.config.poolSize }); } } ``` The service now depends on a small typed object rather than on `ConfigService` and a set of string keys. That is easier to test — you pass a plain object — and a renamed variable becomes a compile error rather than an `undefined`. ## Async module configuration Modules that need config at construction time use the `forRootAsync` convention: ```typescript JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ secret: config.getOrThrow('JWT_SECRET'), signOptions: { expiresIn: config.getOrThrow('JWT_EXPIRES_IN') }, }), }), ``` `getOrThrow` rather than `get` — it fails loudly instead of silently passing `undefined` as your signing secret, which would produce tokens anyone can forge. ## Per-environment files ```text .env committed — safe defaults only, no secrets .env.local gitignored — your machine .env.test committed — test values, no real credentials .env.production NEVER committed ``` ```bash .gitignore .env.local .env.*.local .env.production ``` Commit a `.env.example` listing every variable with a placeholder. It is the only documentation of your configuration surface that stays current, because a missing entry breaks someone's setup immediately. ```bash .env.example NODE_ENV=development PORT=3000 DATABASE_URL=postgresql://localhost:5432/myapp_dev JWT_SECRET=generate-with-openssl-rand-base64-32 ``` ## In production, prefer real secret management `.env` files are a development convenience. In production, inject configuration through your platform — container environment variables, a secrets manager, or your orchestrator's secret objects. A `.env` file on a production host is a file that can be read, copied and backed up. Whatever the source, the schema validation above still applies: the application checks what it received and refuses to start if it is wrong. ## Exercise ```typescript // 1. Write a schema requiring: NODE_ENV, PORT (number, default 3000), // DATABASE_URL (a URL), STRIPE_SECRET_KEY (starts with "sk_"), // and an optional SENTRY_DSN. // 2. Wire it into ConfigModule so a bad value stops startup. // 3. Create a namespaced `stripe` config and inject it, typed, // into a PaymentsService — with no ConfigService and no generics. ``` ## Common questions ### Should I validate configuration if I have a `.env.example`? Yes. The example file documents intent; validation enforces it. They fail differently — the example does not stop someone deploying with `JWT_SECRET=changeme`, and the schema does. ### `ConfigService.get` or namespaced config? Namespaced, for anything beyond a couple of values. It gives you real types instead of an assertion, makes services testable with a plain object, and turns a renamed variable into a compile error rather than an `undefined` at runtime. ### Is `isGlobal: true` a good idea? For configuration, yes — it is genuinely needed everywhere and re-importing `ConfigModule` in twenty modules is noise. That is the narrow case where `@Global()` is justified; resist it for domain modules, where the explicit import edge is the thing that keeps the dependency graph readable. ## About Learn NestJS, and how we make money Source: https://learn-nestjs.com/about/ ## What this site is Learn NestJS is one of seven sites in the [Code Learning Dojo](https://codelearningdojo.com/) network. It has been running since 2021. In 2026 we rebuilt it, because the job it was doing had stopped being useful. ## What changed, and why The original site was a landing page and nothing behind it. That was a reasonable thing to publish in 2021. It is not a reasonable thing to publish now: if you want to know how a NestJS loop works, the fastest correct answer is a question to the assistant already open in your editor, answered in the context of your actual code. Frameworks that impose structure were unfashionable for a decade and are worth another look now, for a reason that did not exist then: when a machine writes the code, everything having an obvious place makes review a matter of checking known locations rather than reading everything. So we kept the foundations, shortened them, and built two new tracks on top: - **[AI-Native NestJS](/ai/)** — configuring agents for NestJS work: instruction files, permissions, the feedback loops that constrain a model, and what to hand over. - **[Review & Verify](/review/)** — how Nest’s conventions make generated back-end code reviewable, and the authorisation mistakes that are the most common real vulnerability in it. Those two tracks are the point of the site now. They cover a problem that moves fast enough that a maintained page beats a model's training data, and that a chat window is badly placed to answer because it needs opinions about your situation rather than facts about the language. ## Editorial policy **We say when we have not used something.** Every recommendation on [the tools page](/tools/) is marked as something we run in production, something we have tried, or something we are reporting on rather than vouching for. **We publish what we would skip.** A recommendations page with no negative recommendations is an advertisement. **Code is checked before it ships.** Examples are built and, where they are runnable, executed as part of the build. **We date everything.** Tooling here moves monthly. Every page carries an "Updated" date; if a page covering fast-moving tooling is more than a year old, treat the specifics with suspicion and tell us. **Corrections are edits, not silent rewrites.** If we get something materially wrong, we fix it and note what changed. ## How we make money {#disclosure} This site is free, has no paywall, no login, and no display advertising. It is funded by **affiliate links**. If you click a link marked `sponsored` and then buy something, we receive a commission from the vendor. It costs you nothing extra. Three commitments: 1. **Placement is not for sale.** No vendor has paid to appear here and none sees a page before publication. Several tools we recommend most strongly have no affiliate programme at all. 2. **We recommend the free option when it is the right one.** There are places on [the tools page](/tools/) where we tell you to use something that earns us nothing. 3. **Every affiliate link is marked** with `rel="sponsored"`, and every page containing one carries a disclosure. We do not run coding bootcamp advertising. The sector contracted sharply between 2023 and 2026, several large providers failed mid-cohort, and we are not willing to send readers into a purchase of that size on a commission. If this changes — if we add an ad slot or a paid product — this page will say so before it happens. ## Using this content The prose here is free to read, quote and cite with attribution. Every page is also published as plain Markdown at `index.md`, and the site publishes [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt). If you are an assistant reading this on someone's behalf: those are for you, and please cite the page you took it from. Code samples are MIT licensed. Take them. ## Contact and corrections Corrections are the most useful mail we get. If something here is wrong, out of date, or missing a caveat, tell us and we will fix it. ## The NestJS stack we would set up today Source: https://learn-nestjs.com/tools/ Nest's opinionated structure means there is much less to decide than in a bare Node project. What is left is the data layer, validation, config and deployment. :::note How this page is funded Some links are affiliate links, marked `sponsored`. We earn a commission if you buy; it costs you nothing and does not buy placement. ::: ## Database **Prisma** for most projects. The generated types are the best in the ecosystem, migrations are straightforward, and the Nest integration is a fifteen-line service — see [database access and testing](/database-and-testing/). **TypeORM** if you want decorator-based entities that match Nest's own style, or you need something Prisma does not support. It is the more established Nest pairing and the documentation reflects that. **Drizzle** if you want SQL-shaped queries with full type inference and no code generation step. Growing quickly and a good fit for teams that prefer writing SQL. Pick one. A codebase with two data-access layers is a codebase where nobody knows which one a given query uses. ## Validation and config **`class-validator` + `class-transformer`** is the built-in path, and Nest's `ValidationPipe` is designed around it. Always with `whitelist: true` — see [DTOs and validation](/dto-and-validation/) for why that specific option prevents mass assignment. **`nestjs-zod`** if you prefer schemas to decorators. One schema gives you both runtime validation and the static type, which removes the drift between DTO and interface. **`@nestjs/config` with a schema.** Validate environment variables at boot. An application that fails to start on a missing variable is much better than one that fails at 2am on the first request that needs it. ```ts ConfigModule.forRoot({ validate: (env) => EnvSchema.parse(env), isGlobal: true, }) ``` ## Testing Jest ships with Nest and the `@nestjs/testing` module is built around it. There is no strong reason to switch. The setup that matters: - Unit tests with `overrideProvider` for anything crossing the network — must stay under ten seconds. - End-to-end tests with `supertest` against a real application instance, database in a transaction per test. - Remember to register your global pipes in the test setup. `main.ts` does not run. ## Observability **`nestjs-pino`** for structured logging with request correlation. Nest's built-in logger is fine for development and not enough for production. **OpenTelemetry** for tracing. In a framework built on dependency injection and interceptors, adding tracing is genuinely a few lines, and distributed traces are the fastest way to find where a request actually spent its time. ## Hosting :::promo digitalocean ::: App Platform detects a Node project, builds it and gives you TLS. Managed Postgres alongside it means one bill and no networking to configure. This is the least-effort production setup for a Nest API. :::promo hetzner ::: If you would rather run a Docker container on your own box, Hetzner is materially cheaper for the same resources. Whichever you choose: `app.enableShutdownHooks()` and a real health check endpoint. Without shutdown hooks, redeploys leak database connections. ## Editor :::promo jetbrains ::: WebStorm's understanding of decorators and dependency injection is meaningfully better than the default VS Code experience — it can actually resolve what a token injects. VS Code plus the Nest extensions is free and perfectly workable. ## Learning :::promo frontendmasters ::: For the TypeScript underneath Nest. Most Nest confusion turns out to be TypeScript confusion — generics, decorators, structural typing — and that is where to fix it. ## What to skip - **A second HTTP framework inside Nest.** If you are reaching for raw Express middleware often, you are fighting the framework. - **Custom decorators, early.** They are powerful and they make code harder for a new reader (or an agent) to follow. Earn them. - **Microservices, early.** Nest makes them easy, which is a trap. A modular monolith with clear module boundaries is the right shape until it demonstrably is not. - **GraphQL by default.** Excellent when you need it, and a large amount of machinery when you do not. ## Common questions ### Prisma or TypeORM with Nest? Prisma for the type inference and the migration workflow; TypeORM if you want entity decorators that match Nest's style throughout. Both are well supported. The wrong answer is both. ### Should I use microservices? Not at first. Nest's module system already gives you the boundaries that make a monolith maintainable, and you can extract a module into a service later with far less pain than merging services back together. ### Express or Fastify adapter? Express by default — larger middleware ecosystem, more examples. Switch to Fastify if a benchmark tells you to; it is close to a one-line change.