--- name: nestjs-patterns description: NestJS architecture patterns for modules, controllers, providers, DTO validation, guards, interceptors, config, and production-grade TypeScript backends. origin: ECC --- # 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 ```text 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 ```ts 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 ```ts @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 ```ts 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 ```ts @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 ```ts @Catch() export class HttpExceptionFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const response = host.switchToHttp().getResponse(); const request = host.switchToHttp().getRequest(); 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 ```ts 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 ```ts 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.