NestJS Complete Guide — Enterprise TypeScript Backend 2024
Advertisement
Introduction
Why This Matters
NestJS is the most opinionated and structured Node.js framework in 2024, and that is precisely its advantage for large teams. It enforces clear separation of concerns through modules, controllers, and services — the same architectural patterns used in Spring Boot and Angular. This consistency dramatically improves code review, onboarding, and long-term maintainability.
Beyond structure, NestJS provides decorator-driven dependency injection, interceptors, guards, pipes, and filters as first-class concepts. You don't need to wire up your own DI container, middleware chains, or validation pipeline. They come built-in, type-safe, and well-tested.
For solo projects or simple CRUD APIs, NestJS may feel like overkill. For APIs with tens of modules, multiple teams, and complex authorization rules, it is the most productive choice in the Node.js ecosystem.
Installation and Setup
npm install -g @nestjs/cli
nest new my-api
cd my-api
npm run start:devModule Architecture
Every NestJS application is a tree of modules. Each module encapsulates a feature.
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Make available to other modules
})
export class UsersModule {}// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UsersModule } from './users/users.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
autoLoadEntities: true,
synchronize: config.get('NODE_ENV') === 'development',
}),
}),
UsersModule,
],
})
export class AppModule {}Controllers
Controllers handle HTTP requests and delegate business logic to services.
// src/users/users.controller.ts
import {
Controller, Get, Post, Put, Delete, Body, Param,
Query, HttpCode, HttpStatus, UseGuards
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ListUsersQuery } from './dto/list-users.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@ApiTags('Users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
findAll(@Query() query: ListUsersQuery) {
return this.usersService.findAll(query);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOneOrFail(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
@Put(':id')
@UseGuards(JwtAuthGuard)
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.usersService.update(id, dto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(JwtAuthGuard)
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}Services and Dependency Injection
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './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 usersRepository: Repository<User>
) {}
async findAll(query: { page?: number; limit?: number }): Promise<User[]> {
const { page = 1, limit = 20 } = query;
return this.usersRepository.find({
skip: (page - 1) * limit,
take: limit,
select: ['id', 'name', 'email', 'role', 'createdAt'],
});
}
async findOneOrFail(id: string): Promise<User> {
const user = await this.usersRepository.findOneBy({ id });
if (!user) throw new NotFoundException(`User ${id} not found`);
return user;
}
async create(dto: CreateUserDto): Promise<User> {
const passwordHash = await bcrypt.hash(dto.password, 12);
const user = this.usersRepository.create({ ...dto, passwordHash });
return this.usersRepository.save(user);
}
async update(id: string, dto: UpdateUserDto): Promise<User> {
const user = await this.findOneOrFail(id);
Object.assign(user, dto);
return this.usersRepository.save(user);
}
async remove(id: string): Promise<void> {
const user = await this.findOneOrFail(id);
await this.usersRepository.remove(user);
}
}Validation Pipes and DTOs
// src/users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsEnum, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string = '';
@IsEmail()
@Transform(({ value }: { value: string }) => value.toLowerCase())
email: string = '';
@IsString()
@MinLength(8)
password: string = '';
@IsEnum(['admin', 'user'])
@IsOptional()
role: 'admin' | 'user' = 'user';
}
// Enable globally in main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));Guards and Authorization
// src/auth/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
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;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user?.role);
}
}
// Usage on controller
@Delete(':id')
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}Common Mistakes
- Not setting
whitelist: trueonValidationPipe— allows unknown properties through - Importing
UsersModuleinto itself causing circular dependency errors - Using
@InjectRepositorywithout registering the entity inTypeOrmModule.forFeature - Throwing raw errors instead of NestJS exceptions — they won't map to HTTP status codes
- Not calling
app.close()in test teardown — leaves open handles causing test timeouts
Best Practices
- Use
@nestjs/configwith validation schemas for environment variables, not rawprocess.env - Apply
ValidationPipeglobally withtransform: trueto auto-convert query strings to numbers - Use NestJS exceptions (
NotFoundException,BadRequestException) for automatic HTTP status mapping - Keep controllers thin — no business logic, only orchestration between request and service
- Write unit tests for services with mocked repositories using
@nestjs/testing
Key Takeaways
- NestJS uses Angular's module, controller, service pattern bringing enterprise structure to Node.js
- Dependency injection is automatic —
@Injectable()services are resolved by the NestJS IoC container - Guards implement
CanActivate— attach them with@UseGuards()for authentication and authorization ValidationPipewithclass-validatorandclass-transformerprovides automatic input validation and transformation@Module()controls what is injectable, exported, and imported — it is the primary encapsulation boundary- NestJS supports both Express and Fastify as HTTP adapters under the hood
- The CLI (
nest generate) scaffolds modules, controllers, services, guards, and interceptors quickly - Interceptors, Guards, Pipes, and Filters run in a defined lifecycle order around each request
Advertisement