NestJS Cheatsheet 0/19

Backend engineeringNestJS 12One real API, start to finish

NestJS
Cheatsheet

Twenty modules that build one tasks API, from the first module to tests and a production build. Every file shown compiles, and every terminal shows output captured from running that exact code.

TypeScript 5.6Node 22, ESM
00

Key terms in plain words

New to building servers? Start here. Every word below shows up later on this page, and each one comes with an everyday comparison. Underlined words in the modules link back to these cards.

35 terms in 5 groups. Hover an underlined word anywhere on the page for a quick reminder, or click it to jump here.

What you are building

A NestJS app works like an airport. Passengers arrive with a request, pass a few checkpoints, and leave with an answer.

API
A set of addresses that other programs can send requests to, where each address does one job. This page builds a small tasks API: list tasks, add one, change one, delete one.
For example GET /api/v1/tasks asks the tasks API for every task.
Request and response
A request is a message sent to your server, such as "give me task 7". The response is what the server sends back: the data, or an error with a number that says what went wrong.
Route
A pairing of an action (GET to read, POST to create, DELETE to remove) with an address. Nest sends each request to the one route that matches it.
For example DELETE /tasks/7 is a different route from GET /tasks/7.
Route handler
The function that does the real work for one route, such as fetching a task. Everything else on this page runs before or after it.
CLI
Command line interface: a program you run by typing in a terminal. The nest command creates a new project and writes the starting files for each new piece, so you don't have to.
For example nest g resource tasks writes a module, controller, service and DTOs in one go.
CommonJS and ESM
Two ways JavaScript files can borrow code from each other. ESM (ES modules) is the modern standard and what Nest 12 ships; CommonJS is the older style that still works alongside it.

The building blocks

The terminals, the desks and the staff, and how the airport puts the right person at the right desk.

Module
A box that groups the pieces for one feature, such as tasks or login. It decides what it keeps private and what it shares with other modules. The root module ties every other one together.
For example TasksModule holds the tasks controller and the tasks service.
Controller
A class whose methods each answer one route. It reads what the request asked for and hands the actual work to a provider.
Provider
Anything Nest can create and hand to other classes. Most often it's a service, a class that holds the real logic, like saving or finding tasks.
For example TasksService is a provider that the tasks controller asks for.
Dependency injection
A class lists what it needs, and Nest creates those things and passes them in. Everyone shares one copy, and in tests you can swap in a stand-in without touching the class.
For example constructor(private tasks: TasksService) asks Nest for the tasks service.
Decorator
A word starting with @ placed just above a class, method or parameter. It doesn't run your code; it attaches instructions Nest reads later.
For example @Get(':id') tells Nest this method answers GET requests for one task.
Injection token
Nest usually finds what to inject by its class. Plain values and interfaces have no class at run time, so you give them a name, a string or a Symbol, and ask for them by that name.
For example @Inject(CLOCK) asks for whatever was registered under CLOCK.
Dynamic module
A module that takes settings when you import it, such as a database address or a secret. forRoot and registerAsync are the usual names for the call that builds one.
Metadata
Extra information attached to a class or method by a decorator, such as "admins only". Guards read it to make decisions, and Nest reads the constructor's metadata to know what to inject.

Checking what comes in

The checkpoints a request passes before it is allowed anywhere near your code.

Middleware
Code that runs first, on every request, before Nest has worked out which handler will answer. Good for jobs that don't care about the route, like stamping each request with an id.
Guard
Code that decides whether a request may reach its handler at all. It returns true to let it through, or false or an error to stop it. Login checks and roles live here.
JWT
JSON Web Token: a signed piece of text the server gives you when you log in. You send it with every later request, and the server checks the signature instead of asking for your password again.
DTO
Data transfer object: a class that describes the shape of the data a request should carry, field by field, with rules for each one.
For example CreateTaskDto says a new task needs a title of 3 to 80 characters.
Validation
Checking incoming data against the rules before your code uses it. Anything missing, wrongly typed or unexpected is turned away with a 400 error.
Pipe
Code that takes one input just before the handler gets it, and either converts it (the text "42" into the number 42) or rejects it with an error.

Shaping what goes out

What happens around the handler, on the way back to the passenger, and when something goes wrong.

Interceptor
Code that wraps the handler: part of it runs before, part after. It can time a request, reshape every answer the same way, or give up on one that takes too long.
Observable (RxJS)
RxJS is a library for values that arrive later. An Observable is its container for such a value. Interceptors add steps to it, such as "wrap the answer" or "stop after 5 seconds".
Exception filter
Code that catches errors and decides what the client sees. One filter can give every error the same shape and hide internal details that should stay private.
HTTP exception and status code
Every response carries a number. 200 means fine, 400 means the request was wrong, 401 means not logged in, 404 means not found, 500 means the server broke. Throwing an HTTP exception sends the matching number.
For example throw new NotFoundException() answers with 404.

Running it for real

Settings, a real database, starting and stopping cleanly, documentation and tests.

Environment variable
A setting handed to the app from outside its code, such as a port number or a secret key. The same code can then run on your laptop and on a server with different settings.
For example JWT_SECRET and PORT are environment variables.
ORM
Object relational mapper: a library that lets you work with database rows as ordinary objects in your code. This page uses TypeORM.
Entity
A class that describes one table in the database: its columns and what kind of value each holds. Each object made from it is one row.
For example TaskEntity maps to the tasks table.
Repository
An object that reads and writes one kind of entity. You ask it to find, save or delete, and it writes the database commands for you.
Transaction
A group of database changes that all succeed together or are all undone together, so the data never ends up half changed.
Migration
A small saved file that changes the database's layout, such as adding a column. Running migrations in order brings any copy of the database up to date the same way.
Lifecycle hooks
Methods Nest calls at set moments: when a module has started, or when the app is about to stop. Use them to warm things up on start and tidy up on the way out.
For example onModuleInit() runs once a module's dependencies are ready.
Graceful shutdown
Stopping the app cleanly: finishing work already under way and closing connections before exiting. Hosting platforms send a SIGTERM signal to ask for this before they stop the app.
OpenAPI and Swagger
OpenAPI is a standard file that describes every route of an API. Swagger reads your controllers and DTOs to write that file, then shows it as a web page where people can try each route.
Unit test and end to end test
A unit test checks one class on its own, with everything around it replaced. An end to end (e2e) test starts the whole app and sends it real requests.
Fake
A simple pretend version of something a class depends on, such as a clock that always says the same time. It makes tests predictable and fast.
For example { provide: CLOCK, useValue: { now: () => '2026-01-01' } }
01

Setup and the CLI

Scaffold a project, generate building blocks with the CLI, and learn the folder layout every other module builds on.

nest newnest generateCommonJS or ESMProject layout

NestJS 12 ships every core package as an ES module. nest new now asks whether to scaffold a CommonJS or an ESM project, and existing CommonJS apps keep working through Node's require(esm), which is why Node 20.19 or 22.12 is the minimum. This cheatsheet uses an ESM project, so relative imports end in .js.

Terminalbash
~/tasks-api $ npm i -g @nestjs/cli
# the nest command
~/tasks-api $ nest new tasks-api
# asks for a package manager, then CommonJS or ESM
~/tasks-api $ nest g resource tasks
# module, controller, service, DTOs and an entity in one go
~/tasks-api $ nest g module auth && nest g guard auth
# or generate the pieces one at a time
~/tasks-api $ npm run start:dev
# watch mode, restarts on every save
~/tasks-api $ nest update
# migrate a v11 project to v12
tasks-api/TREE
src/
  main.ts                     bootstrap
  setup.ts                    prefix, versioning, Swagger
  app.module.ts               root module, global enhancers
  config/                     env.schema.ts, app.config.ts
  auth/                       module, controller, service, guard
  tasks/                      module, controller, service, model
    dto/                      create, update, list query
  common/                     decorators, filters, guards,
                              interceptors, middleware, pipes
  database/                   TypeORM entity, module, repository
test/                         unit and e2e specs
Building blockDecoratorJobModule
Module@Module()Groups related code and wires its dependencies03
Controller@Controller()Maps HTTP routes to handler methods04
Provider@Injectable()Business logic, injected where needed05
PipePipeTransformValidates or converts one argument07
GuardCanActivateDecides whether a request may continue09
InterceptorNestInterceptorWraps the handler, before and after10
Filter@Catch()Turns exceptions into responses08
MiddlewareNestMiddlewareRaw request work before routing11
02

Bootstrap the app

main.ts creates the application from the root module, applies app wide settings and starts listening.

NestFactoryGlobal prefixVersioningShutdown hooks

NestFactory.create builds the dependency graph from AppModule, then you apply settings that belong to the HTTP app rather than to a module. Keeping those settings in a configureApp function lets the e2e tests build exactly the same app.

src/main.tsTS
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { configureApp } from './setup.js';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  configureApp(app);
  app.enableShutdownHooks();             // call onApplicationShutdown on SIGTERM and SIGINT
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

Start it and Nest logs each module as it is initialised, then every route it mapped. Reading this log is the fastest way to check that a controller was registered and that its path is what you expect.

Terminalnode
~/tasks-api $ JWT_SECRET=change-me-in-production npm start
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [NestFactory] Starting Nest application...
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +18ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [InstanceLoader] ConfigModule dependencies initialized +1ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [InstanceLoader] JwtModule dependencies initialized +0ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [InstanceLoader] TasksModule dependencies initialized +0ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [InstanceLoader] AuthModule dependencies initialized +0ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RoutesResolver] AuthController {/api/auth} (version: 1): +28ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RouterExplorer] Mapped {/api/auth/login, POST} (version: 1) route +3ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RoutesResolver] TasksController {/api/tasks} (version: 1): +1ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RouterExplorer] Mapped {/api/tasks, GET} (version: 1) route +1ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RouterExplorer] Mapped {/api/tasks/:id, GET} (version: 1) route +0ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RouterExplorer] Mapped {/api/tasks, POST} (version: 1) route +1ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RouterExplorer] Mapped {/api/tasks/:id, PATCH} (version: 1) route +0ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [RouterExplorer] Mapped {/api/tasks/:id, DELETE} (version: 1) route +1ms
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [TasksService] Seeded 1 task, limit 100
[Nest] 1427  - 10/01/2026, 8:50:59 AM     LOG [NestApplication] Nest application successfully started +1ms
03

Modules

A module groups controllers and providers, lists what it imports and decides what it shares. The root module ties the app together.

@Moduleimports and exportsGlobal modulesDynamic modules

Providers are private to their module until it lists them in exports. Another module gets them by importing the module, never by listing the provider again, which would create a second instance. forRoot and registerAsync return dynamic modules that are configured when they are imported.

src/tasks/tasks.module.tsTS
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller.js';
import { TasksService } from './tasks.service.js';
import { clockProvider, TASKS_LIMIT } from '../common/clock.provider.js';

@Module({
  controllers: [TasksController],
  providers: [
    TasksService,                                // shorthand for { provide: TasksService, useClass: TasksService }
    clockProvider,                               // useFactory behind a Symbol token
    { provide: TASKS_LIMIT, useValue: 100 },     // useValue behind a string token
  ],
  exports: [TasksService],                       // modules that import TasksModule can inject it
})
export class TasksModule {}
src/app.module.tsTS
import { MiddlewareConsumer, Module, NestModule, ValidationPipe } from '@nestjs/common';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { ConfigModule } from '@nestjs/config';
import { appConfig } from './config/app.config.js';
import { envSchema } from './config/env.schema.js';
import { AuthModule } from './auth/auth.module.js';
import { TasksModule } from './tasks/tasks.module.js';
import { AuthGuard } from './auth/auth.guard.js';
import { RolesGuard } from './common/guards/roles.guard.js';
import { HttpErrorFilter } from './common/filters/http-error.filter.js';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor.js';
import { RequestIdMiddleware } from './common/middleware/request-id.middleware.js';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, load: [appConfig], validationSchema: envSchema }),
    AuthModule,
    TasksModule,
  ],
  providers: [
    // Global enhancers registered as providers, so they can inject dependencies
    { provide: APP_PIPE, useValue: new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }) },
    { provide: APP_GUARD, useClass: AuthGuard },
    { provide: APP_GUARD, useClass: RolesGuard },
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
    { provide: APP_FILTER, useClass: HttpErrorFilter },
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(RequestIdMiddleware).forRoutes('*path');
  }
}

Why it matters: registering guards, pipes, interceptors and filters with APP_GUARD and friends, instead of app.useGlobalGuards() in main.ts, lets them inject other providers and keeps them active in tests.

src/auth/auth.module.tsTS
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import type { ConfigType } from '@nestjs/config';
import { appConfig } from '../config/app.config.js';
import { AuthController } from './auth.controller.js';
import { AuthService } from './auth.service.js';

@Module({
  imports: [
    // A dynamic module, configured from another provider at startup
    JwtModule.registerAsync({
      global: true,
      inject: [appConfig.KEY],
      useFactory: (app: ConfigType<typeof appConfig>) => ({
        secret: app.jwtSecret,
        signOptions: { expiresIn: '15m' },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

Why it matters: registerAsync with inject waits for config to load, so secrets never have to be read at import time.

KeyHoldsNote
importsOther modulesTheir exported providers become injectable here
controllersControllersCreated by Nest and mapped to routes
providersServices, factories, valuesPrivate to this module by default
exportsA subset of providers or imported modulesWhat importing modules may inject
@Global()A decorator on the moduleExports become available everywhere; use rarely
04

Controllers and routing

Decorators on a class and its methods map HTTP verbs and paths to handlers, and pull params, query and body out of the request.

@Get, @Post@Param, @Query@Body@HttpCode

Return a value from a handler and Nest serialises it as JSON with status 200, or 201 for @Post. Throw an HTTP exception for errors. Reach for @Res() only when you need the raw response: you then send it yourself, and interceptors can no longer change what goes out unless you pass { passthrough: true }.

src/tasks/tasks.controller.tsTS
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, StandardSchemaValidationPipe, UseInterceptors } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { TasksService } from './tasks.service.js';
import { CreateTaskDto } from './dto/create-task.dto.js';
import { UpdateTaskDto } from './dto/update-task.dto.js';
import { listTasksQuery, type ListTasksQuery } from './dto/list-tasks.query.js';
import { ParseIdPipe } from '../common/pipes/parse-id.pipe.js';
import { CurrentUser } from '../common/decorators/current-user.decorator.js';
import { AdminOnly } from '../common/decorators/roles.decorator.js';
import { EnvelopeInterceptor } from '../common/interceptors/envelope.interceptor.js';

@ApiTags('tasks')
@ApiBearerAuth()
@UseInterceptors(EnvelopeInterceptor)
@Controller({ path: 'tasks', version: '1' })          // /api/v1/tasks
export class TasksController {
  constructor(private readonly tasks: TasksService) {}  // injected by its type

  @Get()                                                // GET /api/v1/tasks?status=open&page=1
  findAll(@Query({ schema: listTasksQuery, pipes: [StandardSchemaValidationPipe] }) query: ListTasksQuery) {
    return this.tasks.findAll(query);
  }

  @Get(':id')                                           // GET /api/v1/tasks/1
  findOne(@Param('id', ParseIdPipe) id: number) {
    return this.tasks.findOne(id);
  }

  @Post()                                               // POST /api/v1/tasks, 201 by default
  create(@Body() dto: CreateTaskDto, @CurrentUser('sub') userId: string) {
    return this.tasks.create(dto, userId);
  }

  @Patch(':id')
  update(@Param('id', ParseIdPipe) id: number, @Body() dto: UpdateTaskDto) {
    return this.tasks.update(id, dto);
  }

  @Delete(':id')
  @AdminOnly()
  @HttpCode(HttpStatus.NO_CONTENT)                      // 204 with an empty body
  remove(@Param('id', ParseIdPipe) id: number) {
    this.tasks.remove(id);
  }
}
Terminalcurl
~/tasks-api $ curl -s localhost:3000/api/v1/tasks -H "authorization: Bearer $TOKEN" | jq
{
  "data": {
    "page": 1,
    "total": 1,
    "items": [
      {
        "id": 1,
        "title": "Read the NestJS docs",
        "status": "open",
        "priority": 3,
        "ownerId": "u_1",
        "createdAt": "2026-10-01T08:50:59.241Z"
      }
    ]
  },
  "meta": {
    "requestId": "81cc8899"
  }
}
~/tasks-api $ curl -s -X POST localhost:3000/api/v1/tasks -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"title":"Write the NestJS cheatsheet","priority":5}' | jq
{
  "data": {
    "id": 2,
    "title": "Write the NestJS cheatsheet",
    "status": "open",
    "priority": 5,
    "ownerId": "u_1",
    "createdAt": "2026-10-01T08:51:00.467Z"
  },
  "meta": {
    "requestId": "484a92b2"
  }
}
~/tasks-api $ curl -s -X PATCH localhost:3000/api/v1/tasks/2 -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"status":"in_progress"}' | jq .data
{
  "id": 2,
  "title": "Write the NestJS cheatsheet",
  "status": "in_progress",
  "priority": 5,
  "ownerId": "u_1",
  "createdAt": "2026-10-01T08:51:00.467Z"
}
DecoratorGives youExample
@Param('id')A path segment/tasks/:id
@Query()The whole query object?status=open&page=2
@Body()The parsed JSON bodyPOST /tasks
@Headers('x-id')One request headerx-id: 42
@Req() / @Res()The platform objectsExpress request and response
@HttpCode(204)A fixed status codeDELETE
@Header('Cache-Control', 'none')A response headerStatic per route
@Redirect(url, 301)A redirectReturn { url } to change it
05

Providers and dependency injection

Mark a class @Injectable and ask for it in a constructor. For values and factories, give them a token and inject with @Inject.

@InjectableConstructor injectionCustom providersTokens

Nest reads constructor parameter types through decorator metadata and hands in a shared instance of each. Interfaces and plain values have no runtime type, so they need a token: a string or a Symbol, injected with @Inject(TOKEN).

src/common/clock.provider.tsTS
import { Provider } from '@nestjs/common';

// Tokens for things that are not classes
export const CLOCK = Symbol('CLOCK');
export const TASKS_LIMIT = 'TASKS_LIMIT';

export interface Clock {
  now(): string;
}

// useFactory: build the value with code, optionally injecting other providers
export const clockProvider: Provider = {
  provide: CLOCK,
  useFactory: (): Clock => ({ now: () => new Date().toISOString() }),
};
src/tasks/tasks.service.tsTS
import { ConflictException, Inject, Injectable, Logger, NotFoundException, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import type { ConfigType } from '@nestjs/config';
import { appConfig } from '../config/app.config.js';
import { CLOCK, TASKS_LIMIT, type Clock } from '../common/clock.provider.js';
import { CreateTaskDto } from './dto/create-task.dto.js';
import { UpdateTaskDto } from './dto/update-task.dto.js';
import type { ListTasksQuery } from './dto/list-tasks.query.js';
import { Task, TaskStatus } from './task.model.js';

@Injectable()
export class TasksService implements OnModuleInit, OnApplicationShutdown {
  private readonly logger = new Logger(TasksService.name);
  private tasks: Task[] = [];
  private nextId = 1;

  constructor(
    @Inject(appConfig.KEY) private readonly config: ConfigType<typeof appConfig>,
    @Inject(CLOCK) private readonly clock: Clock,
    @Inject(TASKS_LIMIT) private readonly limit: number,
  ) {}

  onModuleInit() {
    this.create({ title: 'Read the NestJS docs' }, 'u_1');
    this.logger.log(`Seeded ${this.tasks.length} task, limit ${this.limit}`);
  }

  onApplicationShutdown(signal?: string) {
    this.logger.log(`Shutting down on ${signal}, tasks in memory: ${this.tasks.length}`);
  }

  findAll({ status, page }: ListTasksQuery) {
    const size = this.config.pageSize;
    const rows = this.tasks.filter((t) => !status || t.status === status);
    return { page, total: rows.length, items: rows.slice((page - 1) * size, page * size) };
  }

  findOne(id: number): Task {
    const task = this.tasks.find((t) => t.id === id);
    if (!task) throw new NotFoundException(`Task ${id} not found`);
    return task;
  }

  create(dto: CreateTaskDto, ownerId: string): Task {
    if (this.tasks.length >= this.limit) throw new ConflictException('Task limit reached');
    const task: Task = {
      id: this.nextId++,
      title: dto.title,
      status: dto.status ?? TaskStatus.Open,
      priority: dto.priority ?? 3,
      ownerId,
      createdAt: this.clock.now(),
    };
    this.tasks.push(task);
    return task;
  }

  update(id: number, dto: UpdateTaskDto): Task {
    return Object.assign(this.findOne(id), dto);
  }

  remove(id: number): void {
    this.findOne(id);
    this.tasks = this.tasks.filter((t) => t.id !== id);
  }
}

Why it matters: the service never builds its own clock or reads the environment. Tests replace both with one line each, as module 18 shows.

Provider formUse it whenExample
TasksServiceA class that builds itselfShorthand for useClass
useClassSwapping one implementation for another{ provide: Mailer, useClass: SesMailer }
useValueA constant, a mock, or an existing object{ provide: TASKS_LIMIT, useValue: 100 }
useFactoryBuilding something with code or other providers{ provide: CLOCK, useFactory, inject: [] }
useExistingA second token for the same instance{ provide: 'LOGGER', useExisting: AppLogger }
scope: Scope.REQUESTOne instance per requestSlower; it spreads to every consumer
@Optional()A dependency that may be missingInjects undefined instead of failing
06

Validation

Two ways to check input: class-validator decorators on DTO classes, or since Nest 12, any Standard Schema such as Zod passed straight to @Body or @Query.

DTOsValidationPipeStandard SchemaMapped types

A DTO class describes the body. With whitelist the pipe strips unknown fields, and forbidNonWhitelisted rejects them instead. PartialType, PickType and OmitType derive new DTOs and copy the validation rules with them.

src/tasks/dto/create-task.dto.tsTS
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsInt, IsOptional, IsString, Length, Max, Min } from 'class-validator';
import { TaskStatus } from '../task.model.js';

export class CreateTaskDto {
  @ApiProperty({ example: 'Write the NestJS cheatsheet' })
  @IsString()
  @Length(3, 80)
  title!: string;

  @ApiProperty({ enum: TaskStatus, required: false })
  @IsOptional()
  @IsEnum(TaskStatus)
  status?: TaskStatus;

  @ApiProperty({ minimum: 1, maximum: 5, default: 3, required: false })
  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(5)
  priority?: number;
}
src/tasks/dto/update-task.dto.tsTS
import { PartialType, OmitType } from '@nestjs/swagger';
import { CreateTaskDto } from './create-task.dto.js';

// Every field optional, validators and docs copied from CreateTaskDto
export class UpdateTaskDto extends PartialType(CreateTaskDto) {}

// Mapped types compose: everything except title, all optional
export class UpdateStatusDto extends PartialType(OmitType(CreateTaskDto, ['title'] as const)) {}
Terminalcurl
~/tasks-api $ curl -s -X POST localhost:3000/api/v1/tasks -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"title":"x","priority":9,"done":true}' | jq
{
  "statusCode": 400,
  "message": [
    "property done should not exist",
    "title must be longer than or equal to 3 characters",
    "priority must not be greater than 5"
  ],
  "path": "/api/v1/tasks",
  "requestId": "52154c55"
}

Standard Schema in a decorator

Nest 12 adds a schema option to every parameter decorator. Pair it with StandardSchemaValidationPipe and the schema validates, coerces and fills defaults, while z.infer gives you the TypeScript type for free.

src/tasks/dto/list-tasks.query.tsTS
import { z } from 'zod';

// Nest 12: a Standard Schema can validate a parameter directly
export const listTasksQuery = z.object({
  status: z.enum(['open', 'in_progress', 'done']).optional(),
  page: z.coerce.number().int().min(1).default(1),
});

export type ListTasksQuery = z.infer<typeof listTasksQuery>;
Terminalcurl
~/tasks-api $ curl -s 'localhost:3000/api/v1/tasks?status=archived&page=0' -H "authorization: Bearer $TOKEN" | jq
{
  "statusCode": 400,
  "message": [
    "status: Invalid option: expected one of \"open\"|\"in_progress\"|\"done\"",
    "page: Too small: expected number to be >=1"
  ],
  "path": "/api/v1/tasks?status=archived&page=0",
  "requestId": "bba202e7"
}
~/tasks-api $ curl -s 'localhost:3000/api/v1/tasks?status=open&page=1' -H "authorization: Bearer $TOKEN" | jq .data.total
2
class-validator DTOStandard Schema
Defined asA class with decoratorsA schema object, from Zod, Valibot or ArkType
TypeScript typeThe class itselfz.infer<typeof schema>
Swagger docsRead from @ApiPropertyNeeds the schema converted or described
Wired withValidationPipe@Body({ schema, pipes: [StandardSchemaValidationPipe] })
Good forShared DTOs and generated docsQueries, small bodies, schemas shared with a frontend
07

Pipes

A pipe receives one argument before the handler, and either returns a converted value or throws. Built in pipes cover the common cases.

ParseIntPipeCustom pipesPipe orderDefaultValuePipe

Pass a pipe as the second argument of a parameter decorator, as in @Param('id', ParseIdPipe), or register it for a controller, a route or the whole app. Global pipes run first, so with transform: true the value your own pipe receives is already converted to the type you declared.

src/common/pipes/parse-id.pipe.tsTS
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';

// A pipe transforms or rejects one argument before the handler runs
@Injectable()
export class ParseIdPipe implements PipeTransform<unknown, number> {
  transform(value: unknown, meta: ArgumentMetadata): number {
    const id = Number(value);
    if (!Number.isInteger(id) || id < 1) {
      throw new BadRequestException(`${meta.data} must be a positive integer`);
    }
    return id;
  }
}
Terminalcurl
~/tasks-api $ curl -s localhost:3000/api/v1/tasks/abc -H "authorization: Bearer $TOKEN" | jq
{
  "statusCode": 400,
  "message": "id must be a positive integer",
  "path": "/api/v1/tasks/abc",
  "requestId": "8c475d9c"
}
PipeTurnsInto
ParseIntPipe, ParseFloatPipe"42"42
ParseBoolPipe"true"true
ParseUUIDPipeA stringThe same string, or 400 if it is not a UUID
ParseEnumPipe(Status)A stringA member of the enum
ParseArrayPipe"1,2,3"[1, 2, 3] with items validated
ParseDatePipeAn ISO stringA Date
DefaultValuePipe(1)undefined1, put it before a parse pipe
ValidationPipeA plain objectA validated DTO instance
08

Errors and exception filters

Throw HttpException subclasses from anywhere. An exception filter catches them and decides what the client sees.

HttpExceptionBuilt in exceptions@CatchError shape

Nest already turns a thrown NotFoundException into a 404. Write a filter when every error should share one JSON shape, carry a request id, or hide internal messages. @Catch() with no arguments catches everything, including plain errors, which become a 500.

src/common/filters/http-error.filter.tsTS
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';
import type { Request, Response } from 'express';

// @Catch() with no arguments catches everything
@Catch()
export class HttpErrorFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpErrorFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const req = ctx.getRequest<Request>();
    const res = ctx.getResponse<Response>();

    const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
    const body = exception instanceof HttpException ? exception.getResponse() : 'Internal server error';
    const message = typeof body === 'string' ? body : (body as { message: string | string[] }).message;

    if (status >= 500) this.logger.error(exception);
    res.status(status).json({ statusCode: status, message, path: req.url, requestId: req.headers['x-request-id'] });
  }
}
Terminalcurl
~/tasks-api $ curl -s localhost:3000/api/v1/tasks/99 -H "authorization: Bearer $TOKEN" | jq
{
  "statusCode": 404,
  "message": "Task 99 not found",
  "path": "/api/v1/tasks/99",
  "requestId": "f838b2cb"
}
ExceptionStatus
BadRequestException400
UnauthorizedException401
ForbiddenException403
NotFoundException404
ConflictException409
UnprocessableEntityException422
RequestTimeoutException408
InternalServerErrorException500
ServiceUnavailableException503
09

Guards and JWT auth

A guard returns true to let a request through. Combine an auth guard that reads a JWT with a roles guard that reads route metadata.

CanActivateJwtService@PublicRoles

Guards run after middleware and before pipes, and they get an ExecutionContext, so they can read the handler's metadata through the Reflector. Here every route requires a token unless it is marked @Public(), and routes marked @AdminOnly() also need the admin role.

src/auth/auth.service.tsTS
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Role } from '../common/decorators/roles.decorator.js';

export interface JwtUser {
  sub: string;
  email: string;
  role: Role;
}

// Demo users; a real app reads them from the database with hashed passwords
const USERS = [
  { sub: 'u_1', email: 'shree@example.com', password: 'secret', role: Role.Admin },
  { sub: 'u_2', email: 'ana@example.com', password: 'secret', role: Role.User },
];

@Injectable()
export class AuthService {
  constructor(private readonly jwt: JwtService) {}

  async login(email: string, password: string) {
    const user = USERS.find((u) => u.email === email && u.password === password);
    if (!user) throw new UnauthorizedException('Invalid credentials');
    const payload: JwtUser = { sub: user.sub, email: user.email, role: user.role };
    return { accessToken: await this.jwt.signAsync(payload) };
  }
}
src/auth/auth.guard.tsTS
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { IS_PUBLIC } from '../common/decorators/public.decorator.js';
import type { JwtUser } from './auth.service.js';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly jwt: JwtService, private readonly reflector: Reflector) {}

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC, [ctx.getHandler(), ctx.getClass()]);
    if (isPublic) return true;

    const req = ctx.switchToHttp().getRequest();
    const [scheme, token] = (req.headers.authorization ?? '').split(' ');
    if (scheme !== 'Bearer' || !token) throw new UnauthorizedException('Missing bearer token');

    try {
      req.user = await this.jwt.verifyAsync<JwtUser>(token);   // later steps read request.user
      return true;
    } catch {
      throw new UnauthorizedException('Invalid or expired token');
    }
  }
}
src/common/guards/roles.guard.tsTS
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../decorators/roles.decorator.js';
import type { JwtUser } from '../../auth/auth.service.js';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    // The handler's metadata wins, then the controller's
    const required = this.reflector.getAllAndOverride(Roles, [ctx.getHandler(), ctx.getClass()]);
    if (!required?.length) return true;

    const user: JwtUser | undefined = ctx.switchToHttp().getRequest().user;
    if (user && required.includes(user.role)) return true;
    throw new ForbiddenException(`Requires role: ${required.join(', ')}`);
  }
}
Terminalcurl
~/tasks-api $ curl -s -X POST localhost:3000/api/v1/auth/login -H 'content-type: application/json' -d '{"email":"shree@example.com","password":"secret"}' | jq
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1XzEiLCJlbWFpbCI6InNocmVlQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzkwODQ0NjYwLCJleHAiOjE3OTA4NDU1NjB9.4WidIFyk13GQo8l4FYXxFzJTH1gounG9AIwM2Qm4axM"
}
~/tasks-api $ TOKEN=$(curl -s -X POST localhost:3000/api/v1/auth/login -H 'content-type: application/json' -d '{"email":"shree@example.com","password":"secret"}' | jq -r .accessToken)
# keep the token for the next calls
~/tasks-api $ curl -s localhost:3000/api/v1/tasks | jq
{
  "statusCode": 401,
  "message": "Missing bearer token",
  "path": "/api/v1/tasks",
  "requestId": "ffa986fa"
}
~/tasks-api $ curl -s -X DELETE localhost:3000/api/v1/tasks/1 -H "authorization: Bearer $USER_TOKEN" | jq
{
  "statusCode": 403,
  "message": "Requires role: admin",
  "path": "/api/v1/tasks/1",
  "requestId": "155a6c2d"
}
~/tasks-api $ curl -si -X DELETE localhost:3000/api/v1/tasks/1 -H "authorization: Bearer $TOKEN" | head -n 1
HTTP/1.1 204 No Content
10

Interceptors

An interceptor wraps the handler. Run code before it, then transform, time, cache or retry what it returns with RxJS operators.

NestInterceptornext.handle()map, tap, timeoutResponse envelope

next.handle() returns an Observable of the handler's result. Everything before that call runs on the way in, and the operators you pipe onto it run on the way out. That makes interceptors the place for logging, response shaping and timeouts.

src/common/interceptors/logging.interceptor.tsTS
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger('HTTP');

  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const { method, url } = ctx.switchToHttp().getRequest();
    const started = Date.now();
    // Code before next.handle() runs before the handler; tap() runs after it
    return next.handle().pipe(
      tap(() => this.logger.log(`${method} ${url} ${Date.now() - started}ms`)),
    );
  }
}
src/common/interceptors/envelope.interceptor.tsTS
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable, timeout } from 'rxjs';

export interface Envelope<T> {
  data: T;
  meta: { requestId: string | undefined };
}

// Reshape every successful response, and fail handlers slower than 5 seconds
@Injectable()
export class EnvelopeInterceptor<T> implements NestInterceptor<T, Envelope<T>> {
  intercept(ctx: ExecutionContext, next: CallHandler<T>): Observable<Envelope<T>> {
    const requestId = ctx.switchToHttp().getRequest().headers['x-request-id'];
    return next.handle().pipe(
      timeout(5000),
      map((data) => ({ data, meta: { requestId } })),
    );
  }
}

Why it matters: the controller returns plain objects and stays easy to test. The envelope is added in one place, for every route the interceptor is bound to.

The logging interceptor is global, so the server prints one line per handled request.

Terminalserver log
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] POST /api/v1/auth/login 17ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] POST /api/v1/auth/login 2ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] POST /api/v1/auth/login 1ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] GET /api/v1/tasks 7ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] POST /api/v1/tasks 9ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] GET /api/v1/tasks?status=open&page=1 1ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] PATCH /api/v1/tasks/2 1ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] DELETE /api/v1/tasks/1 0ms
[Nest] 1427  - 10/01/2026, 8:51:00 AM     LOG [HTTP] GET /api/v1/tasks 0ms
11

Middleware

Express style functions that run before routing. Use them for work that needs the raw request, such as ids, logging or body tweaks.

NestMiddlewareMiddlewareConsumerforRoutesexclude

Middleware is applied in a module's configure method rather than with a decorator. It knows nothing about the handler that will run, which is why authorisation belongs in guards. Nest 12 runs on Express 5, so a catch all route is written '*path', a named wildcard.

src/common/middleware/request-id.middleware.tsTS
import { Injectable, NestMiddleware } from '@nestjs/common';
import type { NextFunction, Request, Response } from 'express';
import { randomUUID } from 'node:crypto';

// Middleware runs first, before guards, with the raw request and response
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const id = (req.headers['x-request-id'] as string | undefined) ?? randomUUID().slice(0, 8);
    req.headers['x-request-id'] = id;
    res.setHeader('x-request-id', id);
    next();
  }
}
src/app.module.tsTS
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(RequestIdMiddleware).forRoutes('*path');
  }
}
Terminalcurl
~/tasks-api $ curl -si localhost:3000/api/v1/tasks -H "authorization: Bearer $TOKEN" -H 'x-request-id: trace-42' | grep -i -E '^(HTTP|x-request-id)'
HTTP/1.1 200 OK
x-request-id: trace-42
12

Custom decorators

Make your own parameter decorators, typed metadata decorators, and bundles of decorators that read as one.

createParamDecoratorReflector.createDecoratorSetMetadataapplyDecorators

createParamDecorator pulls something out of the request, like the user a guard attached. Reflector.createDecorator makes a typed metadata decorator that guards read back. applyDecorators folds several decorators into one name so routes stay short.

src/common/decorators/current-user.decorator.tsTS
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { JwtUser } from '../../auth/auth.service.js';

// @CurrentUser() gives the whole user, @CurrentUser('sub') one field
export const CurrentUser = createParamDecorator(
  (field: keyof JwtUser | undefined, ctx: ExecutionContext) => {
    const user: JwtUser = ctx.switchToHttp().getRequest().user;
    return field ? user[field] : user;
  },
);
src/common/decorators/public.decorator.tsTS
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC, true);
src/common/decorators/roles.decorator.tsTS
import { applyDecorators } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ApiBearerAuth, ApiForbiddenResponse } from '@nestjs/swagger';

export enum Role {
  User = 'user',
  Admin = 'admin',
}

// A typed metadata decorator: @Roles([Role.Admin])
export const Roles = Reflector.createDecorator<Role[]>();

// applyDecorators bundles several decorators into one
export function AdminOnly() {
  return applyDecorators(Roles([Role.Admin]), ApiBearerAuth(), ApiForbiddenResponse({ description: 'Admins only' }));
}
13

The request lifecycle

The order every request travels through. Knowing it tells you where each kind of logic belongs.

Order of executionGlobal to routeWhere to put what

Within each stage, global enhancers run first, then those on the controller, then those on the route. Interceptors wrap everything after them, so their after code runs in reverse order. A filter only runs when something along the way throws.

One request, in order

MWGIP →fn→ IF Middleware, guards, interceptors, pipes, the handler, interceptors again. The filter only runs on an error.
StageRunsCan it stop the request?Use it for
1MiddlewareYes, by not calling next()Request ids, raw body handling, CORS
2GuardsYes, by returning false or throwingAuthentication and roles
3Interceptors, beforeYes, by not calling handle()Timing, caching lookups
4PipesYes, by throwingValidation and conversion
5HandlerYes, by throwingThe route's actual work
6Interceptors, afterThey can replace the resultEnvelopes, mapping, timeouts
7Exception filtersThey write the error responseOne error shape for the API
14

Configuration

Load environment variables once, validate them at startup, and inject typed slices of config where you need them.

ConfigModuleregisterAsConfigTypeStartup validation

In Nest 12, ConfigModule.forRoot takes any Standard Schema as validationSchema, replacing the Joi specific option. If the environment does not match, the app refuses to start, which is far better than failing on the first request that reads a missing secret.

src/config/env.schema.tsTS
import { z } from 'zod';

// Any Standard Schema library works here: Zod, Valibot, ArkType
export const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().int().default(3000),
  JWT_SECRET: z.string().min(16, 'JWT_SECRET must be at least 16 characters'),
  PAGE_SIZE: z.coerce.number().int().min(1).max(100).default(20),
});
src/config/app.config.tsTS
import { registerAs } from '@nestjs/config';

// A namespaced, typed slice of config: inject it with @Inject(appConfig.KEY)
export const appConfig = registerAs('app', () => ({
  name: 'tasks-api',
  jwtSecret: process.env.JWT_SECRET!,
  pageSize: Number(process.env.PAGE_SIZE ?? 20),
}));

Why it matters: inject the slice with @Inject(appConfig.KEY) and type it with ConfigType<typeof appConfig>. Typos in config keys become compile errors.

Terminalnode
~/tasks-api $ npm start
# JWT_SECRET is not set
[Nest] 1419  - 10/01/2026, 8:50:57 AM     LOG [NestFactory] Starting Nest application...
[Nest] 1419  - 10/01/2026, 8:50:57 AM   ERROR [ExceptionHandler] Error: Config validation error: JWT_SECRET: Invalid input: expected string, received undefined
15

Database with TypeORM

Register a connection from config, declare entities, inject repositories, and group writes in transactions.

TypeOrmModule@Entity@InjectRepositoryTransactions

forRootAsync opens one connection for the app, and forFeature registers the entities a module may use, which makes Repository<TaskEntity> injectable there. Keep synchronize off outside local development and change the schema with migrations.

src/database/task.entity.tsTS
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import { TaskStatus } from '../tasks/task.model.js';

@Entity('tasks')
export class TaskEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ length: 80 })
  title!: string;

  @Index()
  @Column({ type: 'enum', enum: TaskStatus, default: TaskStatus.Open })
  status!: TaskStatus;

  @Column({ default: 3 })
  priority!: number;

  @Column()
  ownerId!: string;

  @CreateDateColumn()
  createdAt!: Date;
}
src/database/database.module.tsTS
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TaskEntity } from './task.entity.js';
import { TasksRepository } from './tasks.repository.js';

@Module({
  imports: [
    // forRootAsync: read the connection settings from config at startup
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        url: config.getOrThrow<string>('DATABASE_URL'),
        autoLoadEntities: true,     // pick up every entity registered with forFeature
        synchronize: false,         // use migrations in every shared environment
      }),
    }),
    TypeOrmModule.forFeature([TaskEntity]),   // makes Repository<TaskEntity> injectable here
  ],
  providers: [TasksRepository],
  exports: [TasksRepository],
})
export class DatabaseModule {}
src/database/tasks.repository.tsTS
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { TaskEntity } from './task.entity.js';
import { TaskStatus } from '../tasks/task.model.js';

@Injectable()
export class TasksRepository {
  constructor(
    @InjectRepository(TaskEntity) private readonly repo: Repository<TaskEntity>,
    private readonly dataSource: DataSource,
  ) {}

  async page(status: TaskStatus | undefined, page: number, size: number) {
    const [items, total] = await this.repo.findAndCount({
      where: status ? { status } : {},
      order: { createdAt: 'DESC' },
      skip: (page - 1) * size,
      take: size,
    });
    return { page, total, items };
  }

  create(data: Pick<TaskEntity, 'title' | 'ownerId'>) {
    return this.repo.save(this.repo.create(data));
  }

  // Both writes commit together or not at all
  async moveAll(fromOwner: string, toOwner: string) {
    return this.dataSource.transaction(async (manager) => {
      const result = await manager.update(TaskEntity, { ownerId: fromOwner }, { ownerId: toOwner });
      await manager.query('INSERT INTO audit_log (action) VALUES ($1)', [`move ${fromOwner} to ${toOwner}`]);
      return result.affected ?? 0;
    });
  }
}

Why it matters: dataSource.transaction hands you an entity manager bound to one transaction. Use it, not the injected repository, for every write inside the callback.

These three files compile against TypeORM 1.1 in the same project, but they are not wired into the running demo, which keeps its tasks in memory so it needs no database.

16

Lifecycle hooks and shutdown

Run code when a module starts or the app stops: warm caches on boot, close connections and finish work on shutdown.

OnModuleInitOnApplicationBootstrapenableShutdownHooksSIGTERM

Implement the interface on any provider or module. Init hooks run after dependencies are resolved, and they may be async; Nest waits for them. Shutdown hooks only run if you call app.enableShutdownHooks(), which listens for signals such as the SIGTERM a container platform sends before it stops your app.

src/tasks/tasks.service.tsTS
onModuleInit() {
  this.create({ title: 'Read the NestJS docs' }, 'u_1');
  this.logger.log(`Seeded ${this.tasks.length} task, limit ${this.limit}`);
}

onApplicationShutdown(signal?: string) {
  this.logger.log(`Shutting down on ${signal}, tasks in memory: ${this.tasks.length}`);
}
Terminalnode
~/tasks-api $ kill -TERM $(lsof -t -i :3000)
[Nest] 1427  - 10/01/2026, 8:51:01 AM     LOG [TasksService] Shutting down on SIGTERM, tasks in memory: 1
HookRuns
onModuleInit()After this module's dependencies are resolved
onApplicationBootstrap()After every module is initialised, before listening
onModuleDestroy()When a shutdown signal arrives
beforeApplicationShutdown(signal)After every onModuleDestroy, before connections close
onApplicationShutdown(signal)After connections close
17

OpenAPI docs with Swagger

Generate an OpenAPI document from your controllers and DTOs, then serve interactive docs and the raw JSON.

DocumentBuilder@ApiProperty@ApiTagsdocs-json

SwaggerModule.setup serves the UI at /docs and the document at /docs-json. Routes, params and versioned paths are read from the controllers; body shapes come from @ApiProperty on DTOs. The CLI plugin can add most of those decorators for you at build time.

src/setup.tsTS
import { INestApplication, VersioningType } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

// Shared by main.ts and the e2e tests, so both run the same app
export function configureApp(app: INestApplication) {
  app.setGlobalPrefix('api');                              // every route starts with /api
  app.enableVersioning({ type: VersioningType.URI });      // /api/v1/...
  app.enableCors();

  const config = new DocumentBuilder().setTitle('Tasks API').setVersion('1.0').addBearerAuth().build();
  SwaggerModule.setup('docs', app, () => SwaggerModule.createDocument(app, config));
  return app;
}
Terminalcurl
~/tasks-api $ curl -s localhost:3000/docs-json | jq '.paths | keys'
[
  "/api/v1/auth/login",
  "/api/v1/tasks",
  "/api/v1/tasks/{id}"
]
~/tasks-api $ curl -s localhost:3000/docs-json | jq .components.schemas.CreateTaskDto.required
[
  "title"
]
18

Testing

Build a small testing module with fakes for unit tests, or the whole app with one provider overridden for end to end tests.

Test.createTestingModuleoverrideProvidersupertestnode:test

Test.createTestingModule takes the same shape as @Module, so you list the class under test and a fake for each token it injects. For e2e tests, import AppModule, swap only what must be predictable, and send real HTTP requests with supertest. ESM projects from nest new default to Vitest; these specs use Node's built in runner, and the Nest code is identical either way.

test/tasks.service.spec.tsTS
import { beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { NotFoundException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { TasksService } from '../src/tasks/tasks.service.js';
import { appConfig } from '../src/config/app.config.js';
import { CLOCK, TASKS_LIMIT } from '../src/common/clock.provider.js';

describe('TasksService', () => {
  let service: TasksService;

  beforeEach(async () => {
    // A small module with only what the service needs, every dependency faked
    const moduleRef = await Test.createTestingModule({
      providers: [
        TasksService,
        { provide: appConfig.KEY, useValue: { pageSize: 2 } },
        { provide: CLOCK, useValue: { now: () => '2026-01-01T00:00:00.000Z' } },
        { provide: TASKS_LIMIT, useValue: 3 },
      ],
    }).compile();
    service = moduleRef.get(TasksService);
  });

  it('creates a task with default status and priority', () => {
    const task = service.create({ title: 'Ship it' }, 'u_1');
    assert.deepEqual(task, {
      id: 1, title: 'Ship it', status: 'open', priority: 3, ownerId: 'u_1', createdAt: '2026-01-01T00:00:00.000Z',
    });
  });

  it('throws NotFoundException for a missing id', () => {
    assert.throws(() => service.findOne(42), NotFoundException);
  });

  it('pages with the configured page size', () => {
    ['a', 'b', 'c'].forEach((title) => service.create({ title }, 'u_1'));
    assert.equal(service.findAll({ page: 1 }).items.length, 2);
    assert.equal(service.findAll({ page: 2 }).items.length, 1);
  });
});
test/tasks.e2e.spec.tsTS
import { after, before, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
import { configureApp } from '../src/setup.js';
import { CLOCK } from '../src/common/clock.provider.js';

describe('Tasks API (e2e)', () => {
  let app: INestApplication;
  let token: string;

  before(async () => {
    const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
      .overrideProvider(CLOCK)                       // swap one provider, keep the rest real
      .useValue({ now: () => '2026-01-01T00:00:00.000Z' })
      .compile();
    app = configureApp(moduleRef.createNestApplication({ logger: false }));
    await app.init();

    const res = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ email: 'shree@example.com', password: 'secret' });
    token = res.body.accessToken;
  });

  after(() => app.close());

  it('rejects requests without a token', async () => {
    await request(app.getHttpServer()).get('/api/v1/tasks').expect(401);
  });

  it('creates a task', async () => {
    const res = await request(app.getHttpServer())
      .post('/api/v1/tasks')
      .auth(token, { type: 'bearer' })
      .send({ title: 'From the e2e test', priority: 5 })
      .expect(201);
    assert.equal(res.body.data.createdAt, '2026-01-01T00:00:00.000Z');
  });

  it('validates the body', async () => {
    const res = await request(app.getHttpServer())
      .post('/api/v1/tasks')
      .auth(token, { type: 'bearer' })
      .send({ title: 'x' })
      .expect(400);
    assert.deepEqual(res.body.message, ['title must be longer than or equal to 3 characters']);
  });
});
Terminalnode --test
~/tasks-api $ npm run build && JWT_SECRET=test-secret-123456 npm test
▶ Tasks API (e2e)
  ✔ rejects requests without a token (9.510639ms)
  ✔ creates a task (18.857679ms)
  ✔ validates the body (10.826011ms)
✔ Tasks API (e2e) (526.387395ms)
▶ TasksService
  ✔ creates a task with default status and priority (291.913823ms)
  ✔ throws NotFoundException for a missing id (10.951614ms)
  ✔ pages with the configured page size (10.515574ms)
✔ TasksService (314.748781ms)
ℹ tests 6
ℹ suites 2
ℹ pass 6
ℹ fail 0
ℹ duration_ms 2916.395949
19

Build and ship

The compiler settings Nest needs, the scripts that build and run it, and a short checklist before the first deploy.

tsconfigDecorator metadataNode 20.19+Checklist

Nest depends on experimentalDecorators and emitDecoratorMetadata; without metadata, injection by type silently breaks. That is also why runners built on esbuild, which does not emit metadata, need an SWC or tsc step for Nest code.

tsconfig.jsonJSON
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "rootDir": ".",
    "outDir": "dist",
    "sourceMap": true
  },
  "include": ["src", "test"]
}
Terminalbash
~/tasks-api $ npm run build
# tsc writes dist/
~/tasks-api $ NODE_ENV=production node dist/src/main.js
# run the compiled app
CheckWhy
Node 20.19+ or 22.12+Required by Nest 12 for require(esm)
ValidationPipe({ whitelist: true })Unknown fields never reach your services
enableShutdownHooks()Connections close cleanly on SIGTERM
synchronize: falseSchema changes go through migrations
Config validated at startupA bad deploy fails fast, before taking traffic
One error filterNo stack traces or internals in responses
helmet and rate limitingCommon headers and abuse protection; see @nestjs/throttler
nest updateApplies the v11 to v12 migration steps
20

Which one do I need?

Start from what you are trying to do, then reach for the tool in the middle column. The last column takes you to the module that explains it.

I want toReach forExampleModule
Group related featuresA module@Module({ providers, controllers })03
Share a service with another moduleexports, then importsexports: [TasksService]03
Map a URL to codeA controller method@Get(':id')04
Hold business logicA provider@Injectable() class TasksService05
Inject a value or interfaceA token@Inject(CLOCK)05
Validate a bodyDTO and ValidationPipe@IsString() title!: string06
Validate with ZodStandard Schema option@Query({ schema, pipes })06
Convert one argumentA pipe@Param('id', ParseIntPipe)07
Shape every errorAn exception filter@Catch()08
Require a loginA guardAPP_GUARD, AuthGuard09
Allow only some rolesMetadata plus a guard@Roles([Role.Admin])09
Wrap or time responsesAn interceptornext.handle().pipe(map())10
Touch the raw request firstMiddlewareconsumer.apply(...).forRoutes()11
Read the current userA param decoratorcreateParamDecorator12
Load and check env varsConfigModulevalidationSchema: envSchema14
Talk to PostgresTypeORM repository@InjectRepository(TaskEntity)15
Run code on start or stopLifecycle hooksonApplicationShutdown()16
Publish API docsSwaggerSwaggerModule.setup('docs')17
Fake a dependency in testsoverrideProvider.overrideProvider(CLOCK).useValue()18