Node.js backend frameworks like Express and Fastify are highly flexible and unopinionated but do not enforce any architecture.
This lack of structure leads to disorganized, hard-to-maintain codebases in large-scale applications or when multiple developers work together.
NestJS solves this by providing a highly structured, opinionated architecture out of the box, ensuring consistency, scalability, and clean separation of concerns.
Core Architecture Roots:
Heavy inspiration from Angular architecture (Modules, Decorators, Dependency Injection).
Built with and fully supports TypeScript out of the box (also allows raw ES6/ES7 JS).
Uses Express under the hood as the default HTTP server, but abstracting it via an adapter interface.
Easily interchangeable with Fastify for high-performance applications.
Introduction & Architecture
NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications.
Relies on TypeScript Decorators (metadata reflection) and a robust Dependency Injection (DI) container to handle objects and class instantiation.
Follows a strictly Modular design, isolating components into self-contained units (Modules).
Easy Migration to Fastify — Swap HTTP engines with one line of code for massive speed boosts.
Disadvantages & Trade-offs:
Steep Learning Curve — Requires solid understanding of OOP, Dependency Injection, Decorators, RxJS, and TypeScript.
Boilerplate Overhead — Even small/simple endpoints require a controller, a service, a module, and possibly DTOs.
Performance Overhead — The dependency resolution, decorators, and request lifecycle pipelines add a tiny overhead compared to bare Express/Fastify.
Large Bundle Size — Heavy dependency graph increases initial build output and memory footprints.
Setup & CLI Reference
Installation & Project Initiation:
# Install NestJS CLI globallynpm install -g @nestjs/cli# Scaffold a new projectnest new project-name# Run commands inside project root:npm run start # Start standard development servernpm run start:dev # Start dev server with watch-mode (hot reload)npm run start:debug # Start dev server with debugger port opennpm run start:prod # Run compiled JS code from dist/ foldernpm run build # Compile TS code to JS (generates dist/)npm run test # Run unit tests via Jestnpm run test:cov # Run test coverage analysisnpm run test:e2e # Run End-to-End tests
CLI Code Generators:
Generators automatically scaffold files and register them inside their nearest parent module.
# Syntax: nest generate <schematic> <name> [options]# Shorthand: nest g <schematic> <name># Generate basic blocksnest g module users # Generates users.module.tsnest g controller users # Generates users.controller.ts + registers in UsersModulenest g service users # Generates users.service.ts + registers in UsersModule# Generate full-featured CRUD resource (Highly Recommended)# (Creates Module, Controller, Service, Entities, DTOs, and hooks up basic routes)nest g resource items# Generate architectural pipes and filtersnest g guard auth # src/auth/auth.guard.tsnest g interceptor logging # src/logging/logging.interceptor.tsnest g pipe validation # src/validation/validation.pipe.tsnest g filter http-exception # src/http-exception.filter.tsnest g middleware logger # src/logger.middleware.ts
Contains the bootstrapper logic that mounts middlewares, global filters, global pipes, configurations, and starts listening.
import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import { ValidationPipe, VersioningType } from '@nestjs/common';import { HttpExceptionFilter } from './common/filters/http-exception.filter';async function bootstrap() { // Create NestJS app instance utilizing Express under the hood const app = await NestFactory.create(AppModule); // 1. Set global routing prefix app.setGlobalPrefix('api'); // 2. Enable API Versioning (e.g., api/v1/resource) app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1', }); // 3. Enable CORS app.enableCors({ origin: 'http://localhost:4200', // Allow specific frontend URL methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, }); // 4. Configure global validation rules app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip any parameters NOT declared in the DTO forbidNonWhitelisted: true, // Throw a BadRequest error if extra fields are sent transform: true, // Auto-convert query parameters/route params to typed objects/numbers transformOptions: { enableImplicitConversion: true, // Allow automatic parsing of boolean/number types from string query parameters } })); // 5. Apply Global Exception Filter app.useGlobalFilters(new HttpExceptionFilter()); // 6. Start server await app.listen(3000); console.log(`Application successfully listening on: http://localhost:3000/api/v1`);}bootstrap();
Nest Modules (@Module):
Modules are class declarations decorated with @Module(). They compile metadata used by the Nest compiler to orchestrate dependency injection.
Each application has a Root Module (AppModule), which imports all sub-modules forming the module tree.
import { Module, Global } from '@nestjs/common';import { DatabaseService } from './database.service';// Use @Global() with caution! Makes this service immediately importable by all modules// without explicitly listing this module in imports.@Global()@Module({ imports: [], // Declare other imported modules containing needed exported providers controllers: [], // Instantiated controllers by this module providers: [DatabaseService], // Instantiated and injected providers inside this module boundary exports: [DatabaseService], // Exported providers made accessible to other importing modules})export class CoreDatabaseModule {}
Dynamic Modules:
Dynamic modules allow you to pass configuration options dynamically when registering a module (e.g., configuring database connections, API keys, endpoints).
import { Module, DynamicModule, Provider } from '@nestjs/common';import { MailerService } from './mailer.service';export interface MailerOptions { apiKey: string; fromEmail: string;}@Module({})export class MailerModule { // Define configuration method that returns a DynamicModule object static register(options: MailerOptions): DynamicModule { const mailerOptionsProvider: Provider = { provide: 'MAILER_OPTIONS', useValue: options, }; return { module: MailerModule, providers: [mailerOptionsProvider, MailerService], exports: [MailerService], // Export so importing modules can inject MailerService }; }}// Import into AppModule dynamically:// @Module({// imports: [MailerModule.register({ apiKey: 'secret', fromEmail: 'no-reply@app.com' })]// })
Controllers (@Controller):
Handles incoming client requests, delegates business logic execution to services, and returns payload response structures.
import { Controller, Get, Post, Put, Delete, Patch, Body, Param, Query, Headers, Ip, HttpCode, HttpStatus, UseGuards, Res, HttpStatus as Code} from '@nestjs/common';import { Response } from 'express';import { UsersService } from './users.service';import { CreateUserDto } from './dto/create-user.dto';import { UpdateUserDto } from './dto/update-user.dto';@Controller('users') // Base route: /usersexport class UsersController { // Dependency Injection via TypeScript parameter property syntax constructor(private readonly usersService: UsersService) {} // 1. Basic GET with Route Parameter + Pagination Query @Get() async findAll( @Query('limit') limit: number = 10, @Query('offset') offset: number = 0 ) { return this.usersService.findAll(limit, offset); } // 2. GET Route Parameter with typed path validation @Get(':id') async findOne(@Param('id') id: number) { return this.usersService.findOne(id); } // 3. POST request with custom response code and body validation (via DTO) @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } // 4. PATCH request with path params and custom body dto @Patch(':id') async update( @Param('id') id: number, @Body() updateUserDto: UpdateUserDto ) { return this.usersService.update(id, updateUserDto); } // 5. DELETE resource execution returning NO_CONTENT @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async delete(@Param('id') id: number) { await this.usersService.remove(id); } // 6. Advanced: Bypassing Nest response engine using direct platform response object // Warning: Disables automatic serialization, global interceptors execution on response payload @Get('direct-res') getDirectResponse(@Res() res: Response) { return res.status(200).send({ message: 'Express custom response' }); }}
Providers & Services (@Injectable):
Providers are JavaScript classes decorated with @Injectable(). They are managed by Nest’s IoC container and can be injected into other classes via constructors.
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';import { CreateUserDto } from './dto/create-user.dto';import { UpdateUserDto } from './dto/update-user.dto';@Injectable()export class UsersService { // In-memory data store for demonstration private users: any[] = []; async findAll(limit: number, offset: number) { return this.users.slice(offset, offset + limit); } async findOne(id: number) { const user = this.users.find(u => u.id === id); if (!user) { // Standard HTTP exceptions built into NestJS throw new NotFoundException(`User with ID ${id} not found`); } return user; } async create(dto: CreateUserDto) { const exists = this.users.some(u => u.email === dto.email); if (exists) { throw new ConflictException(`Email ${dto.email} already exists`); } const newUser = { id: Date.now(), ...dto }; this.users.push(newUser); return newUser; } async update(id: number, dto: UpdateUserDto) { const user = await this.findOne(id); const updatedUser = { ...user, ...dto }; this.users = this.users.map(u => (u.id === id ? updatedUser : u)); return updatedUser; } async remove(id: number) { await this.findOne(id); this.users = this.users.filter(u => u.id !== id); }}
Custom Providers & Dependency Injection (DI)
NestJS uses Inversion of Control (IoC) to automatically instantiate classes and resolve dependencies.
Advanced Provider Declarations:
Instead of simple array references, you can customize provider instantiations:
Providers can run inside different lifecycles by setting their @Injectable({ scope: Scope }):
import { Injectable, Scope } from '@nestjs/common';// 1. DEFAULT (Singleton) - Instantiated once. Shared across the entire system. (Highly recommended/default)@Injectable({ scope: Scope.DEFAULT })export class SingletonService {}// 2. REQUEST - Instantiated dynamically for EACH incoming request. Garbage collected after completion.// WARNING: Request scoping propagates up. Any service injecting a REQUEST-scoped service becomes REQUEST-scoped.// Can cause severe memory overhead under heavy load.@Injectable({ scope: Scope.REQUEST })export class RequestScopedService {}// 3. TRANSIENT - A dedicated instance is created for each class that injects this provider.@Injectable({ scope: Scope.TRANSIENT })export class TransientService {}
Circular Dependencies (forwardRef):
Occurs when Class A imports Class B, and Class B imports Class A. To resolve this, use forwardRef() to defer instantiation resolution.
// auth.service.ts@Injectable()export class AuthService { constructor( @Inject(forwardRef(() => UsersService)) private usersService: UsersService, ) {}}// users.service.ts@Injectable()export class UsersService { constructor( @Inject(forwardRef(() => AuthService)) private authService: AuthService, ) {}}// Corresponding Module definitions must also wrap imports with forwardRef:// @Module({ imports: [forwardRef(() => UsersModule)] })
Performs authentication and authorization tasks. Implements CanActivate and must return boolean | Promise<boolean> to allow or block route access.
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';import { Reflector } from '@nestjs/core';@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} async canActivate(context: ExecutionContext): Promise<boolean> { // Retrieve route roles metadata attached via custom decorators const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; // No roles defined, proceed } const request = context.switchToHttp().getRequest(); const user = request.user; // Appended by AuthGuard Strategy if (!user || !user.roles) { throw new UnauthorizedException('Access denied: Missing role profile'); } // Verify if user roles intersect with required route roles const hasRole = requiredRoles.some(role => user.roles.includes(role)); if (!hasRole) { throw new UnauthorizedException('Insufficient permissions'); } return true; }}
Interceptors (@UseInterceptors):
Wraps handlers using RxJS Observables to log events, handle response timeouts, cache payloads, or transform incoming/outgoing data.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';import { Observable } from 'rxjs';import { map, timeout } from 'rxjs/operators';export interface ApiResponse<T> { data: T; timestamp: string; success: boolean;}// Interceptor transforming raw return object into structured envelope@Injectable()export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> { return next.handle().pipe( // 1. Transform payload map(data => ({ data, timestamp: new Date().toISOString(), success: true })), // 2. Set timeout limit (e.g., throw error if handler takes > 5 seconds) timeout(5000) ); }}
Pipes (@UsePipes):
Translates and validates input structures. Emits 400 Bad Request if payload structure violates validation constraints.
Uses class-validator and class-transformer properties.
// custom-parse-int.pipe.tsimport { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';@Injectable()export class CustomParseIntPipe implements PipeTransform<string, number> { transform(value: string): number { const val = parseInt(value, 10); if (isNaN(val)) { throw new BadRequestException(`Validation failed: "${value}" is not a valid integer`); } return val; }}// In Controller:// @Get(':id')// findOne(@Param('id', CustomParseIntPipe) id: number) { ... }
Exception Filters
Customizes the JSON structure of error payloads returned to the client. Can target global server runtime crashes, Database schema error handling, and structured HTTP errors.
Uses decorators to validate incoming payloads before they reach the controller.
import { IsString, IsEmail, IsInt, Min, Max, IsOptional, ValidateNested, IsArray, IsEnum } from 'class-validator';import { Type } from 'class-transformer';export enum UserRole { ADMIN = 'admin', USER = 'user', MODERATOR = 'moderator',}export class ProfileDto { @IsString() bio: string; @IsString() avatarUrl: string;}export class CreateUserDto { @IsString() name: string; @IsEmail() email: string; @IsInt() @Min(18) @Max(100) age: number; @IsEnum(UserRole) role: UserRole; // Validating Nested Objects: @ValidateNested() @Type(() => ProfileDto) // Transform plain object to ProfileDto class instance profile: ProfileDto;}
Creating Update DTOs (Don’t Repeat Yourself):
Instead of recreating validation rules, use PartialType to copy fields from your creation DTO and mark them as optional:
import { PartialType } from '@nestjs/mapped-types';import { CreateUserDto } from './create-user.dto';// Inherits all fields of CreateUserDto and makes them optional for patch/update requestsexport class UpdateUserDto extends PartialType(CreateUserDto) {}
Configuration & Environment Variables
Standardizes how configurations are handled, preventing environment leakage and enabling type-safe configurations.
Installation:
npm install @nestjs/config joi
Setting up the ConfigModule:
Configure the global module config in your root imports:
import { Module } from '@nestjs/common';import { ConfigModule } from '@nestjs/config';import * as Joi from 'joi';@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Makes ConfigService available globally envFilePath: `.env.${process.env.NODE_ENV || 'development'}`, // 1. Enforce strict configuration parsing via Joi validation validationSchema: Joi.object({ PORT: Joi.number().default(3000), DB_HOST: Joi.string().required(), DB_URI: Joi.string().required(), JWT_SECRET: Joi.string().required(), }), validationOptions: { allowUnknown: true, // Allow other unvalidated environment parameters abortEarly: true, // Stop execution immediately on invalid configuration schema } }), ],})export class AppModule {}
Injecting and Consuming Environment Variables:
Inject ConfigService into components:
import { Injectable } from '@nestjs/common';import { ConfigService } from '@nestjs/config';@Injectable()export class AuthService { constructor(private configService: ConfigService) {} getJwtSecret(): string { // Typecasting returned string values return this.configService.get<string>('JWT_SECRET'); }}
Type-safe Config Namespaces (registerAs):
Organizes config settings into logical, type-safe namespaces instead of reading generic keys:
Initialize and hook Prisma client connection to NestJS application lifecycle events:
// prisma.service.tsimport { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';import { PrismaClient } from '@prisma/client';@Injectable()export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { async onModuleInit() { await this.$connect(); // Open DB connections } async onModuleDestroy() { await this.$disconnect(); // Close connection pool on app shutdown }}
Registering Service:
Declare a global database module to share PrismaService:
// database.module.tsimport { Module, Global } from '@nestjs/common';import { PrismaService } from './prisma.service';@Global()@Module({ providers: [PrismaService], exports: [PrismaService],})export class DatabaseModule {}
Running Transactions with Prisma:
import { Injectable } from '@nestjs/common';import { PrismaService } from '../database/prisma.service';@Injectable()export class ItemsService { constructor(private prisma: PrismaService) {} async checkoutItem(userId: number, itemId: number, price: number) { // Prisma transactional block ensures both operations complete or roll back return this.prisma.$transaction(async (tx) => { // 1. Deduct user balance const user = await tx.user.update({ where: { id: userId }, data: { balance: { decrement: price } } }); if (user.balance < 0) { throw new Error('Insufficient balance'); } // 2. Register purchase log const log = await tx.purchaseLog.create({ data: { userId, itemId, price } }); return log; }); }}
Security & Authentication
Standardizes authentication structures using Passport strategies.
By default, NestJS uses Express. However, you can swap it for Fastify to double or triple your throughput.
Installation:
npm install @nestjs/platform-fastify
Setup in main.ts:
Import the adapter and pass it to NestFactory.create():
import { NestFactory } from '@nestjs/core';import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';import { AppModule } from './app.module';async function bootstrap() { // Instantiates application running on top of Fastify platform const app = await NestFactory.create<NestFastifyApplication>( AppModule, new FastifyAdapter({ logger: false, // Turn off internal logger configs for benchmarking performance }) ); await app.listen(3000, '0.0.0.0'); // Bind to all interfaces (required inside dockerized containers) console.log(`Application running on Fastify port 3000`);}bootstrap();
2. In-Memory caching setup:
Prevent unnecessary database queries by caching responses in memory (e.g. using cache-manager with Redis).
import { CacheModule, Module } from '@nestjs/common';import * as redisStore from 'cache-manager-redis-store';@Module({ imports: [ CacheModule.register({ store: redisStore, host: 'localhost', port: 6379, ttl: 600, // Cache results for 10 minutes }), ],})export class AppModule {}
Key Takeaways
NestJS enforces an Angular-inspired modular architecture (Modules, Controllers, Services/Providers) on the Node.js backend.
Every feature should reside inside its own self-contained Module that explicitly defines imports, exports, and providers.
Use Dependency Injection (IoC) to inject services into constructors. Mark classes with @Injectable() to manage their lifecycle.
Always configure ValidationPipe globally with options like whitelist: true and transform: true for clean validation and input parsing.
Hook up your middlewares, guards, pipes, interceptors, and exception filters in alignment with the NestJS Request Lifecycle.
Resolve circular dependencies safely using forwardRef(() => classReference) in both strategies and modules.
Choose between TypeORM (using repository classes and transaction QueryRunners) or Prisma (injecting raw Client wrappers) depending on your persistence preferences.
Secure your REST interfaces using passport strategy models, and design custom parameters decorators like @CurrentUser() to clean up controller methods.
Scale your business application patterns using TCP microservices, WebSockets Gateways, or BullMQ background queues.