NestJS is a TypeScript-first Node.js framework with Angular-inspired architecture. Install with npm i -g @nestjs/cli && nest new my-app. Core concepts: Modules (feature boundaries), Controllers (HTTP handlers), Services (business logic), Providers (DI tokens). Protect routes with Guards, transform data with Pipes, log/cache with Interceptors. Integrate databases via TypeORM or Prisma. Test with Jest using Test.createTestingModule().
What Is NestJS?
NestJS is a progressive, TypeScript-first Node.js framework for building efficient, scalable server-side applications. Created by Kamil Mysliwiec in 2017, NestJS draws heavy inspiration from Angular โ bringing decorators, dependency injection, and a module-based architecture to the Node.js backend world.
Under the hood, NestJS is built on top of Express.js by default (with optional Fastify support), giving you access to the entire Express ecosystem while layering on powerful abstractions that make large applications maintainable.
As of 2026, NestJS has over 65,000 GitHub stars, 9+ million weekly npm downloads, and is used in production by companies like Adidas, Roche, Autodesk, and hundreds of enterprise engineering teams.
- NestJS is TypeScript-first and runs on Express or Fastify under the hood.
- Modules, Controllers, Services, and Providers form the four pillars of every NestJS app.
- Built-in dependency injection (IoC container) wires classes together automatically.
- Decorators like
@Controller(),@Get(),@Injectable()drive the framework. - Guards handle authorization, Pipes validate/transform data, Interceptors wrap the request lifecycle.
- Use TypeORM or Prisma for database access with first-party integration modules.
- JWT authentication is implemented via
@nestjs/passportand@nestjs/jwt. - Testing is built-in:
Test.createTestingModule()creates isolated test environments.
Installation and Project Setup
The NestJS CLI is the recommended way to scaffold a new project. It generates the initial file structure, sets up TypeScript configuration, installs all required dependencies, and even initializes a Git repository.
# Install the NestJS CLI globally
npm install -g @nestjs/cli
# Create a new project
nest new my-nestjs-app
# Choose your preferred package manager (npm, yarn, or pnpm)
# The CLI scaffolds the entire project for you
cd my-nestjs-app
# Start the development server with hot reload
npm run start:dev
# Application is running on: http://localhost:3000The generated project structure looks like this:
my-nestjs-app/
โโโ src/
โ โโโ app.controller.ts # Root controller
โ โโโ app.controller.spec.ts # Unit test for root controller
โ โโโ app.module.ts # Root module
โ โโโ app.service.ts # Root service
โ โโโ main.ts # Application entry point
โโโ test/
โ โโโ app.e2e-spec.ts # End-to-end test
โ โโโ jest-e2e.json # e2e Jest config
โโโ nest-cli.json # NestJS CLI config
โโโ tsconfig.json # TypeScript config
โโโ package.jsonThe entry point main.ts bootstraps the application:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable global validation pipe (recommended for all apps)
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip properties not in DTO
forbidNonWhitelisted: true, // Throw on extra properties
transform: true, // Auto-transform payloads to DTO types
}));
// Enable CORS for frontend apps
app.enableCors();
// Global prefix for all routes
app.setGlobalPrefix('api/v1');
await app.listen(3000);
console.log('Application is running on: http://localhost:3000');
}
bootstrap();Core Concepts: Modules and Architecture
NestJS applications are organized around modules. A module is a class annotated with the @Module() decorator. It provides metadata that NestJS uses to organize the application structure.
Every application has at least one module โ the root module (AppModule). Feature modules group related functionality: a Users module handles everything about users, a Products module handles products, and so on.
The @Module() Decorator
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([User]), // Import entity repositories
],
controllers: [UsersController], // Handle incoming HTTP requests
providers: [UsersService], // Business logic + DI providers
exports: [UsersService], // Make service available to other modules
})
export class UsersModule {}
// src/app.module.ts โ Root module
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: 5432,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
autoLoadEntities: true,
synchronize: true, // Disable in production!
}),
UsersModule,
AuthModule,
],
})
export class AppModule {}Generating Modules with the CLI
# Generate a complete module with controller, service, and spec files
nest generate module users
nest generate controller users
nest generate service users
# Or use the resource shorthand (generates CRUD boilerplate)
nest generate resource users
# ? What transport layer do you use? REST API
# ? Would you like to generate CRUD entry points? Yes
# Creates: users.module.ts, users.controller.ts, users.service.ts,
# entities/user.entity.ts, dto/create-user.dto.ts, dto/update-user.dto.tsControllers and Services
Controllers are responsible for handling incoming HTTP requests and returning responses. They are thin layers that delegate actual business logic to services.
Services contain all the business logic and data-access code. Keeping controllers thin and services fat is a NestJS best practice โ it makes both easier to test independently.
Controllers with Decorators
// src/users/users.controller.ts
import {
Controller, Get, Post, Put, Delete, Patch,
Body, Param, Query, ParseIntPipe, HttpCode, HttpStatus,
UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Controller('users') // All routes: /users
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// GET /users?page=1&limit=20
@Get()
findAll(
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
) {
return this.usersService.findAll({ page, limit });
}
// GET /users/:id
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
// POST /users
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// PATCH /users/:id (protected by JWT Guard)
@Patch(':id')
@UseGuards(JwtAuthGuard)
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
// DELETE /users/:id
@Delete(':id')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
}Services: Business Logic Layer
// src/users/users.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async findAll(options: { page: number; limit: number }) {
const { page, limit } = options;
const [users, total] = await this.userRepository.findAndCount({
select: ['id', 'email', 'name', 'createdAt'],
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
return {
data: users,
meta: { page, limit, total, totalPages: Math.ceil(total / limit) },
};
}
async findOne(id: number): Promise<User> {
const user = await this.userRepository.findOne({
where: { id },
select: ['id', 'email', 'name', 'createdAt'],
});
if (!user) {
throw new NotFoundException('User #' + id + ' not found');
}
return user;
}
async create(createUserDto: CreateUserDto): Promise<User> {
const existing = await this.userRepository.findOne({
where: { email: createUserDto.email },
});
if (existing) {
throw new ConflictException('Email already in use');
}
const hashedPassword = await bcrypt.hash(createUserDto.password, 12);
const user = this.userRepository.create({
...createUserDto,
password: hashedPassword,
});
return this.userRepository.save(user);
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);
Object.assign(user, updateUserDto);
return this.userRepository.save(user);
}
async remove(id: number): Promise<void> {
const user = await this.findOne(id);
await this.userRepository.remove(user);
}
}Data Transfer Objects (DTOs) with class-validator
// src/users/dto/create-user.dto.ts
import {
IsEmail, IsString, MinLength, MaxLength,
IsOptional, IsEnum
} from 'class-validator';
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
}
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
@MaxLength(72)
password: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole = UserRole.USER;
}
// Install: npm install class-validator class-transformer
// ValidationPipe in main.ts automatically validates incoming bodiesDependency Injection in Depth
Dependency Injection (DI) is the core pattern that makes NestJS applications testable and maintainable. NestJS's IoC container automatically instantiates and wires dependencies, so you never write new SomeService() manually.
When NestJS starts up, it scans all modules, builds a dependency graph, and instantiates every provider exactly once (singleton scope by default). When a controller or service declares a dependency in its constructor, NestJS injects the pre-created instance automatically.
Provider Scopes
import { Injectable, Scope } from '@nestjs/common';
// DEFAULT โ Singleton: one instance shared across the entire application
@Injectable()
export class SingletonService {}
// REQUEST โ New instance created for each incoming request
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}
// TRANSIENT โ New instance every time it is injected
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {}Custom Providers
// Value provider โ inject a config object
const configProvider = {
provide: 'APP_CONFIG',
useValue: {
apiUrl: process.env.API_URL,
timeout: 5000,
},
};
// Factory provider โ inject the result of a factory function
const databaseProvider = {
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const connection = await createConnection({
host: configService.get('DB_HOST'),
port: configService.get<number>('DB_PORT'),
});
return connection;
},
inject: [ConfigService], // Dependencies of the factory
};
// Class provider โ substitute one class for another (useful for testing)
const loggerProvider = {
provide: Logger,
useClass: process.env.NODE_ENV === 'test' ? MockLogger : Logger,
};
@Module({
providers: [configProvider, databaseProvider, loggerProvider],
})
export class AppModule {}
// Inject a custom provider with @Inject()
@Injectable()
export class UserService {
constructor(
@Inject('APP_CONFIG') private config: AppConfig,
@Inject('DATABASE_CONNECTION') private db: Connection,
) {}
}Circular Dependencies and forwardRef
// When two services depend on each other, use forwardRef()
// service-a.ts
@Injectable()
export class ServiceA {
constructor(
@Inject(forwardRef(() => ServiceB))
private serviceB: ServiceB,
) {}
}
// service-b.ts
@Injectable()
export class ServiceB {
constructor(
@Inject(forwardRef(() => ServiceA))
private serviceA: ServiceA,
) {}
}
// Note: Circular dependencies are usually a design smell.
// Prefer refactoring to a shared third service instead.Database Integration with TypeORM and Prisma
NestJS provides official integration packages for the most popular Node.js ORMs. TypeORM is tightly integrated with NestJS's decorator-based approach, while Prisma offers a schema-first approach with exceptional type safety and developer experience.
TypeORM Integration
# Install TypeORM and the NestJS integration package
npm install @nestjs/typeorm typeorm pg
# For MySQL/MariaDB use: mysql2
# For SQLite use: sqlite3
# For MongoDB use: mongodb// src/users/entities/user.entity.ts
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn, OneToMany,
} from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ select: false }) // Excluded from SELECT by default
password: string;
@Column({ default: 'user' })
role: string;
@Column({ default: true })
isActive: boolean;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// src/posts/entities/post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column('text')
content: string;
@ManyToOne(() => User, (user) => user.posts, { eager: false })
author: User;
@CreateDateColumn()
createdAt: Date;
}
// Using QueryBuilder for complex queries
async findUserWithPosts(userId: number) {
return this.userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.id = :id', { id: userId })
.andWhere('user.isActive = :active', { active: true })
.orderBy('post.createdAt', 'DESC')
.getOne();
}Prisma Integration
# Install Prisma
npm install prisma @prisma/client
npx prisma init
# Define your schema in prisma/schema.prisma
# Then generate the Prisma client
npx prisma generate
# Apply migrations
npx prisma migrate dev --name init// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String
role String @default("user")
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
// src/prisma/prisma.service.ts
import { 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();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
// src/users/users.service.ts (Prisma version)
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findAll() {
return this.prisma.user.findMany({
select: { id: true, email: true, name: true, createdAt: true },
orderBy: { createdAt: 'desc' },
});
}
async create(data: CreateUserDto) {
return this.prisma.user.create({
data: {
...data,
password: await bcrypt.hash(data.password, 12),
},
});
}
}Authentication and Guards
Authentication in NestJS is typically implemented using Passport.js strategies wrapped in the @nestjs/passport package. The most common pattern uses JWT (JSON Web Tokens) for stateless authentication โ perfect for REST APIs and microservices.
Setting Up JWT Authentication
npm install @nestjs/passport @nestjs/jwt passport passport-jwt
npm install --save-dev @types/passport-jwt// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UsersModule } from '../users/users.module';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'supersecret',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user) throw new UnauthorizedException('Invalid credentials');
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) throw new UnauthorizedException('Invalid credentials');
const { password: _, ...result } = user;
return result;
}
async login(email: string, password: string) {
const user = await this.validateUser(email, password);
const payload = { sub: user.id, email: user.email, role: user.role };
return {
access_token: this.jwtService.sign(payload),
user,
};
}
async register(createUserDto: CreateUserDto) {
const user = await this.usersService.create(createUserDto);
return this.login(user.email, createUserDto.password);
}
}JWT Strategy and Guard
// src/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
export interface JwtPayload {
sub: number;
email: string;
role: string;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'supersecret',
});
}
async validate(payload: JwtPayload) {
// This return value is attached to req.user
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}
// src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// src/auth/guards/roles.guard.ts
import {
Injectable, CanActivate, ExecutionContext, ForbiddenException
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
const hasRole = requiredRoles.includes(user.role);
if (!hasRole) throw new ForbiddenException('Insufficient permissions');
return true;
}
}
// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// Usage in a controller
@Get('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
getAdminData(@Request() req) {
return { message: 'Admin only data', user: req.user };
}Auth Controller
// src/auth/auth.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus, Get, UseGuards, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { LoginDto } from './dto/login.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
export class LoginDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
register(@Body() createUserDto: CreateUserDto) {
return this.authService.register(createUserDto);
}
@Post('login')
@HttpCode(HttpStatus.OK)
login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto.email, loginDto.password);
}
@Get('me')
@UseGuards(JwtAuthGuard)
getProfile(@Request() req) {
return req.user;
}
}Pipes, Interceptors, and Exception Filters
NestJS provides three powerful mechanisms to intercept and transform the request/response lifecycle beyond simple guards. These run in a specific order: Guards โ Interceptors (before) โ Pipes โ Handler โ Interceptors (after) โ Filters (on exception).
Custom Pipes
Pipes transform incoming data (e.g., string to number) or validate it before it reaches a route handler. NestJS ships with built-in pipes: ValidationPipe, ParseIntPipe, ParseUUIDPipe, DefaultValuePipe.
// Custom pipe: trim and lowercase a string
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class TrimLowercasePipe implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string {
if (typeof value !== 'string') {
throw new BadRequestException('Expected a string value');
}
return value.trim().toLowerCase();
}
}
// Custom pipe: parse and validate a positive integer
@Injectable()
export class ParsePositiveIntPipe implements PipeTransform<string, number> {
transform(value: string): number {
const val = parseInt(value, 10);
if (isNaN(val) || val <= 0) {
throw new BadRequestException('Expected a positive integer');
}
return val;
}
}
// Use in a controller
@Get('search')
search(@Query('q', TrimLowercasePipe) query: string) {
return this.searchService.search(query);
}
@Get(':id')
findOne(@Param('id', ParsePositiveIntPipe) id: number) {
return this.usersService.findOne(id);
}Interceptors
Interceptors can modify requests before they reach the handler, modify responses after the handler runs, catch exceptions, or add extra logic like logging and caching.
// Logging interceptor โ logs request duration
import {
Injectable, NestInterceptor, ExecutionContext, CallHandler
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const { method, url } = req;
const start = Date.now();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
console.log(method + ' ' + url + ' - ' + duration + 'ms');
}),
);
}
}
// Transform interceptor โ wrap all responses in a data envelope
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T; timestamp: string }> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
data,
timestamp: new Date().toISOString(),
success: true,
})),
);
}
}
// Register globally in main.ts
app.useGlobalInterceptors(new LoggingInterceptor(), new TransformInterceptor());Exception Filters
// src/common/filters/http-exception.filter.ts
import {
ExceptionFilter, Catch, ArgumentsHost,
HttpException, HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
@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();
const error =
typeof exceptionResponse === 'string'
? { message: exceptionResponse }
: (exceptionResponse as object);
response.status(status).json({
...error,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// Catch everything (including non-HTTP errors)
@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;
console.error('Unhandled exception:', exception);
response.status(status).json({
statusCode: status,
message: 'Internal server error',
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// Register globally
app.useGlobalFilters(new AllExceptionsFilter());Testing NestJS Applications
NestJS is designed with testability as a first-class concern. The @nestjs/testing package provides utilities to create isolated testing modules where you can replace real dependencies with mocks. The NestJS CLI automatically generates .spec.ts test files alongside every generated resource.
NestJS uses Jest as the default test runner. All test configuration is pre-configured in package.json.
Unit Testing Services
// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { NotFoundException, ConflictException } from '@nestjs/common';
// Mock repository factory
const mockUserRepository = () => ({
findAndCount: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
});
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
describe('UsersService', () => {
let service: UsersService;
let userRepository: MockRepository<User>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useFactory: mockUserRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
userRepository = module.get<MockRepository<User>>(getRepositoryToken(User));
});
describe('findOne', () => {
it('should return a user when found', async () => {
const mockUser = { id: 1, email: 'test@example.com', name: 'Test User' };
userRepository.findOne.mockResolvedValue(mockUser);
const result = await service.findOne(1);
expect(result).toEqual(mockUser);
expect(userRepository.findOne).toHaveBeenCalledWith({
where: { id: 1 },
select: ['id', 'email', 'name', 'createdAt'],
});
});
it('should throw NotFoundException when user not found', async () => {
userRepository.findOne.mockResolvedValue(null);
await expect(service.findOne(99)).rejects.toThrow(NotFoundException);
});
});
describe('create', () => {
it('should create and return a new user', async () => {
const dto = { name: 'Alice', email: 'alice@example.com', password: 'password123' };
userRepository.findOne.mockResolvedValue(null); // No duplicate
userRepository.create.mockReturnValue({ id: 1, ...dto });
userRepository.save.mockResolvedValue({ id: 1, ...dto });
const result = await service.create(dto);
expect(result).toHaveProperty('id', 1);
expect(userRepository.save).toHaveBeenCalledTimes(1);
});
it('should throw ConflictException if email already exists', async () => {
userRepository.findOne.mockResolvedValue({ id: 1, email: 'alice@example.com' });
const dto = { name: 'Alice', email: 'alice@example.com', password: 'password123' };
await expect(service.create(dto)).rejects.toThrow(ConflictException);
});
});
});Unit Testing Controllers
// src/users/users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
// Mock the entire service
const mockUsersService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{ provide: UsersService, useValue: mockUsersService },
],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('findAll', () => {
it('should return paginated users', async () => {
const mockResult = { data: [], meta: { page: 1, limit: 20, total: 0 } };
mockUsersService.findAll.mockResolvedValue(mockResult);
const result = await controller.findAll(1, 20);
expect(result).toEqual(mockResult);
expect(mockUsersService.findAll).toHaveBeenCalledWith({ page: 1, limit: 20 });
});
});
});End-to-End Tests with Supertest
// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from '../src/users/entities/user.entity';
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(getRepositoryToken(User))
.useValue({
findAndCount: jest.fn().mockResolvedValue([[], 0]),
findOne: jest.fn().mockResolvedValue(null),
})
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/users (GET) โ returns 200 with empty array', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(Array.isArray(res.body.data)).toBe(true);
});
});
it('/users (POST) โ returns 400 with invalid body', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: 'not-an-email', password: 'short' })
.expect(400);
});
it('/users/999 (GET) โ returns 404 for missing user', () => {
return request(app.getHttpServer())
.get('/users/999')
.expect(404);
});
});
// Run tests
// npm run test โ unit tests
// npm run test:e2e โ end-to-end tests
// npm run test:cov โ test coverage reportNestJS vs Express.js: Which Should You Choose?
Both frameworks run on Node.js and can power production-grade APIs. The decision comes down to your team size, application complexity, and how much structure you want out of the box.
| Aspect | Express.js | NestJS |
|---|---|---|
| Architecture | Unopinionated โ you design it | Opinionated โ modules/controllers/services |
| TypeScript | Optional (add manually) | First-class, built-in |
| DI Container | None built-in | Built-in IoC container |
| Learning Curve | Low | Medium (Angular familiarity helps) |
| Boilerplate | Minimal | More (but CLI generates it) |
| Testing | Manual setup | Built-in testing utilities |
| Performance | ~28K req/s (vanilla) | ~25K req/s (Express adapter) |
| Ecosystem | Vast (30M+ npm pkgs) | Growing, plus Express compat |
| Best For | Small APIs, microservices | Large enterprise apps, teams |
| GraphQL | Manual setup | @nestjs/graphql (built-in) |
| WebSockets | socket.io (manual) | @nestjs/websockets (built-in) |
| Microservices | Manual | @nestjs/microservices (built-in) |
When to Choose Express
- Small to medium REST APIs where you want full control over structure
- Microservices that need minimal overhead and fast cold starts
- Teams already deeply familiar with Express middleware patterns
- Projects where you want to cherry-pick only what you need
- Prototypes and MVPs where speed of initial development matters most
When to Choose NestJS
- Large enterprise APIs with multiple teams working in the same codebase
- Applications that will grow significantly over time
- Teams coming from Angular or Java Spring Boot who want familiar patterns
- Projects needing built-in support for GraphQL, WebSockets, or microservices
- When you want architecture enforced by convention rather than documentation
- Monorepo setups where multiple apps share code (NestJS has native monorepo support)
Advanced Topics and Production Tips
Configuration Management
# Install the config module
npm install @nestjs/config
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Available in every module without re-import
envFilePath: '.env',
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
}),
}),
],
})
export class AppModule {}
// Inject config in any service
@Injectable()
export class AppService {
constructor(private configService: ConfigService) {}
getDatabaseUrl(): string {
const host = this.configService.get<string>('DB_HOST');
const port = this.configService.get<number>('DB_PORT', 5432);
return 'postgres://' + host + ':' + port + '/mydb';
}
}Caching with Redis
npm install @nestjs/cache-manager cache-manager cache-manager-redis-store redis
// app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.register({
isGlobal: true,
store: redisStore,
host: process.env.REDIS_HOST || 'localhost',
port: 6379,
ttl: 300, // seconds
}),
],
})
export class AppModule {}
// Use in a controller or service
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { Inject } from '@nestjs/common';
@Injectable()
export class ProductsService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async findAll() {
const cached = await this.cacheManager.get('all_products');
if (cached) return cached;
const products = await this.productRepository.find();
await this.cacheManager.set('all_products', products, 300);
return products;
}
}
// Or use the built-in @CacheKey and @CacheTTL decorators on controllers
@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
@Get()
@CacheKey('all_products')
@CacheTTL(300)
findAll() {
return this.productsService.findAll();
}
}OpenAPI / Swagger Documentation
npm install @nestjs/swagger swagger-ui-express
// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('My API')
.setDescription('API documentation')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
await app.listen(3000);
}
// Annotate DTOs and controllers with Swagger decorators
import { ApiProperty, ApiOperation, ApiTags, ApiBearerAuth } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'Alice Johnson', description: 'Full name of the user' })
@IsString()
name: string;
@ApiProperty({ example: 'alice@example.com' })
@IsEmail()
email: string;
}
@ApiTags('users')
@ApiBearerAuth()
@Controller('users')
export class UsersController {
@ApiOperation({ summary: 'Get all users with pagination' })
@Get()
findAll() { ... }
}
// Visit http://localhost:3000/api/docs for the Swagger UIHealth Checks
npm install @nestjs/terminus
// health/health.module.ts
import { TerminusModule } from '@nestjs/terminus';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [TerminusModule, HttpModule],
controllers: [HealthController],
})
export class HealthModule {}
// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator, HttpHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private http: HttpHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.com'),
]);
}
}
// GET /health returns: { status: "ok", info: { database: { status: "up" } } }Production Checklist
- Set
NODE_ENV=productionโ disables TypeORMsynchronizewarning and enables optimizations - Use environment variables for all secrets โ never hardcode JWT secrets or DB passwords
- Enable Helmet for security headers:
app.use(helmet()) - Enable rate limiting with
@nestjs/throttlerto prevent abuse - Use a
ValidationPipeglobally withwhitelist: trueto strip unknown fields - Disable TypeORM
synchronize: truein production โ use migrations instead - Add health check endpoints for load balancer readiness probes
- Use
app.setGlobalPrefix('api/v1')for API versioning - Configure PM2 or similar process manager with cluster mode for multi-core usage
- Enable structured logging with Pino via
nestjs-pinopackage
Summary
NestJS is the most mature, feature-complete TypeScript Node.js framework available in 2026. Its Angular-inspired architecture brings decades of enterprise software design patterns to the backend: dependency injection, modular organization, decorator-based configuration, and built-in testing utilities.
The learning curve is real โ especially if you are coming from plain Express โ but the investment pays off in large codebases where consistency, testability, and maintainability matter. NestJS's CLI, first-party integrations for TypeORM, Prisma, GraphQL, WebSockets, and microservices, combined with comprehensive documentation and active community, make it the top choice for enterprise Node.js development.
Start with nest new my-app, generate resources with nest generate resource, and follow the module pattern โ your future self (and your teammates) will thank you.