Mastering NestJS: A Comprehensive Guide from Fundamentals to Core Architecture
Unlock the full potential of server-side applications with NestJS. This extensive guide takes you from the basics of Modules and DTOs to intermediate architecture concepts including Execution Contexts, Custom Decorators, and Circular Dependency resolution.
Fundamentals
Node.js and TypeScript Prerequisites
NestJS requires Node.js (v16+) and leverages TypeScript's static typing, decorators, and OOP features extensively. You need solid understanding of async/await, ES6+ features, interfaces, generics, and decorator syntax before diving into NestJS.
// Essential TypeScript concepts for NestJS interface User { id: number; name: string; } async function fetchUser(id: number): Promise<User> { return { id, name: 'John' }; } // Decorator syntax (fundamental to NestJS) function Log(target: any, key: string, descriptor: PropertyDescriptor) { console.log(`Method ${key} called`); }
NestJS CLI Installation and Usage
The NestJS CLI is a powerful command-line tool that scaffolds projects, generates components, and manages the build process, significantly accelerating development workflow.
# Installation npm install -g @nestjs/cli # Common commands nest new project-name # Create new project nest generate module users # Generate module (shorthand: nest g mo users) nest g controller users # Generate controller nest g service users # Generate service nest g resource users # Generate full CRUD resource nest build # Compile the application nest start --watch # Run with hot-reload
Project Structure and Architecture
NestJS follows a modular, layered architecture inspired by Angular, organizing code into cohesive modules where each feature lives in its own directory with controllers handling HTTP, services containing business logic, and modules wiring everything together.
src/
├── app.module.ts # Root module
├── app.controller.ts # Root controller
├── app.service.ts # Root service
├── main.ts # Entry point (bootstrap)
└── users/
├── users.module.ts # Feature module
├── users.controller.ts
├── users.service.ts
├── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
└── entities/
└── user.entity.ts
┌─────────────────────────────────────────────────┐
│ Request │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Middleware │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Guards │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Interceptors (Pre) │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Pipes │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Controller │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Service │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Interceptors (Post) │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Exception Filters │
└─────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Response │
└─────────────────────────────────────────────────┘
Modules
Modules are the fundamental organizational unit in NestJS, encapsulating related components (controllers, providers) and defining clear boundaries between features. Every NestJS app has at least one root module (AppModule).
import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { AuthModule } from '../auth/auth.module'; @Module({ imports: [AuthModule], // Import other modules controllers: [UsersController], // Register controllers providers: [UsersService], // Register services/providers exports: [UsersService], // Make available to other modules }) export class UsersModule {} /* Module Relationship Diagram: ┌──────────────────────────────────────────────────────┐ │ AppModule │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ UsersModule │───▶│ AuthModule │ │ │ │ - Controller │ │ - Controller │ │ │ │ - Service │ │ - Service │ │ │ └─────────────────┘ └─────────────────┘ │ └──────────────────────────────────────────────────────┘ */
Controllers
Controllers handle incoming HTTP requests, delegate business logic to services, and return responses to clients. They're decorated with @Controller() and use method decorators like @Get(), @Post() to define route handlers.
import { Controller, Get, Post, Body, Param, HttpCode, Header } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; @Controller('users') // Route prefix: /users export class UsersController { constructor(private readonly usersService: UsersService) {} @Get() // GET /users findAll() { return this.usersService.findAll(); } @Get(':id') // GET /users/:id findOne(@Param('id') id: string) { return this.usersService.findOne(+id); } @Post() // POST /users @HttpCode(201) @Header('Cache-Control', 'none') create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } }
Providers and Services
Providers are classes annotated with @Injectable() that can be injected as dependencies; services are the most common type, encapsulating business logic and data access separate from controllers.
import { Injectable, NotFoundException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { User } from './entities/user.entity'; @Injectable() export class UsersService { private users: User[] = []; findAll(): User[] { return this.users; } findOne(id: number): User { const user = this.users.find(u => u.id === id); if (!user) { throw new NotFoundException(`User #${id} not found`); } return user; } create(createUserDto: CreateUserDto): User { const user = { id: Date.now(), ...createUserDto, }; this.users.push(user); return user; } }
Dependency Injection Basics
NestJS uses a powerful IoC (Inversion of Control) container that automatically resolves and injects dependencies declared in constructors, promoting loose coupling and testability.
// The DI container automatically resolves dependencies @Injectable() export class UsersService { constructor( private readonly databaseService: DatabaseService, // Auto-injected private readonly loggerService: LoggerService, // Auto-injected ) {} } @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} // Auto-injected } /* DI Container Resolution: ┌─────────────────────────────────────────────────────┐ │ NestJS IoC Container │ ├─────────────────────────────────────────────────────┤ │ Registered Providers: │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ DatabaseService │ │ LoggerService │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ └──────┬─────────────┘ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ UsersService │ │ │ └────────┬────────┘ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ UsersController │ │ │ └─────────────────┘ │ └─────────────────────────────────────────────────────┘ */
Decorators Overview
Decorators are special TypeScript declarations that attach metadata to classes, methods, properties, or parameters, enabling NestJS to understand how components should behave and relate to each other.
// Class Decorators @Controller('users') // Marks class as controller @Injectable() // Marks class as provider @Module({}) // Marks class as module // Method Decorators @Get(':id') // HTTP GET handler @Post() // HTTP POST handler @Put(':id') // HTTP PUT handler @Delete(':id') // HTTP DELETE handler @UseGuards(AuthGuard) // Apply guard @UsePipes(ValidationPipe) // Apply pipe // Parameter Decorators @Param('id') // Route parameter @Query('page') // Query string parameter @Body() // Request body @Headers('authorization') // Request header @Req() // Express request object @Res() // Express response object // Property Decorators (often used with class-validator) class CreateUserDto { @IsString() @MinLength(3) name: string; @IsEmail() email: string; }
Request/Response Handling
NestJS provides decorators to access request data and can handle responses automatically (by returning values) or manually (using the @Res() decorator), with built-in support for JSON serialization.
import { Controller, Get, Post, Req, Res, Body, HttpStatus, Header, Redirect } from '@nestjs/common'; import { Request, Response } from 'express'; @Controller('users') export class UsersController { // Automatic response handling (recommended) @Get() findAll() { return { users: [] }; // Automatically serialized to JSON } // Manual response handling @Get('manual') findAllManual(@Res() res: Response) { res.status(HttpStatus.OK).json({ users: [] }); } // Access full request object @Get('info') getInfo(@Req() req: Request) { return { method: req.method, url: req.url, headers: req.headers, }; } // Redirect @Get('docs') @Redirect('https://docs.nestjs.com', 301) getDocs() {} // Dynamic redirect @Get('redirect') dynamicRedirect() { return { url: 'https://google.com', statusCode: 302 }; } }
Route Parameters and Query Strings
NestJS provides @Param() and @Query() decorators to extract dynamic route segments and query string parameters, with optional type transformation through pipes.
import { Controller, Get, Param, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common'; @Controller('users') export class UsersController { // Route parameters: GET /users/123 @Get(':id') findOne(@Param('id') id: string) { return `User ID: ${id}`; } // Multiple route params: GET /users/123/posts/456 @Get(':userId/posts/:postId') findUserPost( @Param('userId') userId: string, @Param('postId') postId: string, ) { return { userId, postId }; } // With transformation pipe: GET /users/123 @Get(':id') findOneTyped(@Param('id', ParseIntPipe) id: number) { return this.usersService.findOne(id); // id is now a number } // Query strings: GET /users?page=1&limit=10&sort=name @Get() findAll( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, @Query('sort') sort?: string, ) { return { page, limit, sort }; } // Get all params/query at once @Get('all/:id') getAllParams(@Param() params: any, @Query() query: any) { return { params, query }; } }
DTOs (Data Transfer Objects)
DTOs define the shape of data for input validation and documentation, typically implemented as TypeScript classes with validation decorators from class-validator to ensure data integrity at runtime.
// dto/create-user.dto.ts import { IsString, IsEmail, IsOptional, MinLength, IsEnum, ValidateNested, IsArray } from 'class-validator'; import { Type } from 'class-transformer'; export enum UserRole { ADMIN = 'admin', USER = 'user', } export class AddressDto { @IsString() street: string; @IsString() city: string; } export class CreateUserDto { @IsString() @MinLength(2) name: string; @IsEmail() email: string; @IsString() @MinLength(8) password: string; @IsEnum(UserRole) @IsOptional() role?: UserRole = UserRole.USER; @ValidateNested() @Type(() => AddressDto) @IsOptional() address?: AddressDto; } // dto/update-user.dto.ts import { PartialType, OmitType, PickType } from '@nestjs/mapped-types'; // All properties optional export class UpdateUserDto extends PartialType(CreateUserDto) {} // Exclude specific fields export class UpdateProfileDto extends OmitType(CreateUserDto, ['password']) {} // Pick specific fields only export class UpdatePasswordDto extends PickType(CreateUserDto, ['password']) {}
// Enable global validation in main.ts import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip non-whitelisted properties forbidNonWhitelisted: true, // Throw error for unknown properties transform: true, // Auto-transform payloads to DTO instances })); await app.listen(3000); }
Basic CRUD Operations
A complete CRUD implementation in NestJS combines controllers for routing, services for business logic, and DTOs for validation, following RESTful conventions for resource management.
// users.controller.ts import { Controller, Get, Post, Put, Patch, Delete, Body, Param, ParseIntPipe, HttpCode, HttpStatus } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Post() @HttpCode(HttpStatus.CREATED) create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } @Get() findAll() { return this.usersService.findAll(); } @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.usersService.findOne(id); } @Patch(':id') update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ) { return this.usersService.update(id, updateUserDto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) remove(@Param('id', ParseIntPipe) id: number) { return this.usersService.remove(id); } }
// users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; @Injectable() export class UsersService { private users: User[] = []; private idCounter = 1; create(createUserDto: CreateUserDto): User { const user: User = { id: this.idCounter++, ...createUserDto, createdAt: new Date(), }; this.users.push(user); return user; } findAll(): User[] { return this.users; } findOne(id: number): User { const user = this.users.find(u => u.id === id); if (!user) { throw new NotFoundException(`User #${id} not found`); } return user; } update(id: number, updateUserDto: UpdateUserDto): User { const userIndex = this.users.findIndex(u => u.id === id); if (userIndex === -1) { throw new NotFoundException(`User #${id} not found`); } this.users[userIndex] = { ...this.users[userIndex], ...updateUserDto }; return this.users[userIndex]; } remove(id: number): void { const userIndex = this.users.findIndex(u => u.id === id); if (userIndex === -1) { throw new NotFoundException(`User #${id} not found`); } this.users.splice(userIndex, 1); } }
Core Concepts
Custom Providers
Custom providers give you fine-grained control over dependency injection, allowing you to provide values, factories, or alternate implementations using tokens (strings, symbols, or classes) for maximum flexibility.
import { Module, Inject } from '@nestjs/common'; // 1. Value Provider - provide a constant value const CONFIG = { apiKey: 'secret-key', timeout: 5000, }; // 2. Class Provider - use a different class class MockUsersService { findAll() { return []; } } // 3. Factory Provider - dynamic creation with dependencies const databaseProviders = { provide: 'DATABASE_CONNECTION', useFactory: async (configService: ConfigService) => { const config = configService.get('database'); return createConnection(config); }, inject: [ConfigService], // Dependencies for factory }; // 4. Existing Provider - alias for another provider const aliasProvider = { provide: 'AliasedService', useExisting: UsersService, }; @Module({ providers: [ // Standard provider (shorthand) UsersService, // Value provider { provide: 'CONFIG', useValue: CONFIG }, // Class provider { provide: UsersService, useClass: MockUsersService }, // Factory provider databaseProviders, // Existing provider aliasProvider, ], }) export class AppModule {} // Using custom providers @Injectable() export class SomeService { constructor( @Inject('CONFIG') private config: typeof CONFIG, @Inject('DATABASE_CONNECTION') private db: Connection, ) {} }
Provider Scopes (DEFAULT, REQUEST, TRANSIENT)
Provider scopes determine the lifecycle of instances: DEFAULT creates a singleton shared across the app, REQUEST creates a new instance per HTTP request, and TRANSIENT creates a new instance each time it's injected.
import { Injectable, Scope, Inject } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; // DEFAULT (Singleton) - one instance for entire app lifetime @Injectable() // Same as @Injectable({ scope: Scope.DEFAULT }) export class SingletonService { private counter = 0; increment() { return ++this.counter; } // Shared across all requests } // REQUEST - new instance per incoming request @Injectable({ scope: Scope.REQUEST }) export class RequestScopedService { constructor(@Inject(REQUEST) private request: Request) {} getUser() { return this.request.user; // Access request-specific data } } // TRANSIENT - new instance each time it's injected @Injectable({ scope: Scope.TRANSIENT }) export class TransientService { private id = Math.random(); // Different for each injection getId() { return this.id; } } /* Scope Bubble Effect: ┌────────────────────────────────────────────────┐ │ Provider Scope Inheritance │ ├────────────────────────────────────────────────┤ │ │ │ If a SINGLETON depends on a REQUEST-scoped │ │ provider, the singleton becomes REQUEST │ │ scoped as well (scope bubbles up). │ │ │ │ Controller ──▶ Service A (REQUEST) ──▶ │ │ Service B (DEFAULT) │ │ ↓ │ │ Both become REQUEST-scoped! │ │ │ └────────────────────────────────────────────────┘ */
Middleware
Middleware functions execute before route handlers, having access to request/response objects and the next() function, ideal for logging, authentication, request transformation, or any cross-cutting concerns.
import { Injectable, NestMiddleware, Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; // Class-based middleware @Injectable() export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const start = Date.now(); console.log(`[${req.method}] ${req.url} - Start`); res.on('finish', () => { const duration = Date.now() - start; console.log(`[${req.method}] ${req.url} - ${res.statusCode} - ${duration}ms`); }); next(); } } // Functional middleware export function corsMiddleware(req: Request, res: Response, next: NextFunction) { res.header('Access-Control-Allow-Origin', '*'); next(); } // Apply middleware in module @Module({}) export class AppModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware, corsMiddleware) .exclude( { path: 'health', method: RequestMethod.GET }, // Exclude routes ) .forRoutes( { path: 'users', method: RequestMethod.ALL }, // Specific route { path: 'posts/*', method: RequestMethod.GET }, // Wildcard UsersController, // Entire controller ); } } /* Middleware Execution Order: Request │ ▼ ┌─────────────┐ │ Middleware 1│ ──▶ next() ──┐ └─────────────┘ │ ┌──────────────────────┘ ▼ ┌─────────────┐ │ Middleware 2│ ──▶ next() ──┐ └─────────────┘ │ ┌──────────────────────┘ ▼ ┌─────────────┐ │ Guards │ └─────────────┘ │ ▼ Handler */
Exception Filters
Exception filters catch unhandled exceptions and transform them into user-friendly HTTP responses, allowing centralized error handling with custom formatting, logging, and status codes.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, BadRequestException, NotFoundException, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catch specific exception @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); const status = exception.getStatus(); const exceptionResponse = exception.getResponse(); response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, method: request.method, message: typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message, }); } } // Catch ALL exceptions (including non-HTTP) @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof Error ? exception.message : 'Internal server error'; // Log the error console.error('Exception:', exception); response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message, }); } } // Apply filters @Controller('users') @UseFilters(HttpExceptionFilter) // Controller-level export class UsersController { @Get(':id') @UseFilters(new HttpExceptionFilter()) // Method-level findOne(@Param('id') id: string) { throw new NotFoundException('User not found'); } } // Global filter in main.ts app.useGlobalFilters(new AllExceptionsFilter());
Pipes (Built-in and Custom)
Pipes transform input data or validate it before reaching route handlers; NestJS provides built-in pipes like ValidationPipe and ParseIntPipe, and you can create custom pipes for specialized transformations.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, ParseIntPipe, ValidationPipe, DefaultValuePipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe, ParseEnumPipe } from '@nestjs/common'; // Built-in pipes usage @Controller('users') export class UsersController { @Get(':id') findOne( @Param('id', ParseIntPipe) id: number, // Transforms and validates @Query('active', new DefaultValuePipe(true), ParseBoolPipe) active: boolean, @Query('uuid', ParseUUIDPipe) uuid: string, ) { return { id, active, uuid }; } @Get() findAll( @Query('ids', new ParseArrayPipe({ items: Number })) ids: number[], ) { return { ids }; } } // Custom transformation pipe @Injectable() export class ParseDatePipe implements PipeTransform<string, Date> { transform(value: string, metadata: ArgumentMetadata): Date { const date = new Date(value); if (isNaN(date.getTime())) { throw new BadRequestException('Invalid date format'); } return date; } } // Custom validation pipe @Injectable() export class JoiValidationPipe implements PipeTransform { constructor(private schema: Joi.Schema) {} transform(value: any, metadata: ArgumentMetadata) { const { error, value: validatedValue } = this.schema.validate(value); if (error) { throw new BadRequestException(`Validation failed: ${error.message}`); } return validatedValue; } } // Usage @Post() @UsePipes(new JoiValidationPipe(createUserSchema)) create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } // Global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, transformOptions: { enableImplicitConversion: true }, }));
Guards
Guards determine whether a request should be handled by the route handler based on runtime conditions (like authentication or authorization), returning true to allow or false/throwing to deny access.
import { Injectable, CanActivate, ExecutionContext, SetMetadata, UseGuards, UnauthorizedException, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Observable } from 'rxjs'; // Simple auth guard @Injectable() export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const request = context.switchToHttp().getRequest(); const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) { throw new UnauthorizedException('No token provided'); } // Validate token and attach user to request try { request.user = this.validateToken(token); return true; } catch { throw new UnauthorizedException('Invalid token'); } } private validateToken(token: string) { // JWT verification logic return { id: 1, role: 'admin' }; } } // Role-based guard with metadata export const Roles = (...roles: string[]) => SetMetadata('roles', roles); @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; // No roles required } const { user } = context.switchToHttp().getRequest(); const hasRole = requiredRoles.some(role => user.roles?.includes(role)); if (!hasRole) { throw new ForbiddenException('Insufficient permissions'); } return true; } } // Apply guards @Controller('admin') @UseGuards(AuthGuard, RolesGuard) // Controller-level @Roles('admin') export class AdminController { @Get('dashboard') @Roles('admin', 'manager') // Method-level override getDashboard() { return { message: 'Admin dashboard' }; } } // Global guard app.useGlobalGuards(new AuthGuard());
Interceptors
Interceptors bind extra logic before/after method execution, enabling response transformation, logging, caching, exception mapping, and even completely overriding the returned observable using RxJS operators.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UseInterceptors, SetMetadata } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap, map, timeout, catchError } from 'rxjs/operators'; // Logging interceptor @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const method = request.method; const url = request.url; const now = Date.now(); console.log(`Before... ${method} ${url}`); return next.handle().pipe( tap(() => console.log(`After... ${Date.now() - now}ms`)), ); } } // Transform response interceptor @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> { return next.handle().pipe( map(data => ({ success: true, data, timestamp: new Date().toISOString(), })), ); } } // Cache interceptor @Injectable() export class CacheInterceptor implements NestInterceptor { private cache = new Map<string, any>(); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const key = request.url; if (this.cache.has(key)) { console.log('Cache hit'); return of(this.cache.get(key)); } return next.handle().pipe( tap(response => this.cache.set(key, response)), ); } } // Timeout interceptor @Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe(timeout(5000)); // 5 second timeout } } // Apply interceptors @Controller('users') @UseInterceptors(LoggingInterceptor, TransformInterceptor) export class UsersController {} // Global interceptor app.useGlobalInterceptors(new TransformInterceptor()); /* Interceptor Execution Flow: Request ──▶ Interceptor (before) ──▶ Handler ──▶ Interceptor (after) ──▶ Response ┌─────────────────────────────────────────────────────────────────┐ │ Interceptor Pipeline │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ next.handle() returns Observable: │ │ │ │ Request │ │ │ │ │ ▼ │ │ ┌─────────────┐ ┌──────────┐ ┌─────────────┐ │ │ │ Interceptor │──▶│ Handler │──▶│ Interceptor │──▶ Response │ │ │ (before) │ │ │ │ (after) │ │ │ └─────────────┘ └──────────┘ └─────────────┘ │ │ │ ▲ │ │ └──────── RxJS pipe operators ─────┘ │ │ (map, tap, catchError) │ └─────────────────────────────────────────────────────────────────┘ */
Custom Decorators
Custom decorators encapsulate reusable logic for extracting request data, composing multiple decorators, or adding metadata, making controllers cleaner and more declarative.
import { createParamDecorator, ExecutionContext, SetMetadata, applyDecorators, UseGuards, UseInterceptors } from '@nestjs/common'; // Parameter decorator - extract user from request export const CurrentUser = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; return data ? user?.[data] : user; // Return specific property or whole user }, ); // Usage @Get('profile') getProfile(@CurrentUser() user: User) { return user; } @Get('profile') getUserId(@CurrentUser('id') userId: number) { return { userId }; } // Metadata decorator export const Public = () => SetMetadata('isPublic', true); // Composed decorator - combine multiple decorators export function Auth(...roles: Role[]) { return applyDecorators( SetMetadata('roles', roles), UseGuards(AuthGuard, RolesGuard), UseInterceptors(LoggingInterceptor), ApiBearerAuth(), // Swagger decorator ); } // Usage @Get('admin') @Auth(Role.Admin) // Single decorator applies many getAdminData() { return { secret: 'data' }; } // Custom validation decorator export const IsValidPassword = (validationOptions?: ValidationOptions) => { return (object: Object, propertyName: string) => { registerDecorator({ name: 'isValidPassword', target: object.constructor, propertyName: propertyName, options: validationOptions, validator: { validate(value: any) { return typeof value === 'string' && value.length >= 8 && /[A-Z]/.test(value) && /[0-9]/.test(value); }, defaultMessage() { return 'Password must be 8+ chars with uppercase and number'; }, }, }); }; };
Execution Context
ExecutionContext extends ArgumentsHost, providing additional details about the current execution process including the controller class, handler method, and type (HTTP, RPC, WebSocket), enabling context-aware guards and interceptors.
import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; @Injectable() export class AdvancedGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { // Get handler (method) and class information const handler = context.getHandler(); // Method reference const controller = context.getClass(); // Controller class console.log(`Controller: ${controller.name}`); console.log(`Handler: ${handler.name}`); // Get context type const type = context.getType(); // 'http' | 'ws' | 'rpc' // Switch based on context type if (type === 'http') { const httpCtx = context.switchToHttp(); const request = httpCtx.getRequest(); const response = httpCtx.getResponse(); const next = httpCtx.getNext(); return true; } else if (type === 'ws') { const wsCtx = context.switchToWs(); const client = wsCtx.getClient(); const data = wsCtx.getData(); return true; } else if (type === 'rpc') { const rpcCtx = context.switchToRpc(); const data = rpcCtx.getData(); const context = rpcCtx.getContext(); return true; } // Get metadata using reflector const roles = this.reflector.get<string[]>('roles', handler); const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [ handler, controller, ]); // Get all arguments const [req, res, next] = context.getArgs(); return true; } } /* ExecutionContext Hierarchy: ┌──────────────────────────────────────────────────────┐ │ ExecutionContext │ ├──────────────────────────────────────────────────────┤ │ extends ArgumentsHost │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ArgumentsHost methods: │ │ │ │ - getArgs() │ │ │ │ - getArgByIndex(index) │ │ │ │ - switchToHttp() / switchToWs() / switchToRpc()│ │ │ │ - getType() │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ Additional ExecutionContext methods: │ │ - getClass(): Type<any> // Controller class │ │ - getHandler(): Function // Route handler method │ └──────────────────────────────────────────────────────┘ */
Lifecycle Hooks
NestJS provides lifecycle hooks that let you tap into initialization and shutdown events at module and application levels, useful for database connections, cleanup tasks, and graceful shutdowns.
import { Injectable, OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, OnApplicationShutdown, BeforeApplicationShutdown } from '@nestjs/common'; @Injectable() export class DatabaseService implements OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, BeforeApplicationShutdown, OnApplicationShutdown { private connection: any; // Called once the module's dependencies are resolved async onModuleInit() { console.log('1. Module initialized - connecting to database...'); this.connection = await this.connect(); } // Called after all modules are initialized and app is ready async onApplicationBootstrap() { console.log('2. Application bootstrapped - running migrations...'); await this.runMigrations(); } // Called when shutdown signal received (before connections closed) async beforeApplicationShutdown(signal?: string) { console.log(`3. Before shutdown (signal: ${signal}) - completing transactions...`); await this.completeTransactions(); } // Called when module is being destroyed async onModuleDestroy() { console.log('4. Module destroying - cleaning up...'); } // Called right before app exit async onApplicationShutdown(signal?: string) { console.log(`5. Application shutdown (signal: ${signal}) - closing connection...`); await this.disconnect(); } private async connect() { /* ... */ } private async disconnect() { /* ... */ } private async runMigrations() { /* ... */ } private async completeTransactions() { /* ... */ } } // Enable shutdown hooks in main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); // Enable graceful shutdown app.enableShutdownHooks(); await app.listen(3000); } /* Lifecycle Execution Order: ┌─────────────────────────────────────────────────────────────┐ │ Application Lifecycle │ ├─────────────────────────────────────────────────────────────┤ │ │ │ STARTUP: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 1. onModuleInit() - Each provider in module │ │ │ │ 2. onApplicationBootstrap() - After all modules init│ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ SHUTDOWN (when signal received): │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 3. beforeApplicationShutdown() - Cleanup starts │ │ │ │ 4. onModuleDestroy() - Module cleanup │ │ │ │ 5. onApplicationShutdown() - Final cleanup │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ */
Dynamic Modules
Dynamic modules allow runtime configuration, enabling you to create configurable, reusable modules that accept options when imported, perfect for database connections, third-party integrations, and feature flags.
import { Module, DynamicModule, Global, Provider } from '@nestjs/common'; // Options interface export interface DatabaseModuleOptions { host: string; port: number; username: string; password: string; database: string; } export interface DatabaseModuleAsyncOptions { useFactory: (...args: any[]) => Promise<DatabaseModuleOptions> | DatabaseModuleOptions; inject?: any[]; imports?: any[]; } @Global() // Make available everywhere without importing @Module({}) export class DatabaseModule { // Synchronous configuration static forRoot(options: DatabaseModuleOptions): DynamicModule { const providers: Provider[] = [ { provide: 'DATABASE_OPTIONS', useValue: options, }, { provide: 'DATABASE_CONNECTION', useFactory: async (opts: DatabaseModuleOptions) => { return await createConnection(opts); }, inject: ['DATABASE_OPTIONS'], }, DatabaseService, ]; return { module: DatabaseModule, providers, exports: ['DATABASE_CONNECTION', DatabaseService], }; } // Async configuration (for ConfigService, etc.) static forRootAsync(options: DatabaseModuleAsyncOptions): DynamicModule { const providers: Provider[] = [ { provide: 'DATABASE_OPTIONS', useFactory: options.useFactory, inject: options.inject || [], }, { provide: 'DATABASE_CONNECTION', useFactory: async (opts: DatabaseModuleOptions) => { return await createConnection(opts); }, inject: ['DATABASE_OPTIONS'], }, DatabaseService, ]; return { module: DatabaseModule, imports: options.imports || [], providers, exports: ['DATABASE_CONNECTION', DatabaseService], }; } } // Usage - synchronous @Module({ imports: [ DatabaseModule.forRoot({ host: 'localhost', port: 5432, username: 'admin', password: 'secret', database: 'mydb', }), ], }) export class AppModule {} // Usage - async with ConfigService @Module({ imports: [ DatabaseModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ host: configService.get('DB_HOST'), port: configService.get('DB_PORT'), username: configService.get('DB_USER'), password: configService.get('DB_PASS'), database: configService.get('DB_NAME'), }), inject: [ConfigService], }), ], }) export class AppModule {}
Lazy Loading Modules
Lazy loading defers module initialization until first use, reducing initial startup time and memory footprint in large applications or serverless environments where cold start matters.
import { Module, Controller, Get } from '@nestjs/common'; import { LazyModuleLoader } from '@nestjs/core'; // Heavy module that we want to lazy load @Module({ providers: [ReportService], exports: [ReportService], }) export class ReportModule {} @Injectable() export class ReportService { generateReport() { // Heavy computation return { data: 'complex report' }; } } // Controller using lazy loading @Controller('reports') export class ReportsController { constructor(private lazyModuleLoader: LazyModuleLoader) {} @Get() async getReport() { // Module loaded only when this endpoint is hit const moduleRef = await this.lazyModuleLoader.load(() => ReportModule); const reportService = moduleRef.get(ReportService); return reportService.generateReport(); } } // Alternative: Lazy load with dynamic imports @Controller('analytics') export class AnalyticsController { constructor(private lazyModuleLoader: LazyModuleLoader) {} @Get() async getAnalytics() { // Dynamic import for code splitting const { AnalyticsModule } = await import('./analytics/analytics.module'); const moduleRef = await this.lazyModuleLoader.load(() => AnalyticsModule); const analyticsService = moduleRef.get(AnalyticsService); return analyticsService.analyze(); } } /* Lazy Loading Benefits: ┌────────────────────────────────────────────────────────┐ │ Normal Loading │ ├────────────────────────────────────────────────────────┤ │ Startup: Load ALL modules │ │ │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ Mod │ │ Mod │ │ Mod │ │ Mod │ │ Mod │ = Slow │ │ │ A │ │ B │ │ C │ │ D │ │ E │ Start │ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │ └────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────┐ │ Lazy Loading │ ├────────────────────────────────────────────────────────┤ │ Startup: Load only CORE modules │ │ │ │ ┌─────┐ ┌─────┐ │ │ │Core │ │Core │ = Fast Start │ │ │ Mod │ │ Mod │ │ │ └─────┘ └─────┘ │ │ │ │ On-demand: Load when needed │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │Lazy │ │Lazy │ │Lazy │ = Loaded per request │ │ │ Mod │ │ Mod │ │ Mod │ │ │ └─────┘ └─────┘ └─────┘ │ └────────────────────────────────────────────────────────┘ */
Circular Dependency Resolution
Circular dependencies occur when two classes depend on each other; NestJS provides forwardRef() to resolve these by deferring reference resolution until both classes are defined.
import { Injectable, Module, forwardRef, Inject } from '@nestjs/common'; // Problem: UsersService needs AuthService, AuthService needs UsersService // users.service.ts @Injectable() export class UsersService { constructor( @Inject(forwardRef(() => AuthService)) private authService: AuthService, ) {} findOne(id: number) { return { id, name: 'John' }; } async validateUser(token: string) { return this.authService.validateToken(token); } } // auth.service.ts @Injectable() export class AuthService { constructor( @Inject(forwardRef(() => UsersService)) private usersService: UsersService, ) {} validateToken(token: string) { return { valid: true }; } async getUser(id: number) { return this.usersService.findOne(id); } } // Also needed at module level @Module({ imports: [forwardRef(() => AuthModule)], providers: [UsersService], exports: [UsersService], }) export class UsersModule {} @Module({ imports: [forwardRef(() => UsersModule)], providers: [AuthService], exports: [AuthService], }) export class AuthModule {} /* Circular Dependency Visualization: Before forwardRef(): After forwardRef(): ┌─────────────┐ ┌────────────┐ ┌─────────────┐ ┌────────────┐ │UsersService │──▶ │AuthService │ │UsersService │──▶ │AuthService │ └─────────────┘ └────────────┘ └──────┬──────┘ └─────┬──────┘ ▲ │ │ │ │ │ │ forwardRef() │ └───────────────────┘ └────────◀────────┘ ❌ Error! ✓ Resolved! Best Practice: Avoid circular dependencies when possible! - Extract common logic to a third service - Use events/message patterns - Restructure module boundaries */