NestJS Development Patterns
Production-grade NestJS patterns for modular TypeScript backends.
When to Activate
- Building NestJS APIs or services
- Structuring modules, controllers, and providers
- Adding DTO validation, guards, interceptors, or exception filters
- Configuring environment-aware settings and database integrations
- Testing NestJS units or HTTP endpoints
Project Structure
src/
├── app.module.ts
├── main.ts
├── common/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
├── config/
│ ├── configuration.ts
│ └── validation.ts
├── modules/
│ ├── auth/
│ │ ├── auth.controller.ts
│ │ ├── auth.module.ts
│ │ ├── auth.service.ts
│ │ ├── dto/
│ │ ├── guards/
│ │ └── strategies/
│ └── users/
│ ├── dto/
│ ├── entities/
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
└── prisma/ or database/
- Keep domain code inside feature modules.
- Put cross-cutting filters, decorators, guards, and interceptors in
common/.
- Keep DTOs close to the module that owns them.
Bootstrap and Global Validation
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
- Always enable
whitelist and forbidNonWhitelisted on public APIs.
- Prefer one global validation pipe instead of repeating validation config per route.
Modules, Controllers, and Providers
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
getById(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.getById(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
@Injectable()
export class UsersService {
constructor(private readonly usersRepo: UsersRepository) {}
async create(dto: CreateUserDto) {
return this.usersRepo.create(dto);
}
}
- Controllers should stay thin: parse HTTP input, call a provider, return response DTOs.
- Put business logic in injectable services, not controllers.
- Export only the providers other modules genuinely need.
DTOs and Validation
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@Length(2, 80)
name!: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
- Validate every request DTO with
class-validator.
- Use dedicated response DTOs or serializers instead of returning ORM entities directly.
- Avoid leaking internal fields such as password hashes, tokens, or audit columns.
Auth, Guards, and Request Context
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin/report')
getAdminReport(@Req() req: AuthenticatedRequest) {
return this.reportService.getForUser(req.user.id);
}
- Keep auth strategies and guards module-local unless they are truly shared.
- Encode coarse access rules in guards, then do resource-specific authorization in services.
- Prefer explicit request types for authenticated request objects.
Exception Filters and Error Shape
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
const request = host.switchToHttp().getRequest<Request>();
if (exception instanceof HttpException) {
return response.status(exception.getStatus()).json({
path: request.url,
error: exception.getResponse(),
});
}
return response.status(500).json({
path: request.url,
error: 'Internal server error',
});
}
}
- Keep one consistent error envelope across the API.
- Throw framework exceptions for expected client errors; log and wrap unexpected failures centrally.
Config and Environment Validation
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
validate: validateEnv,
});
- Validate env at boot, not lazily at first request.
- Keep config access behind typed helpers or config services.
- Split dev/staging/prod concerns in config factories instead of branching throughout feature code.
Persistence and Transactions
- Keep repository / ORM code behind providers that speak domain language.
- For Prisma or TypeORM, isolate transactional workflows in services that own the unit of work.
- Do not let controllers coordinate multi-step writes directly.
Testing
describe('UsersController', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [UsersModule],
}).compile();
app = moduleRef.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.init();
});
});
- Unit test providers in isolation with mocked dependencies.
- Add request-level tests for guards, validation pipes, and exception filters.
- Reuse the same global pipes/filters in tests that you use in production.
Production Defaults
- Enable structured logging and request correlation ids.
- Terminate on invalid env/config instead of booting partially.
- Prefer async provider initialization for DB/cache clients with explicit health checks.
- Keep background jobs and event consumers in their own modules, not inside HTTP controllers.
- Make rate limiting, auth, and audit logging explicit for public endpoints.
1---2name: nestjs-patterns3description: NestJS architecture patterns for modules, controllers, providers, DTO validation, guards, interceptors, config, and production-grade TypeScript backends. Use when building or reviewing a NestJS backend — modules, providers, DTO validation, guards, or interceptors.4---56# NestJS Development Patterns78Production-grade NestJS patterns for modular TypeScript backends.910## When to Activate1112- Building NestJS APIs or services13- Structuring modules, controllers, and providers14- Adding DTO validation, guards, interceptors, or exception filters15- Configuring environment-aware settings and database integrations16- Testing NestJS units or HTTP endpoints1718## Project Structure1920```text21src/22├── app.module.ts23├── main.ts24├── common/25│ ├── filters/26│ ├── guards/27│ ├── interceptors/28│ └── pipes/29├── config/30│ ├── configuration.ts31│ └── validation.ts32├── modules/33│ ├── auth/34│ │ ├── auth.controller.ts35│ │ ├── auth.module.ts36│ │ ├── auth.service.ts37│ │ ├── dto/38│ │ ├── guards/39│ │ └── strategies/40│ └── users/41│ ├── dto/42│ ├── entities/43│ ├── users.controller.ts44│ ├── users.module.ts45│ └── users.service.ts46└── prisma/ or database/47```4849- Keep domain code inside feature modules.50- Put cross-cutting filters, decorators, guards, and interceptors in `common/`.51- Keep DTOs close to the module that owns them.5253## Bootstrap and Global Validation5455```ts56async function bootstrap() {57 const app = await NestFactory.create(AppModule, { bufferLogs: true });5859 app.useGlobalPipes(60 new ValidationPipe({61 whitelist: true,62 forbidNonWhitelisted: true,63 transform: true,64 transformOptions: { enableImplicitConversion: true },65 }),66 );6768 app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));69 app.useGlobalFilters(new HttpExceptionFilter());7071 await app.listen(process.env.PORT ?? 3000);72}73bootstrap();74```7576- Always enable `whitelist` and `forbidNonWhitelisted` on public APIs.77- Prefer one global validation pipe instead of repeating validation config per route.7879## Modules, Controllers, and Providers8081```ts82@Module({83 controllers: [UsersController],84 providers: [UsersService],85 exports: [UsersService],86})87export class UsersModule {}8889@Controller('users')90export class UsersController {91 constructor(private readonly usersService: UsersService) {}9293 @Get(':id')94 getById(@Param('id', ParseUUIDPipe) id: string) {95 return this.usersService.getById(id);96 }9798 @Post()99 create(@Body() dto: CreateUserDto) {100 return this.usersService.create(dto);101 }102}103104@Injectable()105export class UsersService {106 constructor(private readonly usersRepo: UsersRepository) {}107108 async create(dto: CreateUserDto) {109 return this.usersRepo.create(dto);110 }111}112```113114- Controllers should stay thin: parse HTTP input, call a provider, return response DTOs.115- Put business logic in injectable services, not controllers.116- Export only the providers other modules genuinely need.117118## DTOs and Validation119120```ts121export class CreateUserDto {122 @IsEmail()123 email!: string;124125 @IsString()126 @Length(2, 80)127 name!: string;128129 @IsOptional()130 @IsEnum(UserRole)131 role?: UserRole;132}133```134135- Validate every request DTO with `class-validator`.136- Use dedicated response DTOs or serializers instead of returning ORM entities directly.137- Avoid leaking internal fields such as password hashes, tokens, or audit columns.138139## Auth, Guards, and Request Context140141```ts142@UseGuards(JwtAuthGuard, RolesGuard)143@Roles('admin')144@Get('admin/report')145getAdminReport(@Req() req: AuthenticatedRequest) {146 return this.reportService.getForUser(req.user.id);147}148```149150- Keep auth strategies and guards module-local unless they are truly shared.151- Encode coarse access rules in guards, then do resource-specific authorization in services.152- Prefer explicit request types for authenticated request objects.153154## Exception Filters and Error Shape155156```ts157@Catch()158export class HttpExceptionFilter implements ExceptionFilter {159 catch(exception: unknown, host: ArgumentsHost) {160 const response = host.switchToHttp().getResponse<Response>();161 const request = host.switchToHttp().getRequest<Request>();162163 if (exception instanceof HttpException) {164 return response.status(exception.getStatus()).json({165 path: request.url,166 error: exception.getResponse(),167 });168 }169170 return response.status(500).json({171 path: request.url,172 error: 'Internal server error',173 });174 }175}176```177178- Keep one consistent error envelope across the API.179- Throw framework exceptions for expected client errors; log and wrap unexpected failures centrally.180181## Config and Environment Validation182183```ts184ConfigModule.forRoot({185 isGlobal: true,186 load: [configuration],187 validate: validateEnv,188});189```190191- Validate env at boot, not lazily at first request.192- Keep config access behind typed helpers or config services.193- Split dev/staging/prod concerns in config factories instead of branching throughout feature code.194195## Persistence and Transactions196197- Keep repository / ORM code behind providers that speak domain language.198- For Prisma or TypeORM, isolate transactional workflows in services that own the unit of work.199- Do not let controllers coordinate multi-step writes directly.200201## Testing202203```ts204describe('UsersController', () => {205 let app: INestApplication;206207 beforeAll(async () => {208 const moduleRef = await Test.createTestingModule({209 imports: [UsersModule],210 }).compile();211212 app = moduleRef.createNestApplication();213 app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));214 await app.init();215 });216});217```218219- Unit test providers in isolation with mocked dependencies.220- Add request-level tests for guards, validation pipes, and exception filters.221- Reuse the same global pipes/filters in tests that you use in production.222223## Production Defaults224225- Enable structured logging and request correlation ids.226- Terminate on invalid env/config instead of booting partially.227- Prefer async provider initialization for DB/cache clients with explicit health checks.228- Keep background jobs and event consumers in their own modules, not inside HTTP controllers.229- Make rate limiting, auth, and audit logging explicit for public endpoints.