api-framework-nestjs
NestJS backend framework - modules, controllers, services, DI, guards, pipes, interceptors, exception filters, middleware, DTOs with class-validator
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-framework-nestjs/skills/api-framework-nestjs
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
NestJS Patterns
Quick Guide: NestJS is an opinionated, modular Node.js framework built on TypeScript. Use modules to organize features, controllers for HTTP routing, services for business logic with dependency injection, DTOs with class-validator for validation, guards for auth, and exception filters for error handling. Key gotchas: always register services in module
providers, always enableValidationPipeglobally withwhitelist: true, never put business logic in controllers, never instantiate services withnew. NestJS 11 is the current stable version (opt-in SWC compiler, Express v5, reversed termination hooks).
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use @Injectable() on every service and register it in the module providers array)
(You MUST enable ValidationPipe globally with whitelist: true and forbidNonWhitelisted: true)
(You MUST use DTOs with class-validator decorators for ALL request body validation — never validate manually in controllers)
(You MUST throw NestJS built-in HTTP exceptions (NotFoundException, BadRequestException, etc.) — never send raw status codes)
(You MUST use constructor injection for dependencies — never instantiate services manually with new)
</critical_requirements>
Auto-detection: NestJS, @nestjs/common, @nestjs/core, @Module, @Controller, @Injectable, @Get, @Post, @Body, @Param, @Query, @UseGuards, @UseInterceptors, @UsePipes, @UseFilters, CanActivate, NestInterceptor, PipeTransform, ExceptionFilter, ValidationPipe, class-validator, class-transformer
When to use:
- Building structured backend APIs with TypeScript and dependency injection
- Applications requiring modular architecture with clear separation of concerns
- REST APIs with declarative validation, authentication, and role-based access
- Projects needing the guard/interceptor/pipe/filter request lifecycle
When NOT to use:
- Simple scripts or serverless functions that don't need a framework
- Projects where Express/Fastify alone is sufficient (no DI, no modules needed)
- Frontend code
Detailed Resources:
- examples/core.md — Feature modules, CRUD, DTOs, dynamic modules, exception filters, custom providers
- examples/database.md — NestJS DI patterns for database integration, transactions
- examples/auth.md — Passport.js integration, JWT strategy, auth guards, RBAC
- examples/testing.md — Unit testing with
Test.createTestingModule, e2e with supertest - examples/advanced.md — Interceptors, custom pipes, custom decorators, config, CQRS, Swagger
- reference.md — CLI commands, project structure, decorator tables, decision frameworks
<decision_framework>
Decision Framework
Request Lifecycle
Incoming Request
→ Middleware (logging, CORS, body parsing)
→ Guards (authentication, authorization)
→ Interceptors (pre-handler: transform request, start timing)
→ Pipes (validation, transformation)
→ Route Handler (controller method)
→ Interceptors (post-handler: transform response, log timing)
→ Exception Filters (catch and format errors)
→ Response
Which Layer to Use
Need to process raw request before routing?
├─ YES → Middleware (logging, CORS, rate limiting)
└─ NO → Does it decide allow/deny for a route?
├─ YES → Guard (auth, roles, permissions)
└─ NO → Does it transform/validate input data?
├─ YES → Pipe (validation, type coercion)
└─ NO → Does it wrap handler execution?
├─ YES → Interceptor (timing, caching, response mapping)
└─ NO → Does it handle errors?
├─ YES → Exception Filter
└─ NO → Put it in the service layer
Module Organization
Is this a cross-cutting concern (auth, config, logging)?
├─ YES → Global module or shared module
└─ NO → Is it a business feature (users, orders, products)?
├─ YES → Feature module (users.module.ts)
└─ NO → Is it infrastructure (database, cache, queue)?
├─ YES → Infrastructure module
└─ NO → Part of the closest feature module
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Putting business logic in controllers instead of services
- Missing
@Injectable()on services (DI fails silently at runtime) - Not enabling
ValidationPipeglobally (DTOs are not validated) - Using
anyfor request body instead of typed DTOs - Instantiating services with
newinstead of constructor injection - Throwing raw
Errorinstead of NestJS HTTP exceptions (produces 500 instead of proper status)
Medium Priority Issues:
- Not exporting services from modules (other modules can't import them)
- Importing the entire module when you only need one service
- Missing
whitelist: trueon ValidationPipe (mass-assignment vulnerability) - Using
@Res()decorator outside streaming scenarios (opts out of NestJS response handling) - Not using
PartialType/PickType/OmitTypefor update DTOs (duplicated validation)
Common Mistakes:
- Circular module dependencies — restructure with
forwardRef()or extract shared logic - Forgetting to register providers in the module — service injection fails at runtime
- Using synchronous guards for async operations — return
Promise<boolean>orObservable<boolean> - Not handling all exception types in custom filters — always have a catch-all for unknown errors
Gotchas and Edge Cases:
@UseGuards(AuthGuard)takes a class reference, not an instance — NestJS instantiates via DIValidationPipewithtransform: trueconverts query params to their declared types automatically- Guards execute AFTER middleware but BEFORE interceptors and pipes
@Catch()with no arguments catches ALL exceptions, not just HttpExceptionIntrinsicException(NestJS 11) throws without framework auto-logging — useful for expected flow control- NestJS 11: Termination lifecycle hooks (
OnModuleDestroy,OnApplicationShutdown) now execute in reverse order - NestJS 11: Express v5 requires named wildcards (
/*splatinstead of/*) - NestJS 11: SWC is a supported opt-in compiler via
nest-cli.json("builder": "swc") — 20x faster builds than tsc - NestJS 11:
ParseDatePipeis now built-in — no need for custom date parsing pipes - Request-scoped providers (
Scope.REQUEST) affect performance — use only when needed forwardRef()should be a last resort — circular deps usually signal a design issue
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use @Injectable() on every service and register it in the module providers array)
(You MUST enable ValidationPipe globally with whitelist: true and forbidNonWhitelisted: true)
(You MUST use DTOs with class-validator decorators for ALL request body validation — never validate manually in controllers)
(You MUST throw NestJS built-in HTTP exceptions (NotFoundException, BadRequestException, etc.) — never send raw status codes)
(You MUST use constructor injection for dependencies — never instantiate services manually with new)
Failure to follow these rules will produce unvalidated, untestable NestJS code with broken dependency injection.
</critical_reminders>
Files (skills)
-
examples
-
advanced.md 15.7 KB
# NestJS Advanced Patterns > Interceptors, pipes, custom decorators, and CQRS patterns. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern 1: Interceptors Interceptors wrap the handler execution pipeline. They can transform the response, add logging, implement caching, or handle errors. ### Good Example — Response Wrapping Interceptor ```typescript // interceptors/transform-response.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from "@nestjs/common"; import { Observable } from "rxjs"; import { map } from "rxjs/operators"; interface ApiResponse<T> { success: boolean; data: T; timestamp: string; } @Injectable() export class TransformResponseInterceptor<T> implements NestInterceptor< T, ApiResponse<T> > { intercept( context: ExecutionContext, next: CallHandler<T>, ): Observable<ApiResponse<T>> { return next.handle().pipe( map((data) => ({ success: true, data, timestamp: new Date().toISOString(), })), ); } } ``` **Why good:** Wraps all responses in a consistent envelope, uses RxJS `map` operator, typed with generics, implements `NestInterceptor` interface properly ### Good Example — Logging and Timing Interceptor ```typescript // interceptors/logging.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger, } from "@nestjs/common"; import { Observable } from "rxjs"; import { tap } from "rxjs/operators"; import type { Request } from "express"; const SLOW_REQUEST_THRESHOLD_MS = 3000; @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { const request = context.switchToHttp().getRequest<Request>(); const { method, url } = request; const now = Date.now(); return next.handle().pipe( tap(() => { const duration = Date.now() - now; const logMessage = `${method} ${url} — ${duration}ms`; if (duration > SLOW_REQUEST_THRESHOLD_MS) { this.logger.warn(`SLOW: ${logMessage}`); } else { this.logger.log(logMessage); } }), ); } } ``` **Why good:** Named constant for slow threshold, warns on slow requests, uses `tap` (side effect without transforming data), scoped Logger ### Good Example — Cache Interceptor ```typescript // interceptors/cache.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from "@nestjs/common"; import { Observable, of } from "rxjs"; import { tap } from "rxjs/operators"; import type { Request } from "express"; const CACHE_TTL_MS = 60_000; // 1 minute interface CacheEntry { data: unknown; expiry: number; } @Injectable() export class SimpleCacheInterceptor implements NestInterceptor { private readonly cache = new Map<string, CacheEntry>(); intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { const request = context.switchToHttp().getRequest<Request>(); // Only cache GET requests if (request.method !== "GET") { return next.handle(); } const cacheKey = request.url; const cached = this.cache.get(cacheKey); if (cached && cached.expiry > Date.now()) { return of(cached.data); } return next.handle().pipe( tap((data) => { this.cache.set(cacheKey, { data, expiry: Date.now() + CACHE_TTL_MS, }); }), ); } } ``` **Why good:** Only caches GET requests, TTL-based expiry, `of()` returns cached value as Observable, named constant for TTL ### Applying Interceptors ```typescript // Apply to a single route @UseInterceptors(LoggingInterceptor) @Get() findAll() { /* ... */ } // Apply to all routes in a controller @UseInterceptors(LoggingInterceptor) @Controller('users') export class UsersController { /* ... */ } // Apply globally in main.ts app.useGlobalInterceptors(new TransformResponseInterceptor()); // Apply globally with DI (can inject dependencies) @Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, ], }) export class AppModule {} ``` --- ## Pattern 2: Custom Pipes Pipes transform and validate input data before it reaches the handler. ### Good Example — Using Built-in ParseDatePipe (NestJS 11+) ```typescript // NestJS 11 includes ParseDatePipe — no custom pipe needed import { ParseDatePipe } from '@nestjs/common'; @Get('events') findByDate(@Query('date', ParseDatePipe) date: Date) { return this.eventsService.findByDate(date); } ``` **Why good:** Built-in pipe handles validation and transformation, throws `BadRequestException` for invalid dates, no custom code needed ### Good Example — Custom Pipe (for domain-specific validation) ```typescript // pipes/parse-positive-int.pipe.ts import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; @Injectable() export class ParsePositiveIntPipe implements PipeTransform<string, number> { transform(value: string): number { const num = parseInt(value, 10); if (isNaN(num) || num <= 0) { throw new BadRequestException( `"${value}" is not a valid positive integer.`, ); } return num; } } // Usage @Get('items') findByPage(@Query('page', ParsePositiveIntPipe) page: number) { return this.itemsService.findPage(page); } ``` **Why good:** Custom pipe for domain validation beyond built-ins, typed input/output with `PipeTransform<string, number>`, descriptive error ### Good Example — Enum Validation Pipe ```typescript // pipes/parse-enum.pipe.ts import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; @Injectable() export class ParseEnumPipe<T extends Record<string, string>> implements PipeTransform<string, string> { constructor(private readonly enumType: T) {} transform(value: string): string { const enumValues = Object.values(this.enumType); if (!enumValues.includes(value)) { throw new BadRequestException( `"${value}" is not valid. Expected one of: ${enumValues.join(', ')}`, ); } return value; } } // Usage enum OrderStatus { Pending = 'pending', Processing = 'processing', Shipped = 'shipped', Delivered = 'delivered', } @Get() findByStatus( @Query('status', new ParseEnumPipe(OrderStatus)) status: string, ) { return this.ordersService.findByStatus(status); } ``` **Why good:** Generic enum validation, descriptive error message lists valid values, reusable across different enums --- ## Pattern 3: Custom Decorators ### Good Example — Combined Decorators ```typescript // decorators/api-paginated.decorator.ts import { applyDecorators, Get, UseInterceptors } from '@nestjs/common'; import { TransformResponseInterceptor } from '../interceptors/transform-response.interceptor'; export function ApiPaginated(path?: string) { return applyDecorators( Get(path), UseInterceptors(TransformResponseInterceptor), ); } // Usage — replaces @Get() + @UseInterceptors(...) @ApiPaginated() findAll(@Query() query: QueryProductsDto) { return this.productsService.findAll(query); } ``` **Why good:** `applyDecorators` composes multiple decorators, reduces boilerplate on controller methods, single source of truth for endpoint configuration ### Good Example — Param Decorator with Validation ```typescript // decorators/user-agent.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import type { Request } from 'express'; export const UserAgent = createParamDecorator( (_data: unknown, ctx: ExecutionContext): string => { const request = ctx.switchToHttp().getRequest<Request>(); return request.headers['user-agent'] ?? 'unknown'; }, ); // Usage @Get() findAll(@UserAgent() userAgent: string) { this.logger.log(`Request from: ${userAgent}`); return this.productsService.findAll(); } ``` ### Good Example — Method Decorator for Metadata ```typescript // decorators/cache-ttl.decorator.ts import { SetMetadata } from '@nestjs/common'; const CACHE_TTL_KEY = 'cacheTtl'; const DEFAULT_CACHE_TTL_MS = 60_000; export const CacheTtl = (ttlMs: number = DEFAULT_CACHE_TTL_MS) => SetMetadata(CACHE_TTL_KEY, ttlMs); // Usage @Get('popular') @CacheTtl(300_000) // 5 minutes getPopularProducts() { return this.productsService.getPopular(); } ``` **Why good:** Named constant for default TTL and metadata key, `SetMetadata` stores value for interceptor/guard to read via `Reflector`, clean API --- ## Pattern 4: Config Module Patterns ### Good Example — Typed Configuration ```typescript // config/database.config.ts import { registerAs } from "@nestjs/config"; export const databaseConfig = registerAs("database", () => ({ host: process.env.DB_HOST ?? "localhost", port: parseInt(process.env.DB_PORT ?? "5432", 10), username: process.env.DB_USER ?? "postgres", password: process.env.DB_PASSWORD ?? "", name: process.env.DB_NAME ?? "app", })); ``` ```typescript // config/app.config.ts import { registerAs } from "@nestjs/config"; const DEFAULT_PORT = 3000; export const appConfig = registerAs("app", () => ({ port: parseInt(process.env.PORT ?? String(DEFAULT_PORT), 10), nodeEnv: process.env.NODE_ENV ?? "development", apiPrefix: process.env.API_PREFIX ?? "api/v1", })); ``` ```typescript // app.module.ts import { ConfigModule } from "@nestjs/config"; import { databaseConfig } from "./config/database.config"; import { appConfig } from "./config/app.config"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [databaseConfig, appConfig], }), ], }) export class AppModule {} ``` ```typescript // Usage with injection import { Inject, Injectable } from "@nestjs/common"; import { ConfigType } from "@nestjs/config"; import { databaseConfig } from "../config/database.config"; @Injectable() export class DatabaseService { constructor( @Inject(databaseConfig.KEY) private readonly dbConfig: ConfigType<typeof databaseConfig>, ) { // Fully typed: this.dbConfig.host, this.dbConfig.port, etc. } } ``` **Why good:** `registerAs` creates namespaced config, `ConfigType` provides full type inference, `isGlobal: true` avoids importing ConfigModule everywhere, defaults for development --- ## Pattern 5: CQRS Pattern For complex domains, separate read and write operations. ### Command ```typescript // orders/commands/create-order.command.ts export class CreateOrderCommand { constructor( public readonly userId: number, public readonly items: Array<{ productId: number; quantity: number }>, ) {} } ``` ### Command Handler ```typescript // orders/commands/handlers/create-order.handler.ts import { CommandHandler, ICommandHandler, EventBus } from "@nestjs/cqrs"; import { CreateOrderCommand } from "../create-order.command"; import { OrderCreatedEvent } from "../../events/order-created.event"; @CommandHandler(CreateOrderCommand) export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> { constructor( private readonly ordersRepository: OrdersRepository, private readonly eventBus: EventBus, ) {} async execute(command: CreateOrderCommand) { const order = await this.ordersRepository.create({ userId: command.userId, items: command.items, status: "pending", }); this.eventBus.publish(new OrderCreatedEvent(order.id, command.userId)); return order; } } ``` ### Event and Event Handler ```typescript // orders/events/order-created.event.ts export class OrderCreatedEvent { constructor( public readonly orderId: number, public readonly userId: number, ) {} } ``` ```typescript // orders/events/handlers/order-created.handler.ts import { EventsHandler, IEventHandler } from "@nestjs/cqrs"; import { OrderCreatedEvent } from "../order-created.event"; import { NotificationsService } from "../../../notifications/notifications.service"; @EventsHandler(OrderCreatedEvent) export class OrderCreatedHandler implements IEventHandler<OrderCreatedEvent> { constructor(private readonly notificationsService: NotificationsService) {} async handle(event: OrderCreatedEvent) { await this.notificationsService.sendOrderConfirmation( event.userId, event.orderId, ); } } ``` ### Controller Using CQRS ```typescript // orders/orders.controller.ts import { Controller, Post, Body } from "@nestjs/common"; import { CommandBus, QueryBus } from "@nestjs/cqrs"; import { CreateOrderCommand } from "./commands/create-order.command"; import { GetOrderQuery } from "./queries/get-order.query"; import { CurrentUser } from "../auth/decorators/current-user.decorator"; @Controller("orders") export class OrdersController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {} @Post() createOrder(@CurrentUser("id") userId: number, @Body() dto: CreateOrderDto) { return this.commandBus.execute(new CreateOrderCommand(userId, dto.items)); } @Get(":id") getOrder(@Param("id", ParseIntPipe) id: number) { return this.queryBus.execute(new GetOrderQuery(id)); } } ``` **Why good:** Clean separation of read/write concerns, event-driven side effects (notifications), controller doesn't know about business logic details, commands and queries are plain classes (easy to test) **When to use:** Complex domains with many side effects, systems where read and write models differ significantly, event-sourced architectures **When not to use:** Simple CRUD applications (overkill), small teams (unnecessary complexity) --- ## Pattern 6: Swagger/OpenAPI Documentation ### Setup ```typescript // main.ts import { NestFactory } from "@nestjs/core"; import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; import { AppModule } from "./app.module"; const PORT = 3000; async function bootstrap() { const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() .setTitle("My API") .setDescription("API documentation") .setVersion("1.0") .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup("api/docs", app, document); await app.listen(PORT); } bootstrap(); ``` ### DTO with Swagger Decorators ```typescript // users/dto/create-user.dto.ts import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsEmail, IsString, MinLength, IsOptional } from "class-validator"; const MIN_PASSWORD_LENGTH = 8; export class CreateUserDto { @ApiProperty({ example: "alice@example.com" }) @IsEmail() email: string; @ApiProperty({ minLength: MIN_PASSWORD_LENGTH }) @IsString() @MinLength(MIN_PASSWORD_LENGTH) password: string; @ApiProperty({ example: "Alice" }) @IsString() name: string; @ApiPropertyOptional({ example: "user", enum: ["user", "admin"] }) @IsOptional() @IsString() role?: string; } ``` ### Controller with Swagger Decorators ```typescript import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, } from "@nestjs/swagger"; @ApiTags("users") @ApiBearerAuth() @Controller("users") export class UsersController { @ApiOperation({ summary: "Create a new user" }) @ApiResponse({ status: 201, description: "User created successfully" }) @ApiResponse({ status: 400, description: "Validation failed" }) @ApiResponse({ status: 409, description: "Email already exists" }) @Post() create(@Body() dto: CreateUserDto) { return this.usersService.create(dto); } } ``` **Why good:** Auto-generated API docs at `/api/docs`, `@ApiProperty` examples appear in Swagger UI, `@ApiResponse` documents all possible outcomes, `@ApiBearerAuth` adds auth to Swagger UI --- _For core patterns, see [core.md](core.md). For database patterns, see [database.md](database.md). For auth patterns, see [auth.md](auth.md). For testing, see [testing.md](testing.md)._ -
auth.md 10.4 KB
# NestJS Authentication Examples > Passport.js integration, JWT strategy, and auth guards for NestJS. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern 1: JWT Authentication with Passport ### Auth Module Setup ```typescript // auth/auth.module.ts import { Module } from "@nestjs/common"; import { JwtModule } from "@nestjs/jwt"; import { PassportModule } from "@nestjs/passport"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { AuthService } from "./auth.service"; import { AuthController } from "./auth.controller"; import { JwtStrategy } from "./strategies/jwt.strategy"; import { LocalStrategy } from "./strategies/local.strategy"; import { UsersModule } from "../users/users.module"; const JWT_EXPIRATION = "1h"; @Module({ imports: [ UsersModule, PassportModule, JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ secret: configService.get<string>("JWT_SECRET"), signOptions: { expiresIn: JWT_EXPIRATION }, }), }), ], controllers: [AuthController], providers: [AuthService, JwtStrategy, LocalStrategy], exports: [AuthService], }) export class AuthModule {} ``` **Why good:** Async JWT config reads secret from environment, named constant for expiration, imports `UsersModule` for user lookup, exports `AuthService` for use by other modules ### Auth Service ```typescript // auth/auth.service.ts import { Injectable, UnauthorizedException } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; import * as bcrypt from "bcrypt"; import { UsersService } from "../users/users.service"; const BCRYPT_SALT_ROUNDS = 10; interface JwtPayload { sub: number; email: string; role: string; } interface TokenResponse { accessToken: string; } @Injectable() export class AuthService { constructor( private readonly usersService: UsersService, private readonly jwtService: JwtService, ) {} async validateUser(email: string, password: string) { const user = await this.usersService.findByEmail(email); if (!user) { throw new UnauthorizedException("Invalid credentials"); } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { throw new UnauthorizedException("Invalid credentials"); } // Return user without password const { password: _password, ...result } = user; return result; } async login(user: { id: number; email: string; role: string; }): Promise<TokenResponse> { const payload: JwtPayload = { sub: user.id, email: user.email, role: user.role, }; return { accessToken: this.jwtService.sign(payload), }; } async hashPassword(password: string): Promise<string> { return bcrypt.hash(password, BCRYPT_SALT_ROUNDS); } } ``` **Why good:** Named constant for salt rounds, generic error message ("Invalid credentials") prevents user enumeration, strips password from returned user, typed JWT payload ### Local Strategy (Username/Password) ```typescript // auth/strategies/local.strategy.ts import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { Strategy } from "passport-local"; import { AuthService } from "../auth.service"; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private readonly authService: AuthService) { super({ usernameField: "email" }); // Use email instead of username } async validate(email: string, password: string) { return this.authService.validateUser(email, password); } } ``` **Why good:** `usernameField: 'email'` configures Passport to use email, `validate()` delegates to AuthService, return value is attached to `request.user` ### JWT Strategy (Token Verification) ```typescript // auth/strategies/jwt.strategy.ts import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { ExtractJwt, Strategy } from "passport-jwt"; import { ConfigService } from "@nestjs/config"; interface JwtPayload { sub: number; email: string; role: string; } @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(configService: ConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get<string>("JWT_SECRET"), }); } async validate(payload: JwtPayload) { // Return value is attached to request.user return { id: payload.sub, email: payload.email, role: payload.role, }; } } ``` **Why good:** Extracts token from `Authorization: Bearer <token>` header, secret from config (not hardcoded), `validate()` maps JWT claims to user object ### Auth Controller ```typescript // auth/auth.controller.ts import { Controller, Post, Body, UseGuards, Request, HttpCode, HttpStatus, } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { AuthService } from "./auth.service"; import { LoginDto } from "./dto/login.dto"; @Controller("auth") export class AuthController { constructor(private readonly authService: AuthService) {} @Post("login") @UseGuards(AuthGuard("local")) @HttpCode(HttpStatus.OK) async login( @Request() req: { user: { id: number; email: string; role: string } }, ) { return this.authService.login(req.user); } } ``` ### Login DTO ```typescript // auth/dto/login.dto.ts import { IsEmail, IsString, MinLength } from "class-validator"; const MIN_PASSWORD_LENGTH = 8; export class LoginDto { @IsEmail() email: string; @IsString() @MinLength(MIN_PASSWORD_LENGTH) password: string; } ``` --- ## Pattern 2: Auth Guards for Route Protection ### JWT Auth Guard ```typescript // auth/guards/jwt-auth.guard.ts import { Injectable, ExecutionContext } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { Reflector } from "@nestjs/core"; const IS_PUBLIC_KEY = "isPublic"; @Injectable() export class JwtAuthGuard extends AuthGuard("jwt") { constructor(private readonly reflector: Reflector) { super(); } canActivate(context: ExecutionContext) { // Check for @Public() decorator const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) { return true; } return super.canActivate(context); } } ``` ### Public Decorator ```typescript // auth/decorators/public.decorator.ts import { SetMetadata } from "@nestjs/common"; const IS_PUBLIC_KEY = "isPublic"; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); ``` ### Current User Decorator ```typescript // auth/decorators/current-user.decorator.ts import { createParamDecorator, ExecutionContext } from "@nestjs/common"; interface AuthUser { id: number; email: string; role: string; } export const CurrentUser = createParamDecorator( (data: keyof AuthUser | undefined, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user as AuthUser; return data ? user[data] : user; }, ); ``` ### Global Guard Registration ```typescript // app.module.ts import { Module } from "@nestjs/common"; import { APP_GUARD } from "@nestjs/core"; import { JwtAuthGuard } from "./auth/guards/jwt-auth.guard"; @Module({ providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard, }, ], }) export class AppModule {} ``` ### Usage in Controllers ```typescript // users/users.controller.ts import { Controller, Get, Param, ParseIntPipe } from "@nestjs/common"; import { Public } from "../auth/decorators/public.decorator"; import { CurrentUser } from "../auth/decorators/current-user.decorator"; import { Roles } from "../auth/decorators/roles.decorator"; @Controller("users") export class UsersController { constructor(private readonly usersService: UsersService) {} // Public endpoint — no auth required @Public() @Get("count") getUserCount() { return this.usersService.count(); } // Protected — requires valid JWT (global guard) @Get("me") getProfile(@CurrentUser() user: AuthUser) { return this.usersService.findOne(user.id); } // Protected — requires valid JWT + specific role @Get(":id") @Roles("admin") findOne(@Param("id", ParseIntPipe) id: number) { return this.usersService.findOne(id); } } ``` **Why good:** Global guard protects all routes by default, `@Public()` opts out specific endpoints, `@CurrentUser()` extracts typed user from request, `@Roles()` adds role-based access control --- ## Pattern 3: Role-Based Access Control (RBAC) ### Roles Guard ```typescript // auth/guards/roles.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException, } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; const ROLES_KEY = "roles"; @Injectable() export class RolesGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>( ROLES_KEY, [context.getHandler(), context.getClass()], ); // No roles required — allow access if (!requiredRoles || requiredRoles.length === 0) { return true; } const { user } = context.switchToHttp().getRequest(); if (!user) { throw new ForbiddenException("No user found on request"); } const hasRole = requiredRoles.includes(user.role); if (!hasRole) { throw new ForbiddenException( `Required roles: ${requiredRoles.join(", ")}`, ); } return true; } } ``` ### Roles Decorator ```typescript // auth/decorators/roles.decorator.ts import { SetMetadata } from "@nestjs/common"; const ROLES_KEY = "roles"; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); ``` ### Register as Global Guard ```typescript // app.module.ts import { APP_GUARD } from "@nestjs/core"; @Module({ providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: RolesGuard }, ], }) export class AppModule {} ``` **Why good:** Global guards apply to all routes automatically, `Reflector` reads metadata from decorators, guards compose in order (JWT first, then roles), explicit error messages for debugging --- _For database patterns, see [database.md](database.md). For testing, see [testing.md](testing.md). For advanced patterns, see [advanced.md](advanced.md)._ -
core.md 14.7 KB
# NestJS Core Examples > Complete code examples for NestJS modules, controllers, services, DTOs, and exception handling. See [SKILL.md](../SKILL.md) for core concepts. **More examples:** See [database.md](database.md), [auth.md](auth.md), [testing.md](testing.md), and [advanced.md](advanced.md). --- ## Pattern 1: Feature Module — Complete CRUD A complete feature module with controller, service, DTOs, and proper error handling. ### Module ```typescript // products/products.module.ts import { Module } from "@nestjs/common"; import { ProductsController } from "./products.controller"; import { ProductsService } from "./products.service"; @Module({ controllers: [ProductsController], providers: [ProductsService], exports: [ProductsService], }) export class ProductsModule {} ``` ### DTOs ```typescript // products/dto/create-product.dto.ts import { IsString, IsNumber, IsPositive, IsOptional, IsArray, MaxLength, Min, } from "class-validator"; import { Type } from "class-transformer"; const MAX_NAME_LENGTH = 200; const MAX_DESCRIPTION_LENGTH = 2000; const MIN_PRICE = 0.01; export class CreateProductDto { @IsString() @MaxLength(MAX_NAME_LENGTH) name: string; @IsOptional() @IsString() @MaxLength(MAX_DESCRIPTION_LENGTH) description?: string; @IsNumber({ maxDecimalPlaces: 2 }) @IsPositive() @Min(MIN_PRICE) price: number; @IsOptional() @IsArray() @IsString({ each: true }) tags?: string[]; @IsOptional() @IsNumber() @Min(0) @Type(() => Number) stock?: number; } ``` ```typescript // products/dto/update-product.dto.ts import { PartialType } from "@nestjs/mapped-types"; import { CreateProductDto } from "./create-product.dto"; export class UpdateProductDto extends PartialType(CreateProductDto) {} ``` ```typescript // products/dto/query-products.dto.ts import { IsOptional, IsString, IsNumber, Min, IsEnum } from "class-validator"; import { Type } from "class-transformer"; const DEFAULT_PAGE = 1; const DEFAULT_LIMIT = 20; const MAX_LIMIT = 100; enum SortOrder { Asc = "asc", Desc = "desc", } export class QueryProductsDto { @IsOptional() @IsString() search?: string; @IsOptional() @Type(() => Number) @IsNumber() @Min(1) page?: number = DEFAULT_PAGE; @IsOptional() @Type(() => Number) @IsNumber() @Min(1) limit?: number = DEFAULT_LIMIT; @IsOptional() @IsEnum(SortOrder) sort?: SortOrder = SortOrder.Desc; } ``` **Why good:** Named constants for limits, `@Type(() => Number)` converts query string params, enum for sort order, defaults in class properties, `PartialType` reuses create DTO ### Service ```typescript // products/products.service.ts import { Injectable, NotFoundException, ConflictException, Logger, } from "@nestjs/common"; import type { CreateProductDto } from "./dto/create-product.dto"; import type { UpdateProductDto } from "./dto/update-product.dto"; import type { QueryProductsDto } from "./dto/query-products.dto"; interface Product { id: number; name: string; description?: string; price: number; tags: string[]; stock: number; createdAt: Date; updatedAt: Date; } const DEFAULT_STOCK = 0; @Injectable() export class ProductsService { private readonly logger = new Logger(ProductsService.name); private readonly products: Product[] = []; private nextId = 1; findAll(query: QueryProductsDto) { let results = [...this.products]; // Filter by search if (query.search) { const searchLower = query.search.toLowerCase(); results = results.filter( (p) => p.name.toLowerCase().includes(searchLower) || p.description?.toLowerCase().includes(searchLower), ); } // Sort results.sort((a, b) => { const modifier = query.sort === "asc" ? 1 : -1; return (a.createdAt.getTime() - b.createdAt.getTime()) * modifier; }); // Paginate const page = query.page ?? 1; const limit = query.limit ?? 20; const start = (page - 1) * limit; const items = results.slice(start, start + limit); return { items, total: results.length, page, limit, totalPages: Math.ceil(results.length / limit), }; } findOne(id: number): Product { const product = this.products.find((p) => p.id === id); if (!product) { throw new NotFoundException(`Product with id ${id} not found`); } return product; } create(dto: CreateProductDto): Product { const existing = this.products.find((p) => p.name === dto.name); if (existing) { throw new ConflictException(`Product "${dto.name}" already exists`); } const now = new Date(); const product: Product = { id: this.nextId++, name: dto.name, description: dto.description, price: dto.price, tags: dto.tags ?? [], stock: dto.stock ?? DEFAULT_STOCK, createdAt: now, updatedAt: now, }; this.products.push(product); this.logger.log(`Created product: ${product.name} (id: ${product.id})`); return product; } update(id: number, dto: UpdateProductDto): Product { const product = this.findOne(id); Object.assign(product, dto, { updatedAt: new Date() }); this.logger.log(`Updated product: ${product.name} (id: ${product.id})`); return product; } remove(id: number): void { const index = this.products.findIndex((p) => p.id === id); if (index === -1) { throw new NotFoundException(`Product with id ${id} not found`); } const [removed] = this.products.splice(index, 1); this.logger.log(`Removed product: ${removed.name} (id: ${removed.id})`); } } ``` **Why good:** Logger scoped to class name, paginated results with metadata, NestJS exceptions for error cases, typed DTOs via `import type`, named constant for default stock ### Controller ```typescript // products/products.controller.ts import { Controller, Get, Post, Put, Delete, Body, Param, Query, ParseIntPipe, HttpCode, HttpStatus, } from "@nestjs/common"; import { ProductsService } from "./products.service"; import { CreateProductDto } from "./dto/create-product.dto"; import { UpdateProductDto } from "./dto/update-product.dto"; import { QueryProductsDto } from "./dto/query-products.dto"; @Controller("products") export class ProductsController { constructor(private readonly productsService: ProductsService) {} @Get() findAll(@Query() query: QueryProductsDto) { return this.productsService.findAll(query); } @Get(":id") findOne(@Param("id", ParseIntPipe) id: number) { return this.productsService.findOne(id); } @Post() @HttpCode(HttpStatus.CREATED) create(@Body() createProductDto: CreateProductDto) { return this.productsService.create(createProductDto); } @Put(":id") update( @Param("id", ParseIntPipe) id: number, @Body() updateProductDto: UpdateProductDto, ) { return this.productsService.update(id, updateProductDto); } @Delete(":id") @HttpCode(HttpStatus.NO_CONTENT) remove(@Param("id", ParseIntPipe) id: number) { return this.productsService.remove(id); } } ``` **Why good:** Controller is thin (delegates all logic to service), query DTO handles pagination/search/sort, `ParseIntPipe` validates IDs, explicit HTTP status codes --- ## Pattern 2: Dynamic Modules Dynamic modules accept configuration at import time. ### Good Example — Configurable Module ```typescript // mailer/mailer.module.ts import { Module, DynamicModule } from "@nestjs/common"; import { MailerService } from "./mailer.service"; const MAILER_OPTIONS = "MAILER_OPTIONS"; interface MailerOptions { host: string; port: number; from: string; } @Module({}) export class MailerModule { static forRoot(options: MailerOptions): DynamicModule { return { module: MailerModule, global: true, providers: [ { provide: MAILER_OPTIONS, useValue: options, }, MailerService, ], exports: [MailerService], }; } } ``` ```typescript // mailer/mailer.service.ts import { Injectable, Inject } from "@nestjs/common"; const MAILER_OPTIONS = "MAILER_OPTIONS"; interface MailerOptions { host: string; port: number; from: string; } @Injectable() export class MailerService { constructor( @Inject(MAILER_OPTIONS) private readonly options: MailerOptions, ) {} async sendEmail(to: string, subject: string, body: string) { // Use this.options to send email via your mail transport } } ``` ```typescript // app.module.ts — Usage @Module({ imports: [ MailerModule.forRoot({ host: "smtp.example.com", port: 587, from: "noreply@example.com", }), ], }) export class AppModule {} ``` **Why good:** `forRoot()` pattern for configurable modules, `global: true` makes it available everywhere, token-based injection for options, clear options interface --- ## Pattern 3: Global Exception Filter ### Good Example — Structured Error Responses ```typescript // filters/all-exceptions.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger, } from "@nestjs/common"; import type { Request, Response } from "express"; interface ErrorResponse { statusCode: number; message: string | string[]; error: string; timestamp: string; path: string; } @Catch() export class AllExceptionsFilter implements ExceptionFilter { private readonly logger = new Logger(AllExceptionsFilter.name); catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); let status: number; let message: string | string[]; let error: string; if (exception instanceof HttpException) { status = exception.getStatus(); const exceptionResponse = exception.getResponse(); if (typeof exceptionResponse === "object" && exceptionResponse !== null) { const responseObj = exceptionResponse as Record<string, unknown>; message = (responseObj.message as string | string[]) ?? exception.message; error = (responseObj.error as string) ?? "Error"; } else { message = exception.message; error = "Error"; } } else { status = HttpStatus.INTERNAL_SERVER_ERROR; message = "Internal server error"; error = "Internal Server Error"; // Log unexpected errors with stack trace this.logger.error( `Unexpected error: ${exception instanceof Error ? exception.message : "Unknown"}`, exception instanceof Error ? exception.stack : undefined, ); } const errorResponse: ErrorResponse = { statusCode: status, message, error, timestamp: new Date().toISOString(), path: request.url, }; response.status(status).json(errorResponse); } } ``` ```typescript // main.ts — Register globally import { NestFactory } from "@nestjs/core"; import { ValidationPipe } from "@nestjs/common"; import { AppModule } from "./app.module"; import { AllExceptionsFilter } from "./filters/all-exceptions.filter"; const PORT = 3000; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); app.useGlobalFilters(new AllExceptionsFilter()); await app.listen(PORT); } bootstrap(); ``` **Why good:** Catches all exceptions (not just HttpException), preserves validation error arrays from ValidationPipe, logs unexpected errors with stack trace, consistent response shape --- ## Pattern 4: Nested DTOs and Validation Groups ### Good Example — Complex Validation ```typescript // orders/dto/create-order.dto.ts import { IsString, IsNumber, IsArray, ValidateNested, ArrayMinSize, IsPositive, IsOptional, } from "class-validator"; import { Type } from "class-transformer"; const MIN_ORDER_ITEMS = 1; class OrderItemDto { @IsString() productId: string; @IsNumber() @IsPositive() quantity: number; } class ShippingAddressDto { @IsString() street: string; @IsString() city: string; @IsString() state: string; @IsString() zipCode: string; @IsOptional() @IsString() country?: string; } export class CreateOrderDto { @IsArray() @ArrayMinSize(MIN_ORDER_ITEMS) @ValidateNested({ each: true }) @Type(() => OrderItemDto) items: OrderItemDto[]; @ValidateNested() @Type(() => ShippingAddressDto) shippingAddress: ShippingAddressDto; @IsOptional() @IsString() notes?: string; } ``` **Why good:** `@ValidateNested` with `@Type()` validates nested objects, `{ each: true }` validates each array element, named constant for minimum items, complex real-world shape ### Bad Example — Flat DTO for Nested Data ```typescript // BAD: Flattening nested data into one level export class CreateOrderDto { @IsString() productId: string; @IsNumber() quantity: number; @IsString() shippingStreet: string; @IsString() shippingCity: string; // ... becomes unwieldy for complex data } ``` **Why bad:** Flat DTOs don't represent the actual data shape, can't validate nested arrays, becomes unmanageable with complex payloads --- ## Pattern 5: Custom Providers ### Good Example — Factory, Value, and Class Providers ```typescript // config/providers.ts import { ConfigService } from "@nestjs/config"; // Value provider — static configuration export const APP_NAME_PROVIDER = { provide: "APP_NAME", useValue: "My NestJS App", }; // Class provider — swap implementations export const LOGGER_PROVIDER = { provide: "LOGGER", useClass: process.env.NODE_ENV === "production" ? JsonLogger : ConsoleLogger, }; // Factory provider — async initialization with dependencies export const DATABASE_PROVIDER = { provide: "DATABASE_CONNECTION", useFactory: async (configService: ConfigService) => { const host = configService.get<string>("DB_HOST"); const port = configService.get<number>("DB_PORT"); return createDatabaseConnection({ host, port }); }, inject: [ConfigService], }; ``` ```typescript // Usage — Inject with @Inject token import { Injectable, Inject } from "@nestjs/common"; @Injectable() export class AppService { constructor( @Inject("APP_NAME") private readonly appName: string, @Inject("DATABASE_CONNECTION") private readonly db: DatabaseConnection, ) {} getAppInfo() { return { name: this.appName, dbConnected: this.db.isConnected() }; } } ``` **Why good:** Value providers for constants, class providers for environment-specific implementations, factory providers for async initialization with injected dependencies, string tokens for non-class providers --- _For database patterns, see [database.md](database.md). For auth patterns, see [auth.md](auth.md). For testing, see [testing.md](testing.md). For advanced patterns, see [advanced.md](advanced.md)._ -
database.md 10.6 KB
# NestJS Database Integration Examples > NestJS DI integration patterns for database access. These examples show how to wrap your ORM in NestJS modules and services — not the ORM API itself. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern 1: Prisma Integration NestJS integrates Prisma via a custom service that wraps `PrismaClient` with lifecycle hooks. ### PrismaService ```typescript // prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy, Logger, } from "@nestjs/common"; import { PrismaClient } from "@prisma/client"; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(PrismaService.name); async onModuleInit() { await this.$connect(); this.logger.log("Prisma connected to database"); } async onModuleDestroy() { await this.$disconnect(); this.logger.log("Prisma disconnected from database"); } } ``` ### PrismaModule ```typescript // prisma/prisma.module.ts import { Module, Global } from "@nestjs/common"; import { PrismaService } from "./prisma.service"; @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` **Why good:** `@Global()` makes PrismaService available everywhere without importing PrismaModule, lifecycle hooks handle connect/disconnect, extends PrismaClient for full API access ### Service Using Prisma ```typescript // users/users.service.ts import { Injectable, NotFoundException, ConflictException, } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { Prisma } from "@prisma/client"; import type { CreateUserDto } from "./dto/create-user.dto"; import type { UpdateUserDto } from "./dto/update-user.dto"; const DEFAULT_PAGE = 1; const DEFAULT_LIMIT = 20; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} async findAll(page = DEFAULT_PAGE, limit = DEFAULT_LIMIT) { const skip = (page - 1) * limit; const [items, total] = await Promise.all([ this.prisma.user.findMany({ skip, take: limit, orderBy: { createdAt: "desc" }, select: { id: true, email: true, name: true, role: true, createdAt: true, }, }), this.prisma.user.count(), ]); return { items, total, page, limit, totalPages: Math.ceil(total / limit), }; } async findOne(id: number) { const user = await this.prisma.user.findUnique({ where: { id }, include: { posts: true }, }); if (!user) { throw new NotFoundException(`User with id ${id} not found`); } return user; } async create(dto: CreateUserDto) { try { return await this.prisma.user.create({ data: dto, }); } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002" ) { throw new ConflictException( `User with email ${dto.email} already exists`, ); } throw error; } } async update(id: number, dto: UpdateUserDto) { try { return await this.prisma.user.update({ where: { id }, data: dto, }); } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025" ) { throw new NotFoundException(`User with id ${id} not found`); } throw error; } } async remove(id: number) { try { await this.prisma.user.delete({ where: { id } }); } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025" ) { throw new NotFoundException(`User with id ${id} not found`); } throw error; } } } ``` **Why good:** Handles Prisma-specific error codes (P2002 for unique constraint, P2025 for not found), `select` for query optimization, `Promise.all` for parallel count + data queries, paginated response --- ## Pattern 2: TypeORM Integration TypeORM provides decorator-based entity definitions and a repository pattern. ### Entity ```typescript // users/entities/user.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany, } from "typeorm"; import { Post } from "../../posts/entities/post.entity"; @Entity("users") export class User { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) email: string; @Column() name: string; @Column({ select: false }) password: string; @Column({ default: "user" }) role: string; @OneToMany(() => Post, (post) => post.author) posts: Post[]; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; } ``` ### Module with TypeORM ```typescript // users/users.module.ts import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { User } from "./entities/user.entity"; import { UsersController } from "./users.controller"; import { UsersService } from "./users.service"; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], exports: [UsersService], }) export class UsersModule {} ``` ### Service with Repository ```typescript // users/users.service.ts import { Injectable, NotFoundException } 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"; @Injectable() export class UsersService { constructor( @InjectRepository(User) private readonly usersRepository: Repository<User>, ) {} async findAll(): Promise<User[]> { return this.usersRepository.find({ order: { createdAt: "DESC" }, }); } async findOne(id: number): Promise<User> { const user = await this.usersRepository.findOne({ where: { id }, relations: ["posts"], }); if (!user) { throw new NotFoundException(`User with id ${id} not found`); } return user; } async findByEmail(email: string): Promise<User | null> { return this.usersRepository.findOne({ where: { email }, select: ["id", "email", "name", "password", "role"], }); } async create(dto: CreateUserDto): Promise<User> { const user = this.usersRepository.create(dto); return this.usersRepository.save(user); } async update(id: number, dto: Partial<CreateUserDto>): Promise<User> { const user = await this.findOne(id); Object.assign(user, dto); return this.usersRepository.save(user); } async remove(id: number): Promise<void> { const result = await this.usersRepository.delete(id); if (result.affected === 0) { throw new NotFoundException(`User with id ${id} not found`); } } } ``` **Why good:** `@InjectRepository` for DI-friendly repository access, `select` for sensitive fields (password), relation loading, `create()` + `save()` pattern for proper entity lifecycle ### Root Module with TypeORM Config ```typescript // app.module.ts import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { ConfigModule, ConfigService } from "@nestjs/config"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (configService: ConfigService) => ({ type: "postgres", host: configService.get("DB_HOST"), port: configService.get<number>("DB_PORT"), username: configService.get("DB_USER"), password: configService.get("DB_PASSWORD"), database: configService.get("DB_NAME"), autoLoadEntities: true, synchronize: configService.get("NODE_ENV") !== "production", }), }), ], }) export class AppModule {} ``` **Why good:** Async config from environment variables, `autoLoadEntities` avoids manual entity registration, `synchronize` disabled in production (use migrations) --- ## Pattern 3: Transactions ### Prisma Transaction ```typescript // orders/orders.service.ts @Injectable() export class OrdersService { constructor(private readonly prisma: PrismaService) {} async createOrder(dto: CreateOrderDto) { return this.prisma.$transaction(async (tx) => { // Create order const order = await tx.order.create({ data: { userId: dto.userId, status: "pending", }, }); // Create order items and update stock for (const item of dto.items) { await tx.orderItem.create({ data: { orderId: order.id, productId: item.productId, quantity: item.quantity, }, }); // Decrement stock const product = await tx.product.update({ where: { id: item.productId }, data: { stock: { decrement: item.quantity } }, }); if (product.stock < 0) { throw new BadRequestException( `Insufficient stock for product ${item.productId}`, ); } } return order; }); } } ``` **Why good:** Interactive transaction ensures atomicity, stock check inside transaction prevents race conditions, error rolls back all changes ### TypeORM Transaction ```typescript @Injectable() export class OrdersService { constructor( @InjectRepository(Order) private readonly ordersRepository: Repository<Order>, private readonly dataSource: DataSource, ) {} async createOrder(dto: CreateOrderDto) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const order = queryRunner.manager.create(Order, { userId: dto.userId, status: "pending", }); await queryRunner.manager.save(order); for (const item of dto.items) { const orderItem = queryRunner.manager.create(OrderItem, { orderId: order.id, ...item, }); await queryRunner.manager.save(orderItem); } await queryRunner.commitTransaction(); return order; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } } ``` **Why good:** Explicit transaction control with commit/rollback, `finally` ensures queryRunner release, manager operations within transaction scope --- _For auth patterns, see [auth.md](auth.md). For testing, see [testing.md](testing.md). For advanced patterns, see [advanced.md](advanced.md)._ -
testing.md 16.6 KB
# NestJS Testing Examples > Unit testing services with mocks, e2e testing with supertest. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern 1: Unit Testing Services Use NestJS `Test.createTestingModule()` to create an isolated testing module with mocked dependencies. ### Good Example — Testing a Service ```typescript // users/users.service.spec.ts import { Test, TestingModule } from "@nestjs/testing"; import { NotFoundException, ConflictException } from "@nestjs/common"; import { UsersService } from "./users.service"; import { PrismaService } from "../prisma/prisma.service"; const MOCK_USER = { id: 1, email: "alice@example.com", name: "Alice", role: "user", createdAt: new Date(), updatedAt: new Date(), }; const MOCK_CREATE_DTO = { email: "bob@example.com", name: "Bob", password: "hashed_password", }; describe("UsersService", () => { let service: UsersService; let prisma: PrismaService; const mockPrismaService = { user: { findUnique: jest.fn(), findMany: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), count: jest.fn(), }, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, { provide: PrismaService, useValue: mockPrismaService, }, ], }).compile(); service = module.get<UsersService>(UsersService); prisma = module.get<PrismaService>(PrismaService); // Reset mocks between tests jest.clearAllMocks(); }); describe("findOne", () => { it("should return a user when found", async () => { mockPrismaService.user.findUnique.mockResolvedValue(MOCK_USER); const result = await service.findOne(1); expect(result).toEqual(MOCK_USER); expect(mockPrismaService.user.findUnique).toHaveBeenCalledWith({ where: { id: 1 }, include: { posts: true }, }); }); it("should throw NotFoundException when user not found", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); await expect(service.findOne(999)).rejects.toThrow(NotFoundException); }); }); describe("create", () => { it("should create and return a new user", async () => { const newUser = { id: 2, ...MOCK_CREATE_DTO, createdAt: new Date(), updatedAt: new Date(), }; mockPrismaService.user.create.mockResolvedValue(newUser); const result = await service.create(MOCK_CREATE_DTO); expect(result).toEqual(newUser); expect(mockPrismaService.user.create).toHaveBeenCalledWith({ data: MOCK_CREATE_DTO, }); }); it("should throw ConflictException on duplicate email", async () => { const prismaError = new Error("Unique constraint failed"); Object.assign(prismaError, { code: "P2002" }); mockPrismaService.user.create.mockRejectedValue(prismaError); await expect(service.create(MOCK_CREATE_DTO)).rejects.toThrow( ConflictException, ); }); }); describe("remove", () => { it("should delete a user", async () => { mockPrismaService.user.delete.mockResolvedValue(MOCK_USER); await service.remove(1); expect(mockPrismaService.user.delete).toHaveBeenCalledWith({ where: { id: 1 }, }); }); it("should throw NotFoundException when deleting non-existent user", async () => { const prismaError = new Error("Record not found"); Object.assign(prismaError, { code: "P2025" }); mockPrismaService.user.delete.mockRejectedValue(prismaError); await expect(service.remove(999)).rejects.toThrow(NotFoundException); }); }); }); ``` **Why good:** `Test.createTestingModule` mirrors the real module, mock Prisma methods with `jest.fn()`, test both happy path and error cases, `jest.clearAllMocks()` prevents test pollution, named constants for test data ### Bad Example — Testing with Real Database ```typescript // BAD: Unit test hitting real database describe("UsersService", () => { let service: UsersService; beforeAll(async () => { // BAD: Using real PrismaService connects to database const module = await Test.createTestingModule({ providers: [UsersService, PrismaService], }).compile(); service = module.get(UsersService); }); it("should find users", async () => { // BAD: Depends on database state const users = await service.findAll(); expect(users).toBeDefined(); }); }); ``` **Why bad:** Unit tests should not hit real databases, tests become flaky depending on database state, slow execution, not isolated --- ## Pattern 2: Unit Testing Controllers ### Good Example — Testing Controller with Mocked Service ```typescript // users/users.controller.spec.ts import { Test, TestingModule } from "@nestjs/testing"; import { UsersController } from "./users.controller"; import { UsersService } from "./users.service"; const MOCK_USER = { id: 1, email: "alice@example.com", name: "Alice", role: "user", }; const MOCK_USERS_RESPONSE = { items: [MOCK_USER], total: 1, page: 1, limit: 20, totalPages: 1, }; describe("UsersController", () => { let controller: UsersController; const mockUsersService = { findAll: jest.fn().mockResolvedValue(MOCK_USERS_RESPONSE), findOne: jest.fn().mockResolvedValue(MOCK_USER), create: jest.fn().mockResolvedValue(MOCK_USER), update: jest.fn().mockResolvedValue(MOCK_USER), remove: jest.fn().mockResolvedValue(undefined), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: mockUsersService, }, ], }).compile(); controller = module.get<UsersController>(UsersController); jest.clearAllMocks(); }); describe("findAll", () => { it("should return paginated users", async () => { const result = await controller.findAll({ page: 1, limit: 20 }); expect(result).toEqual(MOCK_USERS_RESPONSE); expect(mockUsersService.findAll).toHaveBeenCalledWith(1, 20); }); }); describe("findOne", () => { it("should return a single user", async () => { const result = await controller.findOne(1); expect(result).toEqual(MOCK_USER); expect(mockUsersService.findOne).toHaveBeenCalledWith(1); }); }); describe("create", () => { it("should create a user", async () => { const dto = { email: "alice@example.com", name: "Alice", password: "password123", }; const result = await controller.create(dto); expect(result).toEqual(MOCK_USER); expect(mockUsersService.create).toHaveBeenCalledWith(dto); }); }); }); ``` **Why good:** Controllers are thin, so tests verify delegation to service, mock service prevents testing business logic twice, tests verify correct arguments passed to service --- ## Pattern 3: Unit Testing Guards ### Good Example — Testing a Custom Guard ```typescript // auth/guards/roles.guard.spec.ts import { ExecutionContext, ForbiddenException } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { RolesGuard } from "./roles.guard"; function createMockExecutionContext( user: { role: string } | undefined, requiredRoles: string[] | undefined, ): ExecutionContext { const mockReflector = { getAllAndOverride: jest.fn().mockReturnValue(requiredRoles), }; const context = { switchToHttp: () => ({ getRequest: () => ({ user }), }), getHandler: () => jest.fn(), getClass: () => jest.fn(), } as unknown as ExecutionContext; return context; } describe("RolesGuard", () => { let guard: RolesGuard; let reflector: Reflector; beforeEach(() => { reflector = new Reflector(); guard = new RolesGuard(reflector); }); it("should allow access when no roles are required", () => { jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(undefined); const context = createMockExecutionContext({ role: "user" }, undefined); expect(guard.canActivate(context)).toBe(true); }); it("should allow access when user has required role", () => { jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(["admin"]); const context = createMockExecutionContext({ role: "admin" }, ["admin"]); expect(guard.canActivate(context)).toBe(true); }); it("should deny access when user lacks required role", () => { jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(["admin"]); const context = createMockExecutionContext({ role: "user" }, ["admin"]); expect(() => guard.canActivate(context)).toThrow(ForbiddenException); }); }); ``` **Why good:** Tests all three paths (no roles required, role match, role mismatch), mock ExecutionContext matches NestJS interface, verifies guard throws correct exception type --- ## Pattern 4: E2E Testing with Supertest ### Good Example — Full API Integration Test ```typescript // test/users.e2e-spec.ts import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication, ValidationPipe, HttpStatus } from "@nestjs/common"; import * as request from "supertest"; import { AppModule } from "../src/app.module"; import { PrismaService } from "../src/prisma/prisma.service"; describe("UsersController (e2e)", () => { let app: INestApplication; let prisma: PrismaService; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); // Mirror production configuration app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.init(); prisma = app.get(PrismaService); }); afterAll(async () => { await app.close(); }); beforeEach(async () => { // Clean database between tests await prisma.user.deleteMany(); }); describe("POST /users", () => { it("should create a user with valid data", () => { return request(app.getHttpServer()) .post("/users") .send({ email: "alice@example.com", name: "Alice", password: "securepassword", }) .expect(HttpStatus.CREATED) .expect((res) => { expect(res.body).toHaveProperty("id"); expect(res.body.email).toBe("alice@example.com"); expect(res.body.name).toBe("Alice"); // Password should NOT be in response expect(res.body).not.toHaveProperty("password"); }); }); it("should reject invalid email", () => { return request(app.getHttpServer()) .post("/users") .send({ email: "not-an-email", name: "Alice", password: "securepassword", }) .expect(HttpStatus.BAD_REQUEST) .expect((res) => { expect(res.body.message).toContain("email must be an email"); }); }); it("should reject unknown properties", () => { return request(app.getHttpServer()) .post("/users") .send({ email: "alice@example.com", name: "Alice", password: "securepassword", isAdmin: true, // Unknown property }) .expect(HttpStatus.BAD_REQUEST); }); it("should reject duplicate email", async () => { // Create first user await request(app.getHttpServer()) .post("/users") .send({ email: "alice@example.com", name: "Alice", password: "securepassword", }) .expect(HttpStatus.CREATED); // Try duplicate return request(app.getHttpServer()) .post("/users") .send({ email: "alice@example.com", name: "Alice 2", password: "securepassword", }) .expect(HttpStatus.CONFLICT); }); }); describe("GET /users/:id", () => { it("should return a user", async () => { const createRes = await request(app.getHttpServer()).post("/users").send({ email: "alice@example.com", name: "Alice", password: "securepassword", }); return request(app.getHttpServer()) .get(`/users/${createRes.body.id}`) .expect(HttpStatus.OK) .expect((res) => { expect(res.body.email).toBe("alice@example.com"); }); }); it("should return 404 for non-existent user", () => { return request(app.getHttpServer()) .get("/users/99999") .expect(HttpStatus.NOT_FOUND); }); }); describe("DELETE /users/:id", () => { it("should delete a user", async () => { const createRes = await request(app.getHttpServer()).post("/users").send({ email: "alice@example.com", name: "Alice", password: "securepassword", }); await request(app.getHttpServer()) .delete(`/users/${createRes.body.id}`) .expect(HttpStatus.NO_CONTENT); // Verify deleted return request(app.getHttpServer()) .get(`/users/${createRes.body.id}`) .expect(HttpStatus.NOT_FOUND); }); }); }); ``` **Why good:** Mirrors production config (ValidationPipe), tests HTTP status codes and response shapes, cleans database between tests, verifies validation rejects invalid data, tests the full request-response cycle ### Bad Example — E2E Test Without Validation Setup ```typescript // BAD: Missing ValidationPipe — validation not tested describe("UsersController (e2e)", () => { let app: INestApplication; beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); // BAD: No ValidationPipe — invalid data will pass through await app.init(); }); it("should validate input", () => { // This test PASSES even though email is invalid // because ValidationPipe is not configured return request(app.getHttpServer()) .post("/users") .send({ email: "not-an-email" }) .expect(201); // BAD: Should be 400 }); }); ``` **Why bad:** E2E tests without ValidationPipe don't test validation, invalid data passes through, tests give false confidence --- ## Pattern 5: Testing Auth-Protected Routes ### Good Example — E2E with Authentication ```typescript // test/auth.e2e-spec.ts import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication, ValidationPipe, HttpStatus } from "@nestjs/common"; import * as request from "supertest"; import { AppModule } from "../src/app.module"; describe("Auth (e2e)", () => { let app: INestApplication; let accessToken: string; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true }), ); await app.init(); }); afterAll(async () => { await app.close(); }); it("should register a new user", () => { return request(app.getHttpServer()) .post("/auth/register") .send({ email: "test@example.com", name: "Test User", password: "securepassword", }) .expect(HttpStatus.CREATED); }); it("should login and receive JWT", async () => { const res = await request(app.getHttpServer()) .post("/auth/login") .send({ email: "test@example.com", password: "securepassword", }) .expect(HttpStatus.OK); expect(res.body).toHaveProperty("accessToken"); accessToken = res.body.accessToken; }); it("should access protected route with token", () => { return request(app.getHttpServer()) .get("/users/me") .set("Authorization", `Bearer ${accessToken}`) .expect(HttpStatus.OK) .expect((res) => { expect(res.body.email).toBe("test@example.com"); }); }); it("should reject protected route without token", () => { return request(app.getHttpServer()) .get("/users/me") .expect(HttpStatus.UNAUTHORIZED); }); it("should reject invalid token", () => { return request(app.getHttpServer()) .get("/users/me") .set("Authorization", "Bearer invalid-token") .expect(HttpStatus.UNAUTHORIZED); }); }); ``` **Why good:** Tests full auth flow (register, login, access protected route), verifies both valid and invalid tokens, uses `.set('Authorization', ...)` for bearer token, tests guard behavior end-to-end --- _For core patterns, see [core.md](core.md). For database patterns, see [database.md](database.md). For advanced patterns, see [advanced.md](advanced.md)._
-
-
reference.md 9.8 KB
# NestJS Reference > CLI commands, project structure, decorator tables, pipes, exceptions, and provider scopes. See [SKILL.md](SKILL.md) for core concepts, decision frameworks, and red flags. --- ## CLI Commands ### Project Scaffolding ```bash # Create new project nest new my-project # Create with specific package manager nest new my-project --package-manager pnpm ``` ### Code Generation ```bash # Generate a complete CRUD resource (module + controller + service + DTOs) nest generate resource users nest g res users # Generate individual components nest generate module users # nest g mo users nest generate controller users # nest g co users nest generate service users # nest g s users nest generate guard auth # nest g gu auth nest generate interceptor logging # nest g itc logging nest generate pipe validation # nest g pi validation nest generate filter http-exception # nest g f http-exception nest generate middleware logger # nest g mi logger nest generate decorator roles # nest g d roles nest generate class dto/create-user # nest g cl dto/create-user ``` ### Build and Run ```bash # Development nest start --watch # Debug mode nest start --debug --watch # Production build nest build # Run production node dist/main.js ``` --- ## Standard Project Structure ``` src/ ├── main.ts # App entry point, bootstrap ├── app.module.ts # Root module ├── app.controller.ts # Root controller (health check) ├── app.service.ts # Root service │ ├── config/ # Configuration │ ├── app.config.ts # App config (registerAs) │ └── database.config.ts # Database config │ ├── common/ # Shared utilities │ ├── decorators/ # Custom decorators │ ├── filters/ # Exception filters │ ├── guards/ # Auth/role guards │ ├── interceptors/ # Response/logging interceptors │ ├── middleware/ # HTTP middleware │ └── pipes/ # Custom pipes │ ├── prisma/ # Database (Prisma) │ ├── prisma.module.ts │ └── prisma.service.ts │ ├── auth/ # Auth feature module │ ├── auth.module.ts │ ├── auth.controller.ts │ ├── auth.service.ts │ ├── dto/ │ │ └── login.dto.ts │ ├── guards/ │ │ ├── jwt-auth.guard.ts │ │ └── roles.guard.ts │ ├── strategies/ │ │ ├── jwt.strategy.ts │ │ └── local.strategy.ts │ └── decorators/ │ ├── current-user.decorator.ts │ ├── public.decorator.ts │ └── roles.decorator.ts │ ├── users/ # Users feature module │ ├── users.module.ts │ ├── users.controller.ts │ ├── users.service.ts │ ├── dto/ │ │ ├── create-user.dto.ts │ │ ├── update-user.dto.ts │ │ └── query-users.dto.ts │ └── entities/ │ └── user.entity.ts # TypeORM entity (if using TypeORM) │ └── orders/ # Orders feature module ├── orders.module.ts ├── orders.controller.ts ├── orders.service.ts └── dto/ └── create-order.dto.ts test/ ├── app.e2e-spec.ts # E2E tests └── jest-e2e.json # E2E test config ``` --- ## Decorator Quick Reference ### Class Decorators | Decorator | Purpose | Example | | ----------------------- | ------------------------------ | --------------------------------------------------- | | `@Module({...})` | Define a module | `@Module({ controllers: [...], providers: [...] })` | | `@Controller(path?)` | Define a controller | `@Controller('users')` | | `@Injectable()` | Mark class for DI | `@Injectable() export class UsersService {}` | | `@Global()` | Make module globally available | `@Global() @Module({...})` | | `@Catch(...exceptions)` | Exception filter | `@Catch(HttpException)` | ### Route Decorators | Decorator | HTTP Method | Example | | ---------------- | ----------- | ---------------- | | `@Get(path?)` | GET | `@Get(':id')` | | `@Post(path?)` | POST | `@Post()` | | `@Put(path?)` | PUT | `@Put(':id')` | | `@Patch(path?)` | PATCH | `@Patch(':id')` | | `@Delete(path?)` | DELETE | `@Delete(':id')` | | `@All(path?)` | All methods | `@All('*')` | ### Parameter Decorators | Decorator | Extracts | Example | | ---------------- | ---------------- | ------------------------------------------------- | | `@Body(key?)` | Request body | `@Body() dto: CreateUserDto` | | `@Param(key?)` | Route params | `@Param('id', ParseIntPipe) id: number` | | `@Query(key?)` | Query string | `@Query('page') page: string` | | `@Headers(key?)` | Request headers | `@Headers('authorization') auth: string` | | `@Req()` | Express Request | `@Req() req: Request` | | `@Res()` | Express Response | `@Res() res: Response` (disables NestJS response) | | `@Ip()` | Client IP | `@Ip() ip: string` | | `@Session()` | Session object | `@Session() session: Record<string, any>` | ### Handler Decorators | Decorator | Purpose | Example | | ------------------------- | ----------------------- | --------------------------------------- | | `@HttpCode(status)` | Set response status | `@HttpCode(HttpStatus.NO_CONTENT)` | | `@Header(name, value)` | Set response header | `@Header('Cache-Control', 'none')` | | `@Redirect(url, code?)` | Redirect response | `@Redirect('https://example.com', 301)` | | `@UseGuards(...guards)` | Apply guards | `@UseGuards(AuthGuard)` | | `@UseInterceptors(...i)` | Apply interceptors | `@UseInterceptors(LoggingInterceptor)` | | `@UsePipes(...pipes)` | Apply pipes | `@UsePipes(new ValidationPipe())` | | `@UseFilters(...filters)` | Apply exception filters | `@UseFilters(HttpExceptionFilter)` | ### Metadata Decorators | Decorator | Purpose | Example | | ------------------------ | ------------------ | ---------------------------------- | | `@SetMetadata(key, val)` | Set route metadata | `@SetMetadata('roles', ['admin'])` | --- ## Built-in Pipes | Pipe | Purpose | | ------------------ | ----------------------------------- | | `ValidationPipe` | Validates DTO with class-validator | | `ParseIntPipe` | Converts string to integer | | `ParseFloatPipe` | Converts string to float | | `ParseBoolPipe` | Converts string to boolean | | `ParseUUIDPipe` | Validates UUID format | | `ParseDatePipe` | Converts string to Date (NestJS 11) | | `ParseEnumPipe` | Validates enum membership | | `ParseArrayPipe` | Parses and validates arrays | | `DefaultValuePipe` | Sets default for undefined params | --- ## Built-in HTTP Exceptions | Exception | Status Code | | ------------------------------- | ----------- | | `BadRequestException` | 400 | | `UnauthorizedException` | 401 | | `ForbiddenException` | 403 | | `NotFoundException` | 404 | | `MethodNotAllowedException` | 405 | | `NotAcceptableException` | 406 | | `RequestTimeoutException` | 408 | | `ConflictException` | 409 | | `GoneException` | 410 | | `PayloadTooLargeException` | 413 | | `UnsupportedMediaTypeException` | 415 | | `UnprocessableEntityException` | 422 | | `InternalServerErrorException` | 500 | | `NotImplementedException` | 501 | | `BadGatewayException` | 502 | | `ServiceUnavailableException` | 503 | | `GatewayTimeoutException` | 504 | --- ## Provider Scopes | Scope | Lifetime | Use Case | | --------------------- | ------------- | ------------------------------------------- | | `DEFAULT` (Singleton) | App lifetime | Most services, shared state | | `REQUEST` | Per request | Request-specific data (user context) | | `TRANSIENT` | Per injection | Stateful providers that shouldn't be shared | ```typescript @Injectable({ scope: Scope.REQUEST }) export class RequestScopedService { // New instance per HTTP request } ``` --- ## Module Metadata | Property | Type | Purpose | | ------------- | ------------------------ | -------------------------------- | | `imports` | `Module[]` | Other modules to import | | `controllers` | `Controller[]` | Controllers in this module | | `providers` | `Provider[]` | Services/providers for DI | | `exports` | `(Provider \| string)[]` | Providers available to importers | --- > For decision frameworks and red flags, see [SKILL.md](SKILL.md). -
SKILL.md 14.4 KB
--- name: api-framework-nestjs description: NestJS backend framework - modules, controllers, services, DI, guards, pipes, interceptors, exception filters, middleware, DTOs with class-validator --- # NestJS Patterns > **Quick Guide:** NestJS is an opinionated, modular Node.js framework built on TypeScript. Use modules to organize features, controllers for HTTP routing, services for business logic with dependency injection, DTOs with class-validator for validation, guards for auth, and exception filters for error handling. Key gotchas: always register services in module `providers`, always enable `ValidationPipe` globally with `whitelist: true`, never put business logic in controllers, never instantiate services with `new`. NestJS 11 is the current stable version (opt-in SWC compiler, Express v5, reversed termination hooks). --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `@Injectable()` on every service and register it in the module `providers` array)** **(You MUST enable `ValidationPipe` globally with `whitelist: true` and `forbidNonWhitelisted: true`)** **(You MUST use DTOs with class-validator decorators for ALL request body validation — never validate manually in controllers)** **(You MUST throw NestJS built-in HTTP exceptions (`NotFoundException`, `BadRequestException`, etc.) — never send raw status codes)** **(You MUST use constructor injection for dependencies — never instantiate services manually with `new`)** </critical_requirements> --- **Auto-detection:** NestJS, @nestjs/common, @nestjs/core, @Module, @Controller, @Injectable, @Get, @Post, @Body, @Param, @Query, @UseGuards, @UseInterceptors, @UsePipes, @UseFilters, CanActivate, NestInterceptor, PipeTransform, ExceptionFilter, ValidationPipe, class-validator, class-transformer **When to use:** - Building structured backend APIs with TypeScript and dependency injection - Applications requiring modular architecture with clear separation of concerns - REST APIs with declarative validation, authentication, and role-based access - Projects needing the guard/interceptor/pipe/filter request lifecycle **When NOT to use:** - Simple scripts or serverless functions that don't need a framework - Projects where Express/Fastify alone is sufficient (no DI, no modules needed) - Frontend code **Detailed Resources:** - [examples/core.md](examples/core.md) — Feature modules, CRUD, DTOs, dynamic modules, exception filters, custom providers - [examples/database.md](examples/database.md) — NestJS DI patterns for database integration, transactions - [examples/auth.md](examples/auth.md) — Passport.js integration, JWT strategy, auth guards, RBAC - [examples/testing.md](examples/testing.md) — Unit testing with `Test.createTestingModule`, e2e with supertest - [examples/advanced.md](examples/advanced.md) — Interceptors, custom pipes, custom decorators, config, CQRS, Swagger - [reference.md](reference.md) — CLI commands, project structure, decorator tables, decision frameworks --- <philosophy> ## Philosophy NestJS enforces a **modular, decorator-driven architecture** inspired by Angular. Every feature is organized into modules containing controllers (HTTP layer), services (business logic), and supporting infrastructure (guards, pipes, interceptors, filters). **Core principles:** 1. **Modularity** — Group related controllers, services, and providers into feature modules. Modules are the primary organizational unit. 2. **Dependency injection** — Never instantiate services manually. Declare them as `@Injectable()` and let NestJS resolve the dependency graph via constructor injection. 3. **Decorator-driven** — Decorators (`@Controller`, `@Get`, `@Body`, `@UseGuards`) attach metadata that NestJS uses to build routing, validation, and middleware pipelines. 4. **Separation of concerns** — Controllers handle HTTP request/response. Services handle business logic. Guards handle authorization. Pipes handle validation/transformation. Filters handle exceptions. 5. **Convention over configuration** — Follow NestJS conventions (one module per feature, one controller per resource, DTOs for validation) to get batteries-included functionality. </philosophy> --- <patterns> ## Key Patterns ### Module System Every NestJS app has a root `AppModule` that imports feature modules. Each feature module groups its controller, service, and providers. Export services that other modules need. ```typescript // Feature module — one per resource @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], // Available to other modules }) export class UsersModule {} ``` **Why good:** Encapsulation per feature, explicit dependency graph via imports/exports, testable in isolation See [examples/core.md](examples/core.md) for complete CRUD module, dynamic modules, and custom providers. --- ### Controllers — Thin Routing Layer Controllers should only extract request data and delegate to services. No business logic. ```typescript @Controller("users") export class UsersController { constructor(private readonly usersService: UsersService) {} @Get(":id") findOne(@Param("id", ParseIntPipe) id: number) { return this.usersService.findOne(id); } @Post() @HttpCode(HttpStatus.CREATED) create(@Body() dto: CreateUserDto) { return this.usersService.create(dto); } } ``` **Why good:** `ParseIntPipe` validates and converts param, `@HttpCode` for explicit status, thin delegation to service **Anti-pattern:** Business logic, manual validation, or database access in controllers — always delegate to services. --- ### DTOs with class-validator Use DTOs with class-validator decorators for all request validation. Enable `ValidationPipe` globally. ```typescript const MIN_PASSWORD_LENGTH = 8; export class CreateUserDto { @IsEmail() email: string; @IsString() @MinLength(MIN_PASSWORD_LENGTH) password: string; } // Update DTO — reuses validation rules export class UpdateUserDto extends PartialType(CreateUserDto) {} ``` ```typescript // main.ts — Enable globally app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Strip unknown properties forbidNonWhitelisted: true, // Reject unknown properties transform: true, // Auto-transform to DTO instances }), ); ``` **Why good:** Declarative validation, `whitelist` prevents mass-assignment, `PartialType` avoids duplicating rules See [examples/core.md](examples/core.md) for nested DTOs, query DTOs with pagination, and validation groups. --- ### Services and Dependency Injection Services contain business logic. Decorate with `@Injectable()` and inject via constructor. ```typescript @Injectable() export class UsersService { findOne(id: number): User { const user = this.users.find((u) => u.id === id); if (!user) { throw new NotFoundException(`User with id ${id} not found`); } return user; } } ``` **Why good:** `@Injectable()` enables DI, throws NestJS HTTP exceptions, pure business logic with no HTTP concerns #### Custom Providers Use token-based injection for non-class providers (factory, value, class providers): ```typescript const DATABASE_CONNECTION = "DATABASE_CONNECTION"; const databaseProvider = { provide: DATABASE_CONNECTION, useFactory: async (configService: ConfigService) => { return createConnection(configService.get("database")); }, inject: [ConfigService], }; // Inject with @Inject token constructor(@Inject(DATABASE_CONNECTION) private readonly db: Connection) {} ``` See [examples/core.md](examples/core.md) for complete provider examples. --- ### Exception Handling Throw NestJS built-in HTTP exceptions from services. Use exception filters for custom error response formatting. ```typescript // Service — throw built-in exceptions throw new NotFoundException("Resource not found"); throw new ConflictException("Resource already exists"); throw new BadRequestException("Invalid input"); throw new UnauthorizedException("Authentication required"); ``` **Key point:** NestJS auto-converts these to proper HTTP responses with correct status codes. Never send raw status codes. For custom error response shapes, use a global `@Catch()` exception filter. See [examples/core.md](examples/core.md). --- ### Guards and Middleware Guards decide whether a request proceeds (authorization). Middleware runs before routing (logging, CORS). ```typescript // Guard — implements CanActivate @Injectable() export class JwtAuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest<Request>(); // Validate token, attach user to request return true; } } // Apply to routes @UseGuards(JwtAuthGuard, RolesGuard) @Controller("admin") export class AdminController {} ``` **Why good:** Guards are injectable (can use services), composable (run in order), use `Reflector` for metadata-driven access control See [examples/auth.md](examples/auth.md) for JWT auth, Passport.js integration, RBAC, and `@Public()` decorator. --- ### Interceptors Interceptors wrap handler execution for cross-cutting concerns (response wrapping, logging, caching). ```typescript @Injectable() export class TransformResponseInterceptor<T> implements NestInterceptor { intercept( context: ExecutionContext, next: CallHandler, ): Observable<ApiResponse<T>> { return next.handle().pipe( map((data) => ({ success: true, data, timestamp: new Date().toISOString(), })), ); } } ``` See [examples/advanced.md](examples/advanced.md) for logging, caching, and custom pipe patterns. </patterns> --- <decision_framework> ## Decision Framework ### Request Lifecycle ``` Incoming Request → Middleware (logging, CORS, body parsing) → Guards (authentication, authorization) → Interceptors (pre-handler: transform request, start timing) → Pipes (validation, transformation) → Route Handler (controller method) → Interceptors (post-handler: transform response, log timing) → Exception Filters (catch and format errors) → Response ``` ### Which Layer to Use ``` Need to process raw request before routing? ├─ YES → Middleware (logging, CORS, rate limiting) └─ NO → Does it decide allow/deny for a route? ├─ YES → Guard (auth, roles, permissions) └─ NO → Does it transform/validate input data? ├─ YES → Pipe (validation, type coercion) └─ NO → Does it wrap handler execution? ├─ YES → Interceptor (timing, caching, response mapping) └─ NO → Does it handle errors? ├─ YES → Exception Filter └─ NO → Put it in the service layer ``` ### Module Organization ``` Is this a cross-cutting concern (auth, config, logging)? ├─ YES → Global module or shared module └─ NO → Is it a business feature (users, orders, products)? ├─ YES → Feature module (users.module.ts) └─ NO → Is it infrastructure (database, cache, queue)? ├─ YES → Infrastructure module └─ NO → Part of the closest feature module ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Putting business logic in controllers instead of services - Missing `@Injectable()` on services (DI fails silently at runtime) - Not enabling `ValidationPipe` globally (DTOs are not validated) - Using `any` for request body instead of typed DTOs - Instantiating services with `new` instead of constructor injection - Throwing raw `Error` instead of NestJS HTTP exceptions (produces 500 instead of proper status) **Medium Priority Issues:** - Not exporting services from modules (other modules can't import them) - Importing the entire module when you only need one service - Missing `whitelist: true` on ValidationPipe (mass-assignment vulnerability) - Using `@Res()` decorator outside streaming scenarios (opts out of NestJS response handling) - Not using `PartialType` / `PickType` / `OmitType` for update DTOs (duplicated validation) **Common Mistakes:** - Circular module dependencies — restructure with `forwardRef()` or extract shared logic - Forgetting to register providers in the module — service injection fails at runtime - Using synchronous guards for async operations — return `Promise<boolean>` or `Observable<boolean>` - Not handling all exception types in custom filters — always have a catch-all for unknown errors **Gotchas and Edge Cases:** - `@UseGuards(AuthGuard)` takes a class reference, not an instance — NestJS instantiates via DI - `ValidationPipe` with `transform: true` converts query params to their declared types automatically - Guards execute AFTER middleware but BEFORE interceptors and pipes - `@Catch()` with no arguments catches ALL exceptions, not just HttpException - `IntrinsicException` (NestJS 11) throws without framework auto-logging — useful for expected flow control - NestJS 11: Termination lifecycle hooks (`OnModuleDestroy`, `OnApplicationShutdown`) now execute in reverse order - NestJS 11: Express v5 requires named wildcards (`/*splat` instead of `/*`) - NestJS 11: SWC is a supported opt-in compiler via `nest-cli.json` (`"builder": "swc"`) — 20x faster builds than tsc - NestJS 11: `ParseDatePipe` is now built-in — no need for custom date parsing pipes - Request-scoped providers (`Scope.REQUEST`) affect performance — use only when needed - `forwardRef()` should be a last resort — circular deps usually signal a design issue </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `@Injectable()` on every service and register it in the module `providers` array)** **(You MUST enable `ValidationPipe` globally with `whitelist: true` and `forbidNonWhitelisted: true`)** **(You MUST use DTOs with class-validator decorators for ALL request body validation — never validate manually in controllers)** **(You MUST throw NestJS built-in HTTP exceptions (`NotFoundException`, `BadRequestException`, etc.) — never send raw status codes)** **(You MUST use constructor injection for dependencies — never instantiate services manually with `new`)** **Failure to follow these rules will produce unvalidated, untestable NestJS code with broken dependency injection.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.