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.
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
- Think of it as the list of flights an airport offers, each with its own gate
- 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/tasksasks the tasks API for every task. - Request and response
- Think of it as a passenger arriving with a question and leaving with an answer
- 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
- Think of it as a numbered gate, such as Gate 12 for flights to Delhi
- 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/7is a different route fromGET /tasks/7. - Route handler
- Think of it as the flight itself, the reason for the whole trip
- 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
- Think of it as the planning office that draws up new terminals from a standard plan
- Command line interface: a program you run by typing in a terminal. The
nestcommand creates a new project and writes the starting files for each new piece, so you don't have to. - For example
nest g resource taskswrites a module, controller, service and DTOs in one go. - CommonJS and ESM
- Think of it as two kinds of luggage tag, an older one and a newer standard
- 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
- Think of it as one terminal of the airport, with its own desks and staff
- 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
TasksModuleholds the tasks controller and the tasks service. - Controller
- Think of it as the desk at a gate that takes each passenger's request and passes it to the right staff
- A class whose methods each answer one route. It reads what the request asked for and hands the actual work to a provider.
- Provider
- Think of it as a staff team, such as catering or baggage, that any desk can call on
- 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
TasksServiceis a provider that the tasks controller asks for. - Dependency injection
- Think of it as the airport assigning staff to a desk, so no desk hires its own
- 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
- Think of it as a label stuck on a desk or a bag that tells the airport how to treat it
- 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
- Think of it as a named key on the staff board, for helpers who have no uniform to recognise them by
- 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 underCLOCK. - Dynamic module
- Think of it as a ready-made terminal you order with your own settings filled in
- A module that takes settings when you import it, such as a database address or a secret.
forRootandregisterAsyncare the usual names for the call that builds one. - Metadata
- Think of it as notes written on a passenger's file that staff further along can read
- 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
- Think of it as the doors and check-in hall every passenger walks through before anyone looks at their flight
- 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
- Think of it as passport control, which only ever says yes or no
- 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
- Think of it as a passport with a stamp that shows at once if anyone has tampered with it
- 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
- Think of it as the arrival form listing exactly which details a passenger must fill in
- 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
CreateTaskDtosays a new task needs atitleof 3 to 80 characters. - Validation
- Think of it as checking each arrival form is filled in properly before the passenger moves on
- 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
- Think of it as the boarding pass scanner at the gate, the last check before you board
- 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
- Think of it as a host who meets you before the gate and sees you off after you land
- 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)
- Think of it as a baggage belt that will carry the result out once it's ready
- 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
- Think of it as the customer service desk that turns any problem into a clear notice for the passenger
- 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
- Think of it as the short code on the departure board: delayed, cancelled, gate changed
- 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
- Think of it as the notice pinned up each morning with today's runway and staff numbers
- 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_SECRETandPORTare environment variables. - ORM
- Think of it as an interpreter who turns your plain requests into the database's own language
- Object relational mapper: a library that lets you work with database rows as ordinary objects in your code. This page uses TypeORM.
- Entity
- Think of it as the template for one row in the airport's passenger ledger
- 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
TaskEntitymaps to thetaskstable. - Repository
- Think of it as the records clerk for one ledger, who finds, adds and edits entries for you
- 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
- Think of it as a booking where every leg of the journey is confirmed together, or none of them is
- A group of database changes that all succeed together or are all undone together, so the data never ends up half changed.
- Migration
- Think of it as a written, numbered plan for rebuilding part of the terminal
- 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
- Think of it as the opening and closing checklists a terminal runs every day
- 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
- Think of it as closing the gates but letting passengers already on board fly
- 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
- Think of it as the printed airport guide, kept up to date from the signs themselves
- 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
- Think of it as testing one scanner on a bench, compared with walking a real passenger through the whole airport
- 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
- Think of it as a stand-in staff member used in a fire drill
- 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' } }
Setup and the CLI
Scaffold a project, generate building blocks with the CLI, and learn the folder layout every other module builds on.
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.
~/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
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 block | Decorator | Job | Module |
|---|---|---|---|
| Module | @Module() | Groups related code and wires its dependencies | 03 |
| Controller | @Controller() | Maps HTTP routes to handler methods | 04 |
| Provider | @Injectable() | Business logic, injected where needed | 05 |
| Pipe | PipeTransform | Validates or converts one argument | 07 |
| Guard | CanActivate | Decides whether a request may continue | 09 |
| Interceptor | NestInterceptor | Wraps the handler, before and after | 10 |
| Filter | @Catch() | Turns exceptions into responses | 08 |
| Middleware | NestMiddleware | Raw request work before routing | 11 |
Bootstrap the app
main.ts creates the application from the root module, applies app wide settings and starts listening.
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.
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.
~/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
Modules
A module groups controllers and providers, lists what it imports and decides what it shares. The root module ties the app together.
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.
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 {}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.
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.
| Key | Holds | Note |
|---|---|---|
imports | Other modules | Their exported providers become injectable here |
controllers | Controllers | Created by Nest and mapped to routes |
providers | Services, factories, values | Private to this module by default |
exports | A subset of providers or imported modules | What importing modules may inject |
@Global() | A decorator on the module | Exports become available everywhere; use rarely |
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.
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 }.
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);
}
}~/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" }
| Decorator | Gives you | Example |
|---|---|---|
@Param('id') | A path segment | /tasks/:id |
@Query() | The whole query object | ?status=open&page=2 |
@Body() | The parsed JSON body | POST /tasks |
@Headers('x-id') | One request header | x-id: 42 |
@Req() / @Res() | The platform objects | Express request and response |
@HttpCode(204) | A fixed status code | DELETE |
@Header('Cache-Control', 'none') | A response header | Static per route |
@Redirect(url, 301) | A redirect | Return { url } to change it |
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.
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).
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() }),
};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 form | Use it when | Example |
|---|---|---|
TasksService | A class that builds itself | Shorthand for useClass |
useClass | Swapping one implementation for another | { provide: Mailer, useClass: SesMailer } |
useValue | A constant, a mock, or an existing object | { provide: TASKS_LIMIT, useValue: 100 } |
useFactory | Building something with code or other providers | { provide: CLOCK, useFactory, inject: [] } |
useExisting | A second token for the same instance | { provide: 'LOGGER', useExisting: AppLogger } |
scope: Scope.REQUEST | One instance per request | Slower; it spreads to every consumer |
@Optional() | A dependency that may be missing | Injects undefined instead of failing |
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.
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.
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;
}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)) {}~/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.
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>;~/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 DTO | Standard Schema | |
|---|---|---|
| Defined as | A class with decorators | A schema object, from Zod, Valibot or ArkType |
| TypeScript type | The class itself | z.infer<typeof schema> |
| Swagger docs | Read from @ApiProperty | Needs the schema converted or described |
| Wired with | ValidationPipe | @Body({ schema, pipes: [StandardSchemaValidationPipe] }) |
| Good for | Shared DTOs and generated docs | Queries, small bodies, schemas shared with a frontend |
Pipes
A pipe receives one argument before the handler, and either returns a converted value or throws. Built in pipes cover the common cases.
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.
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;
}
}~/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" }
| Pipe | Turns | Into |
|---|---|---|
ParseIntPipe, ParseFloatPipe | "42" | 42 |
ParseBoolPipe | "true" | true |
ParseUUIDPipe | A string | The same string, or 400 if it is not a UUID |
ParseEnumPipe(Status) | A string | A member of the enum |
ParseArrayPipe | "1,2,3" | [1, 2, 3] with items validated |
ParseDatePipe | An ISO string | A Date |
DefaultValuePipe(1) | undefined | 1, put it before a parse pipe |
ValidationPipe | A plain object | A validated DTO instance |
Errors and exception filters
Throw HttpException subclasses from anywhere. An exception filter catches them and decides what the client sees.
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.
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'] });
}
}~/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" }
| Exception | Status |
|---|---|
BadRequestException | 400 |
UnauthorizedException | 401 |
ForbiddenException | 403 |
NotFoundException | 404 |
ConflictException | 409 |
UnprocessableEntityException | 422 |
RequestTimeoutException | 408 |
InternalServerErrorException | 500 |
ServiceUnavailableException | 503 |
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.
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.
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) };
}
}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');
}
}
}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(', ')}`);
}
}~/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
Interceptors
An interceptor wraps the handler. Run code before it, then transform, time, cache or retry what it returns with RxJS operators.
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.
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`)),
);
}
}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.
[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
Middleware
Express style functions that run before routing. Use them for work that needs the raw request, such as ids, logging or body tweaks.
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.
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();
}
}export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*path');
}
}~/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
Custom decorators
Make your own parameter decorators, typed metadata decorators, and bundles of decorators that read as one.
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.
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;
},
);import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC, true);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' }));
}The request lifecycle
The order every request travels through. Knowing it tells you where each kind of logic belongs.
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.| Stage | Runs | Can it stop the request? | Use it for |
|---|---|---|---|
| 1 | Middleware | Yes, by not calling next() | Request ids, raw body handling, CORS |
| 2 | Guards | Yes, by returning false or throwing | Authentication and roles |
| 3 | Interceptors, before | Yes, by not calling handle() | Timing, caching lookups |
| 4 | Pipes | Yes, by throwing | Validation and conversion |
| 5 | Handler | Yes, by throwing | The route's actual work |
| 6 | Interceptors, after | They can replace the result | Envelopes, mapping, timeouts |
| 7 | Exception filters | They write the error response | One error shape for the API |
Configuration
Load environment variables once, validate them at startup, and inject typed slices of config where you need them.
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.
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),
});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.
~/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
Database with TypeORM
Register a connection from config, declare entities, inject repositories, and group writes in transactions.
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.
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;
}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 {}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.
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.
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.
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}`);
}~/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
| Hook | Runs |
|---|---|
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 |
OpenAPI docs with Swagger
Generate an OpenAPI document from your controllers and DTOs, then serve interactive docs and the raw 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.
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;
}~/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" ]
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.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.
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);
});
});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']);
});
});~/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
Build and ship
The compiler settings Nest needs, the scripts that build and run it, and a short checklist before the first deploy.
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.
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"rootDir": ".",
"outDir": "dist",
"sourceMap": true
},
"include": ["src", "test"]
}~/tasks-api $ npm run build # tsc writes dist/ ~/tasks-api $ NODE_ENV=production node dist/src/main.js # run the compiled app
| Check | Why |
|---|---|
| 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: false | Schema changes go through migrations |
| Config validated at startup | A bad deploy fails fast, before taking traffic |
| One error filter | No stack traces or internals in responses |
helmet and rate limiting | Common headers and abuse protection; see @nestjs/throttler |
nest update | Applies the v11 to v12 migration steps |
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 to | Reach for | Example | Module |
|---|---|---|---|
| Group related features | A module | @Module({ providers, controllers }) | 03 |
| Share a service with another module | exports, then imports | exports: [TasksService] | 03 |
| Map a URL to code | A controller method | @Get(':id') | 04 |
| Hold business logic | A provider | @Injectable() class TasksService | 05 |
| Inject a value or interface | A token | @Inject(CLOCK) | 05 |
| Validate a body | DTO and ValidationPipe | @IsString() title!: string | 06 |
| Validate with Zod | Standard Schema option | @Query({ schema, pipes }) | 06 |
| Convert one argument | A pipe | @Param('id', ParseIntPipe) | 07 |
| Shape every error | An exception filter | @Catch() | 08 |
| Require a login | A guard | APP_GUARD, AuthGuard | 09 |
| Allow only some roles | Metadata plus a guard | @Roles([Role.Admin]) | 09 |
| Wrap or time responses | An interceptor | next.handle().pipe(map()) | 10 |
| Touch the raw request first | Middleware | consumer.apply(...).forRoutes() | 11 |
| Read the current user | A param decorator | createParamDecorator | 12 |
| Load and check env vars | ConfigModule | validationSchema: envSchema | 14 |
| Talk to Postgres | TypeORM repository | @InjectRepository(TaskEntity) | 15 |
| Run code on start or stop | Lifecycle hooks | onApplicationShutdown() | 16 |
| Publish API docs | Swagger | SwaggerModule.setup('docs') | 17 |
| Fake a dependency in tests | overrideProvider | .overrideProvider(CLOCK).useValue() | 18 |