NestJS Patterns
Project Structure
src/
├── main.ts # Bootstrap
├── app.module.ts # Root module
├── common/
│ ├── decorators/
│ ├── filters/ # Exception filters
│ ├── guards/ # Auth guards
│ ├── interceptors/ # Logging, transform
│ └── pipes/ # Validation
└── modules/
└── users/
├── users.module.ts
├── users.controller.ts
├── users.service.ts
├── users.repository.ts
├── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
└── entities/
└── user.entity.ts
main.ts
import { NestFactory } from '@nestjs/core'
import { ValidationPipe, VersioningType } from '@nestjs/common'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// Global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown fields
forbidNonWhitelisted: true,
transform: true, // auto-cast types
transformOptions: { enableImplicitConversion: true },
}))
// API versioning
app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' })
// Swagger
const config = new DocumentBuilder()
.setTitle('API')
.setVersion('1.0')
.addBearerAuth()
.build()
SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, config))
app.enableCors({ origin: process.env.CORS_ORIGIN })
await app.listen(process.env.PORT ?? 3000)
}
bootstrap()
Module
// 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])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Controller
// users/users.controller.ts
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, HttpCode, HttpStatus, UseGuards } from '@nestjs/common'
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { PaginationDto } from '../common/dto/pagination.dto'
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@ApiOperation({ summary: 'List users' })
findAll(@Query() pagination: PaginationDto) {
return this.usersService.findAll(pagination)
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id)
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto)
}
@Patch(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.usersService.update(id, dto)
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.usersService.remove(id)
}
}
Service
// 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 type { CreateUserDto } from './dto/create-user.dto'
import type { UpdateUserDto } from './dto/update-user.dto'
import type { PaginationDto } from '../common/dto/pagination.dto'
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepo: Repository<User>
) {}
async findAll({ page = 1, limit = 20 }: PaginationDto) {
const [users, total] = await this.userRepo.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
})
return { users, total, page, limit }
}
async findOne(id: string): Promise<User> {
const user = await this.userRepo.findOneBy({ id })
if (!user) throw new NotFoundException(`User ${id} not found`)
return user
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.userRepo.findOneBy({ email: dto.email })
if (existing) throw new ConflictException('Email already in use')
const user = this.userRepo.create(dto)
return this.userRepo.save(user)
}
async update(id: string, dto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id)
Object.assign(user, dto)
return this.userRepo.save(user)
}
async remove(id: string): Promise<void> {
const user = await this.findOne(id)
await this.userRepo.remove(user)
}
}
DTOs with class-validator
// dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsEnum } from 'class-validator'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { Role } from '../entities/user.entity'
export class CreateUserDto {
@ApiProperty({ example: 'alice@example.com' })
@IsEmail()
email: string
@ApiProperty({ example: 'Alice' })
@IsString()
@MinLength(1)
@MaxLength(100)
name: string
@ApiPropertyOptional({ enum: Role, default: Role.USER })
@IsOptional()
@IsEnum(Role)
role?: Role
}
Guards
// guards/jwt-auth.guard.ts
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err: any, user: any) {
if (err || !user) throw err || new UnauthorizedException()
return user
}
}
Interceptors
// interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(_ctx: ExecutionContext, next: CallHandler): Observable<{ data: T }> {
return next.handle().pipe(map(data => ({ data })))
}
}
Exception Filters
// filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common'
import type { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private logger = new Logger(HttpExceptionFilter.name)
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const res = ctx.getResponse<Response>()
const req = ctx.getRequest<Request>()
const status = exception.getStatus()
this.logger.warn(`${req.method} ${req.url} → ${status}`)
res.status(status).json({
statusCode: status,
message: exception.message,
path: req.url,
timestamp: new Date().toISOString(),
})
}
}