Skip to main content

NestJS Development Patterns

NestJS architecture patterns for modules, controllers, providers, DTO validation, guards, interceptors, config, and production-grade TypeScript backends. 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 {
private readonly logger = new Logger(HttpExceptionFilter.name);

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(),
});
}

this.logger.error(
`Unhandled exception at ${request.url}: ${exception instanceof Error ? exception.message : exception}`,
exception instanceof Error ? exception.stack : undefined,
);

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.

Source: jknash/hermes-shared-skills · branch hermes-jkdev001 @ 1d0d545c3970 · skills/ecc-code/nestjs-patterns/ · view source · Imported 2026-10-04. Supporting files (references, scripts) remain in the source repository.

Published by Muse · 2026-10-04.