NestJS
Purpose
Build NestJS applications where the module graph reflects the domain, validation happens at the edge, and cross-cutting concerns live in guards and interceptors rather than being copied into every controller.
When to Use
- Building or reviewing a NestJS service.
- Structuring modules, providers, and their scopes.
- Implementing authentication, authorization, and request validation.
- Writing unit and end-to-end tests for a Nest application.
Capabilities
- Module and provider design, including dynamic modules.
- Validation with
class-validator and the global ValidationPipe.
- Guards (authorization), interceptors (cross-cutting), filters (error mapping).
- Data access with Prisma or TypeORM, correctly scoped.
- Testing with the Nest testing module and Supertest.
Inputs
- The domain boundaries the modules should follow.
- The authentication scheme and the authorization model.
- The persistence layer.
Outputs
- Modules that encapsulate a domain and export only their public services.
- A global validation pipe with whitelisting enabled.
- Controllers that are thin, and services that contain the logic.
Workflow
- Model the modules on the domain — One module per bounded capability, exporting the services other modules may use. A module that exports everything is not a boundary.
- Enable strict validation globally —
whitelist: true and forbidNonWhitelisted: true. Without these, a client can send extra fields and your DTO will happily carry them into the service.
- Push cross-cutting concerns out of controllers — Auth in a guard, logging and timing in an interceptor, error mapping in an exception filter.
- Keep providers stateless and singleton — Request-scoped providers cascade: anything that injects one becomes request-scoped too, and performance degrades quietly.
- Test at two levels — Unit tests for services with mocked dependencies, and end-to-end tests through the real HTTP stack with a real (containerized) database.
Best Practices
ValidationPipe without whitelist: true is decoration, not validation. Extra properties pass straight through.
transform: true on the pipe converts payloads into DTO class instances — otherwise your @Type decorators and defaults do nothing.
- Circular module dependencies are a design smell.
forwardRef is an escape hatch that hides a boundary you drew wrong.
- Do not inject the repository into the controller. The controller's job is HTTP; the service's job is the domain.
- Global exception filters map domain errors to HTTP status codes in one place. Throwing
HttpException from a service couples the domain to the transport.
- Use
ConfigModule with a validation schema so a missing environment variable fails at boot.
Examples
Validation, guard, and thin controller:
// main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // strip unknown properties
forbidNonWhitelisted: true, // and reject the request if any are present
transform: true, // instantiate the DTO class
}),
);
export class CreateOrderDto {
@IsUUID() customerId!: string;
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => OrderLineDto)
lines!: OrderLineDto[];
}
@Controller("orders")
@UseGuards(JwtAuthGuard, TenantGuard)
export class OrdersController {
constructor(private readonly orders: OrdersService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreateOrderDto, @CurrentUser() user: User): Promise<OrderView> {
return this.orders.place(user.tenantId, dto);
}
}
Domain errors mapped centrally:
@Catch(DomainError)
export class DomainExceptionFilter implements ExceptionFilter {
catch(error: DomainError, host: ArgumentsHost) {
const status = {
NOT_FOUND: 404,
CONFLICT: 409,
INVALID: 422,
}[error.kind] ?? 400;
host.switchToHttp().getResponse().status(status).json({
type: `https://api.example.com/errors/${error.kind.toLowerCase()}`,
title: error.message,
status,
});
}
}
Notes
- Request-scoped providers instantiate a new instance per request and force the entire injection chain above them to do the same. Measure before using one.
- Nest's
TestingModule lets you override any provider, which is almost always preferable to mocking a module's internals.
- Interceptors run around the handler and can transform the response. That makes them the right place for a response envelope — and the wrong place for business logic.
1---2name: nestjs3description: Use when building NestJS services. Covers module structure, providers and scopes, validation pipes, guards and interceptors, TypeORM/Prisma integration, and testing.4---56# NestJS78## Purpose910Build NestJS applications where the module graph reflects the domain, validation happens at the edge, and cross-cutting concerns live in guards and interceptors rather than being copied into every controller.1112## When to Use1314- Building or reviewing a NestJS service.15- Structuring modules, providers, and their scopes.16- Implementing authentication, authorization, and request validation.17- Writing unit and end-to-end tests for a Nest application.1819## Capabilities2021- Module and provider design, including dynamic modules.22- Validation with `class-validator` and the global `ValidationPipe`.23- Guards (authorization), interceptors (cross-cutting), filters (error mapping).24- Data access with Prisma or TypeORM, correctly scoped.25- Testing with the Nest testing module and Supertest.2627## Inputs2829- The domain boundaries the modules should follow.30- The authentication scheme and the authorization model.31- The persistence layer.3233## Outputs3435- Modules that encapsulate a domain and export only their public services.36- A global validation pipe with whitelisting enabled.37- Controllers that are thin, and services that contain the logic.3839## Workflow40411. **Model the modules on the domain** — One module per bounded capability, exporting the services other modules may use. A module that exports everything is not a boundary.422. **Enable strict validation globally** — `whitelist: true` and `forbidNonWhitelisted: true`. Without these, a client can send extra fields and your DTO will happily carry them into the service.433. **Push cross-cutting concerns out of controllers** — Auth in a guard, logging and timing in an interceptor, error mapping in an exception filter.444. **Keep providers stateless and singleton** — Request-scoped providers cascade: anything that injects one becomes request-scoped too, and performance degrades quietly.455. **Test at two levels** — Unit tests for services with mocked dependencies, and end-to-end tests through the real HTTP stack with a real (containerized) database.4647## Best Practices4849- `ValidationPipe` without `whitelist: true` is decoration, not validation. Extra properties pass straight through.50- `transform: true` on the pipe converts payloads into DTO class instances — otherwise your `@Type` decorators and defaults do nothing.51- Circular module dependencies are a design smell. `forwardRef` is an escape hatch that hides a boundary you drew wrong.52- Do not inject the repository into the controller. The controller's job is HTTP; the service's job is the domain.53- Global exception filters map domain errors to HTTP status codes in one place. Throwing `HttpException` from a service couples the domain to the transport.54- Use `ConfigModule` with a validation schema so a missing environment variable fails at boot.5556## Examples5758**Validation, guard, and thin controller:**5960```typescript61// main.ts62app.useGlobalPipes(63 new ValidationPipe({64 whitelist: true, // strip unknown properties65 forbidNonWhitelisted: true, // and reject the request if any are present66 transform: true, // instantiate the DTO class67 }),68);69```7071```typescript72export class CreateOrderDto {73 @IsUUID() customerId!: string;7475 @IsArray()76 @ArrayMinSize(1)77 @ValidateNested({ each: true })78 @Type(() => OrderLineDto)79 lines!: OrderLineDto[];80}8182@Controller("orders")83@UseGuards(JwtAuthGuard, TenantGuard)84export class OrdersController {85 constructor(private readonly orders: OrdersService) {}8687 @Post()88 @HttpCode(HttpStatus.CREATED)89 create(@Body() dto: CreateOrderDto, @CurrentUser() user: User): Promise<OrderView> {90 return this.orders.place(user.tenantId, dto);91 }92}93```9495**Domain errors mapped centrally:**9697```typescript98@Catch(DomainError)99export class DomainExceptionFilter implements ExceptionFilter {100 catch(error: DomainError, host: ArgumentsHost) {101 const status = {102 NOT_FOUND: 404,103 CONFLICT: 409,104 INVALID: 422,105 }[error.kind] ?? 400;106107 host.switchToHttp().getResponse().status(status).json({108 type: `https://api.example.com/errors/${error.kind.toLowerCase()}`,109 title: error.message,110 status,111 });112 }113}114```115116## Notes117118- Request-scoped providers instantiate a new instance per request and force the entire injection chain above them to do the same. Measure before using one.119- Nest's `TestingModule` lets you override any provider, which is almost always preferable to mocking a module's internals.120- Interceptors run around the handler and can transform the response. That makes them the right place for a response envelope — and the wrong place for business logic.