NestJS Complete Guide — Enterprise TypeScript Backend 2024

Sanjeev SharmaSanjeev Sharma
6 min read

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:dev

Module 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: true on ValidationPipe — allows unknown properties through
  • Importing UsersModule into itself causing circular dependency errors
  • Using @InjectRepository without registering the entity in TypeOrmModule.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/config with validation schemas for environment variables, not raw process.env
  • Apply ValidationPipe globally with transform: true to 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
  • ValidationPipe with class-validator and class-transformer provides 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading