Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
40 prioritized NestJS best practices across architecture, DI, security, performance, testing, and microservices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
AGENTS.md
1# NestJS Best Practices23**Version 1.1.0**4NestJS Best Practices5January 202667> **Note:**8> This document is mainly for agents and LLMs to follow when maintaining,9> generating, or refactoring NestJS codebases. Humans may also find it10> useful, but guidance here is optimized for automation and consistency11> by AI-assisted workflows.1213---1415## Abstract1617Comprehensive best practices and architecture guide for NestJS applications, designed for AI agents and LLMs. Contains 40 rules across 10 categories, prioritized by impact from critical (architecture, dependency injection) to incremental (DevOps patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.1819---2021## Table of Contents22231. [Architecture](#1-architecture) — **CRITICAL**24- 1.1 [Avoid Circular Dependencies](#11-avoid-circular-dependencies)25- 1.2 [Organize by Feature Modules](#12-organize-by-feature-modules)26- 1.3 [Use Proper Module Sharing Patterns](#13-use-proper-module-sharing-patterns)27- 1.4 [Single Responsibility for Services](#14-single-responsibility-for-services)28- 1.5 [Use Event-Driven Architecture for Decoupling](#15-use-event-driven-architecture-for-decoupling)29- 1.6 [Use Repository Pattern for Data Access](#16-use-repository-pattern-for-data-access)302. [Dependency Injection](#2-dependency-injection) — **CRITICAL**31- 2.1 [Avoid Service Locator Anti-Pattern](#21-avoid-service-locator-anti-pattern)32- 2.2 [Apply Interface Segregation Principle](#22-apply-interface-segregation-principle)33- 2.3 [Honor Liskov Substitution Principle](#23-honor-liskov-substitution-principle)34- 2.4 [Prefer Constructor Injection](#24-prefer-constructor-injection)35- 2.5 [Understand Provider Scopes](#25-understand-provider-scopes)36- 2.6 [Use Injection Tokens for Interfaces](#26-use-injection-tokens-for-interfaces)373. [Error Handling](#3-error-handling) — **HIGH**38- 3.1 [Handle Async Errors Properly](#31-handle-async-errors-properly)39- 3.2 [Throw HTTP Exceptions from Services](#32-throw-http-exceptions-from-services)40- 3.3 [Use Exception Filters for Error Handling](#33-use-exception-filters-for-error-handling)414. [Security](#4-security) — **HIGH**42- 4.1 [Implement Secure JWT Authentication](#41-implement-secure-jwt-authentication)43- 4.2 [Implement Rate Limiting](#42-implement-rate-limiting)44- 4.3 [Sanitize Output to Prevent XSS](#43-sanitize-output-to-prevent-xss)45- 4.4 [Use Guards for Authentication and Authorization](#44-use-guards-for-authentication-and-authorization)46- 4.5 [Validate All Input with DTOs and Pipes](#45-validate-all-input-with-dtos-and-pipes)475. [Performance](#5-performance) — **HIGH**48- 5.1 [Use Async Lifecycle Hooks Correctly](#51-use-async-lifecycle-hooks-correctly)49- 5.2 [Use Lazy Loading for Large Modules](#52-use-lazy-loading-for-large-modules)50- 5.3 [Optimize Database Queries](#53-optimize-database-queries)51- 5.4 [Use Caching Strategically](#54-use-caching-strategically)526. [Testing](#6-testing) — **MEDIUM-HIGH**53- 6.1 [Use Supertest for E2E Testing](#61-use-supertest-for-e2e-testing)54- 6.2 [Mock External Services in Tests](#62-mock-external-services-in-tests)55- 6.3 [Use Testing Module for Unit Tests](#63-use-testing-module-for-unit-tests)567. [Database & ORM](#7-database-orm) — **MEDIUM-HIGH**57- 7.1 [Avoid N+1 Query Problems](#71-avoid-n-1-query-problems)58- 7.2 [Use Database Migrations](#72-use-database-migrations)59- 7.3 [Use Transactions for Multi-Step Operations](#73-use-transactions-for-multi-step-operations)608. [API Design](#8-api-design) — **MEDIUM**61- 8.1 [Use DTOs and Serialization for API Responses](#81-use-dtos-and-serialization-for-api-responses)62- 8.2 [Use Interceptors for Cross-Cutting Concerns](#82-use-interceptors-for-cross-cutting-concerns)63- 8.3 [Use Pipes for Input Transformation](#83-use-pipes-for-input-transformation)64- 8.4 [Use API Versioning for Breaking Changes](#84-use-api-versioning-for-breaking-changes)659. [Microservices](#9-microservices) — **MEDIUM**66- 9.1 [Implement Health Checks for Microservices](#91-implement-health-checks-for-microservices)67- 9.2 [Use Message and Event Patterns Correctly](#92-use-message-and-event-patterns-correctly)68- 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs)6910. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM**70- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)71- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)72- 10.3 [Use Structured Logging](#103-use-structured-logging)7374---7576## 1. Architecture7778**Section Impact: CRITICAL**7980### 1.1 Avoid Circular Dependencies8182**Impact: CRITICAL** — "#1 cause of runtime crashes"8384Circular dependencies occur when Module A imports Module B, and Module B imports Module A (directly or transitively). NestJS can sometimes resolve these through forward references, but they indicate architectural problems and should be avoided. This is the #1 cause of runtime crashes in NestJS applications.8586**Incorrect (circular module imports):**8788```typescript89// users.module.ts90@Module({91imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular92providers: [UsersService],93exports: [UsersService],94})95export class UsersModule {}9697// orders.module.ts98@Module({99imports: [UsersModule], // Circular dependency!100providers: [OrdersService],101exports: [OrdersService],102})103export class OrdersModule {}104```105106**Correct (extract shared logic or use events):**107108```typescript109// Option 1: Extract shared logic to a third module110// shared.module.ts111@Module({112providers: [SharedService],113exports: [SharedService],114})115export class SharedModule {}116117// users.module.ts118@Module({119imports: [SharedModule],120providers: [UsersService],121})122export class UsersModule {}123124// orders.module.ts125@Module({126imports: [SharedModule],127providers: [OrdersService],128})129export class OrdersModule {}130131// Option 2: Use events for decoupled communication132// users.service.ts133@Injectable()134export class UsersService {135constructor(private eventEmitter: EventEmitter2) {}136137async createUser(data: CreateUserDto) {138const user = await this.userRepo.save(data);139this.eventEmitter.emit('user.created', user);140return user;141}142}143144// orders.service.ts145@Injectable()146export class OrdersService {147@OnEvent('user.created')148handleUserCreated(user: User) {149// React to user creation without direct dependency150}151}152```153154Reference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency)155156---157158### 1.2 Organize by Feature Modules159160**Impact: CRITICAL** — "3-5x faster onboarding and development"161162Organize your application into feature modules that encapsulate related functionality. Each feature module should be self-contained with its own controllers, services, entities, and DTOs. Avoid organizing by technical layer (all controllers together, all services together). This enables 3-5x faster onboarding and feature development.163164**Incorrect (technical layer organization):**165166```typescript167// Technical layer organization (anti-pattern)168src/169├── controllers/170│ ├── users.controller.ts171│ ├── orders.controller.ts172│ └── products.controller.ts173├── services/174│ ├── users.service.ts175│ ├── orders.service.ts176│ └── products.service.ts177├── entities/178│ ├── user.entity.ts179│ ├── order.entity.ts180│ └── product.entity.ts181└── app.module.ts // Imports everything directly182```183184**Correct (feature module organization):**185186```typescript187// Feature module organization188src/189├── users/190│ ├── dto/191│ │ ├── create-user.dto.ts192│ │ └── update-user.dto.ts193│ ├── entities/194│ │ └── user.entity.ts195│ ├── users.controller.ts196│ ├── users.service.ts197│ ├── users.repository.ts198│ └── users.module.ts199├── orders/200│ ├── dto/201│ ├── entities/202│ ├── orders.controller.ts203│ ├── orders.service.ts204│ └── orders.module.ts205├── shared/206│ ├── guards/207│ ├── interceptors/208│ ├── filters/209│ └── shared.module.ts210└── app.module.ts211212// users.module.ts213@Module({214imports: [TypeOrmModule.forFeature([User])],215controllers: [UsersController],216providers: [UsersService, UsersRepository],217exports: [UsersService], // Only export what others need218})219export class UsersModule {}220221// app.module.ts222@Module({223imports: [224ConfigModule.forRoot(),225TypeOrmModule.forRoot(),226UsersModule,227OrdersModule,228SharedModule,229],230})231export class AppModule {}232```233234Reference: [NestJS Modules](https://docs.nestjs.com/modules)235236---237238### 1.3 Use Proper Module Sharing Patterns239240**Impact: CRITICAL** — Prevents duplicate instances, memory leaks, and state inconsistency241242NestJS modules are singletons by default. When a service is properly exported from a module and that module is imported elsewhere, the same instance is shared. However, providing a service in multiple modules creates separate instances, leading to memory waste, state inconsistency, and confusing behavior. Always encapsulate services in dedicated modules, export them explicitly, and import the module where needed.243244**Incorrect (service provided in multiple modules):**245246```typescript247// StorageService provided directly in multiple modules - WRONG248// storage.service.ts249@Injectable()250export class StorageService {251private cache = new Map(); // Each instance has separate state!252253store(key: string, value: any) {254this.cache.set(key, value);255}256}257258// app.module.ts259@Module({260providers: [StorageService], // Instance #1261controllers: [AppController],262})263export class AppModule {}264265// videos.module.ts266@Module({267providers: [StorageService], // Instance #2 - different from AppModule!268controllers: [VideosController],269})270export class VideosModule {}271272// Problems:273// 1. Two separate StorageService instances exist274// 2. cache.set() in VideosModule doesn't affect AppModule's cache275// 3. Memory wasted on duplicate instances276// 4. Debugging nightmares when state doesn't sync277```278279**Correct (dedicated module with exports):**280281```typescript282// storage/storage.module.ts283@Module({284providers: [StorageService],285exports: [StorageService], // Make available to importers286})287export class StorageModule {}288289// videos/videos.module.ts290@Module({291imports: [StorageModule], // Import the module, not the service292controllers: [VideosController],293providers: [VideosService],294})295export class VideosModule {}296297// channels/channels.module.ts298@Module({299imports: [StorageModule], // Same instance shared300controllers: [ChannelsController],301providers: [ChannelsService],302})303export class ChannelsModule {}304305// app.module.ts306@Module({307imports: [308StorageModule, // Only if AppModule itself needs StorageService309VideosModule,310ChannelsModule,311],312})313export class AppModule {}314315// Now all modules share the SAME StorageService instance316```317318**When to use @Global() (sparingly):**319320```typescript321// ONLY for truly cross-cutting concerns322@Global()323@Module({324providers: [ConfigService, LoggerService],325exports: [ConfigService, LoggerService],326})327export class CoreModule {}328329// Import once in AppModule330@Module({331imports: [CoreModule], // Registered globally, available everywhere332})333export class AppModule {}334335// Other modules don't need to import CoreModule336@Module({337controllers: [UsersController],338providers: [UsersService], // Can inject ConfigService without importing339})340export class UsersModule {}341342// WARNING: Don't make everything global!343// - Hides dependencies (can't see what a module needs from imports)344// - Makes testing harder345// - Reserve for: config, logging, database connections346```347348**Module re-exporting pattern:**349350```typescript351// common.module.ts - shared utilities352@Module({353providers: [DateService, ValidationService],354exports: [DateService, ValidationService],355})356export class CommonModule {}357358// core.module.ts - re-exports common for convenience359@Module({360imports: [CommonModule, DatabaseModule],361exports: [CommonModule, DatabaseModule], // Re-export for consumers362})363export class CoreModule {}364365// feature.module.ts - imports CoreModule, gets both366@Module({367imports: [CoreModule], // Gets CommonModule + DatabaseModule368controllers: [FeatureController],369})370export class FeatureModule {}371```372373Reference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules)374375---376377### 1.4 Single Responsibility for Services378379**Impact: CRITICAL** — "40%+ improvement in testability"380381Each service should have a single, well-defined responsibility. Avoid "god services" that handle multiple unrelated concerns. If a service name includes "And" or handles more than one domain concept, it likely violates single responsibility. This reduces complexity and improves testability by 40%+.382383**Incorrect (god service anti-pattern):**384385```typescript386// God service anti-pattern387@Injectable()388export class UserAndOrderService {389constructor(390private userRepo: UserRepository,391private orderRepo: OrderRepository,392private mailer: MailService,393private payment: PaymentService,394) {}395396async createUser(dto: CreateUserDto) {397const user = await this.userRepo.save(dto);398await this.mailer.sendWelcome(user);399return user;400}401402async createOrder(userId: string, dto: CreateOrderDto) {403const order = await this.orderRepo.save({ userId, ...dto });404await this.payment.charge(order);405await this.mailer.sendOrderConfirmation(order);406return order;407}408409async calculateOrderStats(userId: string) {410// Stats logic mixed in411}412413async validatePayment(orderId: string) {414// Payment logic mixed in415}416}417```418419**Correct (focused services with single responsibility):**420421```typescript422// Focused services with single responsibility423@Injectable()424export class UsersService {425constructor(private userRepo: UserRepository) {}426427async create(dto: CreateUserDto): Promise<User> {428return this.userRepo.save(dto);429}430431async findById(id: string): Promise<User> {432return this.userRepo.findOneOrFail({ where: { id } });433}434}435436@Injectable()437export class OrdersService {438constructor(private orderRepo: OrderRepository) {}439440async create(userId: string, dto: CreateOrderDto): Promise<Order> {441return this.orderRepo.save({ userId, ...dto });442}443444async findByUser(userId: string): Promise<Order[]> {445return this.orderRepo.find({ where: { userId } });446}447}448449@Injectable()450export class OrderStatsService {451constructor(private orderRepo: OrderRepository) {}452453async calculateForUser(userId: string): Promise<OrderStats> {454// Focused stats calculation455}456}457458// Orchestration in controller or dedicated orchestrator459@Controller('orders')460export class OrdersController {461constructor(462private orders: OrdersService,463private payment: PaymentService,464private notifications: NotificationService,465) {}466467@Post()468async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) {469const order = await this.orders.create(user.id, dto);470await this.payment.charge(order);471await this.notifications.sendOrderConfirmation(order);472return order;473}474}475```476477Reference: [NestJS Providers](https://docs.nestjs.com/providers)478479---480481### 1.5 Use Event-Driven Architecture for Decoupling482483**Impact: MEDIUM-HIGH** — Enables async processing and modularity484485Use `@nestjs/event-emitter` for intra-service events and message brokers for inter-service communication. Events allow modules to react to changes without direct dependencies, improving modularity and enabling async processing.486487**Incorrect (direct service coupling):**488489```typescript490// Direct service coupling491@Injectable()492export class OrdersService {493constructor(494private inventoryService: InventoryService,495private emailService: EmailService,496private analyticsService: AnalyticsService,497private notificationService: NotificationService,498private loyaltyService: LoyaltyService,499) {}500501async createOrder(dto: CreateOrderDto): Promise<Order> {502const order = await this.repo.save(dto);503504// Tight coupling - OrdersService knows about all consumers505await this.inventoryService.reserve(order.items);506await this.emailService.sendConfirmation(order);507await this.analyticsService.track('order_created', order);508await this.notificationService.push(order.userId, 'Order placed');509await this.loyaltyService.addPoints(order.userId, order.total);510511// Adding new behavior requires modifying this service512return order;513}514}515```516517**Correct (event-driven decoupling):**518519```typescript520// Use EventEmitter for decoupling521import { EventEmitter2 } from '@nestjs/event-emitter';522523// Define event524export class OrderCreatedEvent {525constructor(526public readonly orderId: string,527public readonly userId: string,528public readonly items: OrderItem[],529public readonly total: number,530) {}531}532533// Service emits events534@Injectable()535export class OrdersService {536constructor(537private eventEmitter: EventEmitter2,538private repo: Repository<Order>,539) {}540541async createOrder(dto: CreateOrderDto): Promise<Order> {542const order = await this.repo.save(dto);543544// Emit event - no knowledge of consumers545this.eventEmitter.emit(546'order.created',547new OrderCreatedEvent(order.id, order.userId, order.items, order.total),548);549550return order;551}552}553554// Listeners in separate modules555@Injectable()556export class InventoryListener {557@OnEvent('order.created')558async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {559await this.inventoryService.reserve(event.items);560}561}562563@Injectable()564export class EmailListener {565@OnEvent('order.created')566async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {567await this.emailService.sendConfirmation(event.orderId);568}569}570571@Injectable()572export class AnalyticsListener {573@OnEvent('order.created')574async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {575await this.analyticsService.track('order_created', {576orderId: event.orderId,577total: event.total,578});579}580}581```582583Reference: [NestJS Events](https://docs.nestjs.com/techniques/events)584585---586587### 1.6 Use Repository Pattern for Data Access588589**Impact: HIGH** — Decouples business logic from database590591Create custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code.592593**Incorrect (complex queries in services):**594595```typescript596// Complex queries in services597@Injectable()598export class UsersService {599constructor(600@InjectRepository(User) private repo: Repository<User>,601) {}602603async findActiveWithOrders(minOrders: number): Promise<User[]> {604// Complex query logic mixed with business logic605return this.repo606.createQueryBuilder('user')607.leftJoinAndSelect('user.orders', 'order')608.where('user.isActive = :active', { active: true })609.andWhere('user.deletedAt IS NULL')610.groupBy('user.id')611.having('COUNT(order.id) >= :min', { min: minOrders })612.orderBy('user.createdAt', 'DESC')613.getMany();614}615616// Service becomes bloated with query logic617}618```619620**Correct (custom repository with encapsulated queries):**621622```typescript623// Custom repository with encapsulated queries624@Injectable()625export class UsersRepository {626constructor(627@InjectRepository(User) private repo: Repository<User>,628) {}629630async findById(id: string): Promise<User | null> {631return this.repo.findOne({ where: { id } });632}633634async findByEmail(email: string): Promise<User | null> {635return this.repo.findOne({ where: { email } });636}637638async findActiveWithMinOrders(minOrders: number): Promise<User[]> {639return this.repo640.createQueryBuilder('user')641.leftJoinAndSelect('user.orders', 'order')642.where('user.isActive = :active', { active: true })643.andWhere('user.deletedAt IS NULL')644.groupBy('user.id')645.having('COUNT(order.id) >= :min', { min: minOrders })646.orderBy('user.createdAt', 'DESC')647.getMany();648}649650async save(user: User): Promise<User> {651return this.repo.save(user);652}653}654655// Clean service with business logic only656@Injectable()657export class UsersService {658constructor(private usersRepo: UsersRepository) {}659660async getActiveUsersWithOrders(): Promise<User[]> {661return this.usersRepo.findActiveWithMinOrders(1);662}663664async create(dto: CreateUserDto): Promise<User> {665const existing = await this.usersRepo.findByEmail(dto.email);666if (existing) {667throw new ConflictException('Email already registered');668}669670const user = new User();671user.email = dto.email;672user.name = dto.name;673return this.usersRepo.save(user);674}675}676```677678Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)679680---681682## 2. Dependency Injection683684**Section Impact: CRITICAL**685686### 2.1 Avoid Service Locator Anti-Pattern687688**Impact: HIGH** — Hides dependencies and breaks testability689690Avoid using `ModuleRef.get()` or global containers to resolve dependencies at runtime. This hides dependencies, makes code harder to test, and breaks the benefits of dependency injection. Use constructor injection instead.691692**Incorrect (service locator anti-pattern):**693694```typescript695// Use ModuleRef to get dependencies dynamically696@Injectable()697export class OrdersService {698constructor(private moduleRef: ModuleRef) {}699700async createOrder(dto: CreateOrderDto): Promise<Order> {701// Dependencies are hidden - not visible in constructor702const usersService = this.moduleRef.get(UsersService);703const inventoryService = this.moduleRef.get(InventoryService);704const paymentService = this.moduleRef.get(PaymentService);705706const user = await usersService.findOne(dto.userId);707// ... rest of logic708}709}710711// Global singleton container712class ServiceContainer {713private static instance: ServiceContainer;714private services = new Map<string, any>();715716static getInstance(): ServiceContainer {717if (!this.instance) {718this.instance = new ServiceContainer();719}720return this.instance;721}722723get<T>(key: string): T {724return this.services.get(key);725}726}727```728729**Correct (constructor injection with explicit dependencies):**730731```typescript732// Use constructor injection - dependencies are explicit733@Injectable()734export class OrdersService {735constructor(736private usersService: UsersService,737private inventoryService: InventoryService,738private paymentService: PaymentService,739) {}740741async createOrder(dto: CreateOrderDto): Promise<Order> {742const user = await this.usersService.findOne(dto.userId);743const inventory = await this.inventoryService.check(dto.items);744// Dependencies are clear and testable745}746}747748// Easy to test with mocks749describe('OrdersService', () => {750let service: OrdersService;751752beforeEach(async () => {753const module = await Test.createTestingModule({754providers: [755OrdersService,756{ provide: UsersService, useValue: mockUsersService },757{ provide: InventoryService, useValue: mockInventoryService },758{ provide: PaymentService, useValue: mockPaymentService },759],760}).compile();761762service = module.get(OrdersService);763});764});765766// VALID: Factory pattern for dynamic instantiation767@Injectable()768export class HandlerFactory {769constructor(private moduleRef: ModuleRef) {}770771getHandler(type: string): Handler {772switch (type) {773case 'email':774return this.moduleRef.get(EmailHandler);775case 'sms':776return this.moduleRef.get(SmsHandler);777default:778return this.moduleRef.get(DefaultHandler);779}780}781}782```783784Reference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref)785786---787788### 2.2 Apply Interface Segregation Principle789790**Impact: HIGH** — Reduces coupling and improves testability by 30-50%791792Clients should not be forced to depend on interfaces they don't use. In NestJS, this means keeping interfaces small and focused on specific capabilities rather than creating "fat" interfaces that bundle unrelated methods. When a service only needs to send emails, it shouldn't depend on an interface that also includes SMS, push notifications, and logging. Split large interfaces into role-based ones.793794**Incorrect (fat interface forcing unused dependencies):**795796```typescript797// Fat interface - forces all consumers to depend on everything798interface NotificationService {799sendEmail(to: string, subject: string, body: string): Promise<void>;800sendSms(phone: string, message: string): Promise<void>;801sendPush(userId: string, notification: PushPayload): Promise<void>;802sendSlack(channel: string, message: string): Promise<void>;803logNotification(type: string, payload: any): Promise<void>;804getDeliveryStatus(id: string): Promise<DeliveryStatus>;805retryFailed(id: string): Promise<void>;806scheduleNotification(dto: ScheduleDto): Promise<string>;807}808809// Consumer only needs email, but must mock everything for tests810@Injectable()811export class OrdersService {812constructor(813private notifications: NotificationService, // Depends on 8 methods, uses 1814) {}815816async confirmOrder(order: Order): Promise<void> {817await this.notifications.sendEmail(818order.customer.email,819'Order Confirmed',820`Your order ${order.id} has been confirmed.`,821);822}823}824825// Testing is painful - must mock unused methods826const mockNotificationService = {827sendEmail: jest.fn(),828sendSms: jest.fn(), // Never used, but required829sendPush: jest.fn(), // Never used, but required830sendSlack: jest.fn(), // Never used, but required831logNotification: jest.fn(), // Never used, but required832getDeliveryStatus: jest.fn(), // Never used, but required833retryFailed: jest.fn(), // Never used, but required834scheduleNotification: jest.fn(), // Never used, but required835};836```837838**Correct (segregated interfaces by capability):**839840```typescript841// Segregated interfaces - each focused on one capability842interface EmailSender {843sendEmail(to: string, subject: string, body: string): Promise<void>;844}845846interface SmsSender {847sendSms(phone: string, message: string): Promise<void>;848}849850interface PushSender {851sendPush(userId: string, notification: PushPayload): Promise<void>;852}853854interface NotificationLogger {855logNotification(type: string, payload: any): Promise<void>;856}857858interface NotificationScheduler {859scheduleNotification(dto: ScheduleDto): Promise<string>;860}861862// Implementation can implement multiple interfaces863@Injectable()864export class NotificationService implements EmailSender, SmsSender, PushSender {865async sendEmail(to: string, subject: string, body: string): Promise<void> {866// Email implementation867}868869async sendSms(phone: string, message: string): Promise<void> {870// SMS implementation871}872873async sendPush(userId: string, notification: PushPayload): Promise<void> {874// Push implementation875}876}877878// Or separate implementations879@Injectable()880export class SendGridEmailService implements EmailSender {881async sendEmail(to: string, subject: string, body: string): Promise<void> {882// SendGrid-specific implementation883}884}885886// Consumer depends only on what it needs887@Injectable()888export class OrdersService {889constructor(890@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency891) {}892893async confirmOrder(order: Order): Promise<void> {894await this.emailSender.sendEmail(895order.customer.email,896'Order Confirmed',897`Your order ${order.id} has been confirmed.`,898);899}900}901902// Testing is simple - only mock what's used903const mockEmailSender: EmailSender = {904sendEmail: jest.fn(),905};906907// Module registration with tokens908export const EMAIL_SENDER = Symbol('EMAIL_SENDER');909export const SMS_SENDER = Symbol('SMS_SENDER');910911@Module({912providers: [913{ provide: EMAIL_SENDER, useClass: SendGridEmailService },914{ provide: SMS_SENDER, useClass: TwilioSmsService },915],916exports: [EMAIL_SENDER, SMS_SENDER],917})918export class NotificationModule {}919```920921**Combining interfaces when needed:**922923```typescript924// Sometimes a consumer legitimately needs multiple capabilities925interface EmailAndSmsSender extends EmailSender, SmsSender {}926927// Or use intersection types928type MultiChannelSender = EmailSender & SmsSender & PushSender;929930// Consumer that genuinely needs multiple channels931@Injectable()932export class AlertService {933constructor(934@Inject(MULTI_CHANNEL_SENDER)935private sender: EmailSender & SmsSender,936) {}937938async sendCriticalAlert(user: User, message: string): Promise<void> {939await Promise.all([940this.sender.sendEmail(user.email, 'Critical Alert', message),941this.sender.sendSms(user.phone, message),942]);943}944}945```946947Reference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)948949---950951### 2.3 Honor Liskov Substitution Principle952953**Impact: HIGH** — Ensures implementations are truly interchangeable without breaking callers954955Subtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations.956957**Incorrect (implementation violates the contract):**958959```typescript960// Base interface with clear contract961interface PaymentGateway {962/**963* Charges the specified amount.964* @returns PaymentResult on success965* @throws PaymentFailedException on payment failure966*/967charge(amount: number, currency: string): Promise<PaymentResult>;968}969970// Production implementation - follows the contract971@Injectable()972export class StripeService implements PaymentGateway {973async charge(amount: number, currency: string): Promise<PaymentResult> {974const response = await this.stripe.charges.create({ amount, currency });975return { success: true, transactionId: response.id, amount };976}977}978979// Mock that violates LSP - different behavior!980@Injectable()981export class MockPaymentService implements PaymentGateway {982async charge(amount: number, currency: string): Promise<PaymentResult> {983// VIOLATION 1: Throws for valid input (contract says return PaymentResult)984if (amount > 1000) {985throw new Error('Mock does not support large amounts');986}987988// VIOLATION 2: Returns null instead of PaymentResult989if (currency !== 'USD') {990return null as any; // Real service would convert or reject properly991}992993// VIOLATION 3: Missing required field994return { success: true } as PaymentResult; // Missing transactionId!995}996}997998// Consumer trusts the contract999@Injectable()1000export class OrdersService {1001constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}10021003async checkout(order: Order): Promise<void> {1004const result = await this.payment.charge(order.total, order.currency);1005// These fail with MockPaymentService:1006await this.saveTransaction(result.transactionId); // undefined!1007await this.sendReceipt(result); // might be null!1008}1009}1010```10111012**Correct (implementations honor the contract):**10131014```typescript1015// Well-defined interface with documented behavior1016interface PaymentGateway {1017/**1018* Charges the specified amount.1019* @param amount - Amount in smallest currency unit (cents)1020* @param currency - ISO 4217 currency code1021* @returns PaymentResult with transactionId, success status, and amount1022* @throws PaymentFailedException if charge is declined1023* @throws InvalidCurrencyException if currency is not supported1024*/1025charge(amount: number, currency: string): Promise<PaymentResult>;10261027/**1028* Refunds a previous charge.1029* @throws TransactionNotFoundException if transactionId is invalid1030*/1031refund(transactionId: string, amount?: number): Promise<RefundResult>;1032}10331034// Production implementation1035@Injectable()1036export class StripeService implements PaymentGateway {1037async charge(amount: number, currency: string): Promise<PaymentResult> {1038try {1039const response = await this.stripe.charges.create({ amount, currency });1040return {1041success: true,1042transactionId: response.id,1043amount: response.amount,1044};1045} catch (error) {1046if (error.type === 'card_error') {1047throw new PaymentFailedException(error.message);1048}1049throw error;1050}1051}10521053async refund(transactionId: string, amount?: number): Promise<RefundResult> {1054// Implementation...1055}1056}10571058// Mock that honors LSP - same contract, same behavior shape1059@Injectable()1060export class MockPaymentService implements PaymentGateway {1061private transactions = new Map<string, PaymentResult>();10621063async charge(amount: number, currency: string): Promise<PaymentResult> {1064// Honor the contract: validate currency like real service would1065if (!['USD', 'EUR', 'GBP'].includes(currency)) {1066throw new InvalidCurrencyException(`Unsupported currency: ${currency}`);1067}10681069// Simulate decline for specific test scenarios1070if (amount === 99999) {1071throw new PaymentFailedException('Card declined (test scenario)');1072}10731074// Return same shape as production1075const result: PaymentResult = {1076success: true,1077transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`,1078amount,1079};10801081this.transactions.set(result.transactionId, result);1082return result;1083}10841085async refund(transactionId: string, amount?: number): Promise<RefundResult> {1086// Honor the contract: throw if transaction not found1087if (!this.transactions.has(transactionId)) {1088throw new TransactionNotFoundException(transactionId);1089}10901091return {1092success: true,1093refundId: `refund_${transactionId}`,1094amount: amount ?? this.transactions.get(transactionId)!.amount,1095};1096}1097}10981099// Consumer can swap implementations safely1100@Injectable()1101export class OrdersService {1102constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}11031104async checkout(order: Order): Promise<Order> {1105try {1106const result = await this.payment.charge(order.total, order.currency);1107// Works with both StripeService and MockPaymentService1108order.transactionId = result.transactionId;1109order.status = 'paid';1110return order;1111} catch (error) {1112if (error instanceof PaymentFailedException) {1113order.status = 'payment_failed';1114return order;1115}1116throw error;1117}1118}1119}1120```11211122**Testing LSP compliance:**11231124```typescript1125// Shared test suite that any implementation must pass1126function testPaymentGatewayContract(1127createGateway: () => PaymentGateway,1128) {1129describe('PaymentGateway contract', () => {1130let gateway: PaymentGateway;11311132beforeEach(() => {1133gateway = createGateway();1134});11351136it('returns PaymentResult with all required fields', async () => {1137const result = await gateway.charge(1000, 'USD');1138expect(result).toHaveProperty('success');1139expect(result).toHaveProperty('transactionId');1140expect(result).toHaveProperty('amount');1141expect(typeof result.transactionId).toBe('string');1142});11431144it('throws InvalidCurrencyException for unsupported currency', async () => {1145await expect(gateway.charge(1000, 'INVALID'))1146.rejects.toThrow(InvalidCurrencyException);1147});11481149it('throws TransactionNotFoundException for invalid refund', async () => {1150await expect(gateway.refund('nonexistent'))1151.rejects.toThrow(TransactionNotFoundException);1152});1153});1154}11551156// Run against all implementations1157describe('StripeService', () => {1158testPaymentGatewayContract(() => new StripeService(mockStripeClient));1159});11601161describe('MockPaymentService', () => {1162testPaymentGatewayContract(() => new MockPaymentService());1163});1164```11651166Reference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)11671168---11691170### 2.4 Prefer Constructor Injection11711172**Impact: CRITICAL** — Required for proper DI and testing11731174Always use constructor injection over property injection. Constructor injection makes dependencies explicit, enables TypeScript type checking, ensures dependencies are available when the class is instantiated, and improves testability. This is required for proper DI, testing, and TypeScript support.11751176**Incorrect (property injection with hidden dependencies):**11771178```typescript1179// Property injection - avoid unless necessary1180@Injectable()1181export class UsersService {1182@Inject()1183private userRepo: UserRepository; // Hidden dependency11841185@Inject('CONFIG')1186private config: ConfigType; // Also hidden11871188async findAll() {1189return this.userRepo.find();1190}1191}11921193// Problems:1194// 1. Dependencies not visible in constructor1195// 2. Service can be instantiated without dependencies in tests1196// 3. TypeScript can't enforce dependency types at instantiation1197```11981199**Correct (constructor injection with explicit dependencies):**12001201```typescript1202// Constructor injection - explicit and testable1203@Injectable()1204export class UsersService {1205constructor(1206private readonly userRepo: UserRepository,1207@Inject('CONFIG') private readonly config: ConfigType,1208) {}12091210async findAll(): Promise<User[]> {1211return this.userRepo.find();1212}1213}12141215// Testing is straightforward1216describe('UsersService', () => {1217let service: UsersService;1218let mockRepo: jest.Mocked<UserRepository>;12191220beforeEach(() => {1221mockRepo = {1222find: jest.fn(),1223save: jest.fn(),1224} as any;12251226service = new UsersService(mockRepo, { dbUrl: 'test' });1227});12281229it('should find all users', async () => {1230mockRepo.find.mockResolvedValue([{ id: '1', name: 'Test' }]);1231const result = await service.findAll();1232expect(result).toHaveLength(1);1233});1234});12351236// Only use property injection for optional dependencies1237@Injectable()1238export class LoggingService {1239@Optional()1240@Inject('ANALYTICS')1241private analytics?: AnalyticsService;12421243log(message: string) {1244console.log(message);1245this.analytics?.track('log', message); // Optional enhancement1246}1247}1248```12491250Reference: [NestJS Providers](https://docs.nestjs.com/providers)12511252---12531254### 2.5 Understand Provider Scopes12551256**Impact: CRITICAL** — Prevents data leaks and performance issues12571258NestJS has three provider scopes: DEFAULT (singleton), REQUEST (per-request instance), and TRANSIENT (new instance for each injection). Most providers should be singletons. Request-scoped providers have performance implications as they bubble up through the dependency tree. Understanding scopes prevents memory leaks and incorrect data sharing.12591260**Incorrect (wrong scope usage):**12611262```typescript1263// Request-scoped when not needed (performance hit)1264@Injectable({ scope: Scope.REQUEST })1265export class UsersService {1266// This creates a new instance for EVERY request1267// All dependencies also become request-scoped1268async findAll() {1269return this.userRepo.find();1270}1271}12721273// Singleton with mutable request state1274@Injectable() // Default: singleton1275export class RequestContextService {1276private userId: string; // DANGER: Shared across all requests!12771278setUser(userId: string) {1279this.userId = userId; // Overwrites for all concurrent requests1280}12811282getUser() {1283return this.userId; // Returns wrong user!1284}1285}1286```12871288**Correct (appropriate scope for each use case):**12891290```typescript1291// Singleton for stateless services (default, most common)1292@Injectable()1293export class UsersService {1294constructor(private readonly userRepo: UserRepository) {}12951296async findById(id: string): Promise<User> {1297return this.userRepo.findOne({ where: { id } });1298}1299}13001301// Request-scoped ONLY when you need request context1302@Injectable({ scope: Scope.REQUEST })1303export class RequestContextService {1304private userId: string;13051306setUser(userId: string) {1307this.userId = userId;1308}13091310getUser(): string {1311return this.userId;1312}1313}13141315// Better: Use NestJS built-in request context1316import { REQUEST } from '@nestjs/core';1317import { Request } from 'express';13181319@Injectable({ scope: Scope.REQUEST })1320export class AuditService {1321constructor(@Inject(REQUEST) private request: Request) {}13221323log(action: string) {1324console.log(`User ${this.request.user?.id} performed ${action}`);1325}1326}13271328// Best: Use ClsModule for async context (no scope bubble-up)1329import { ClsService } from 'nestjs-cls';13301331@Injectable() // Stays singleton!1332export class AuditService {1333constructor(private cls: ClsService) {}13341335log(action: string) {1336const userId = this.cls.get('userId');1337console.log(`User ${userId} performed ${action}`);1338}1339}1340```13411342Reference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes)13431344---13451346### 2.6 Use Injection Tokens for Interfaces13471348**Impact: HIGH** — Enables interface-based DI at runtime13491350TypeScript interfaces are erased at compile time and can't be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces. This enables swapping implementations for testing or different environments.13511352**Incorrect (interface can't be used as token):**13531354```typescript1355// Interface can't be used as injection token1356interface PaymentGateway {1357charge(amount: number): Promise<PaymentResult>;1358}13591360@Injectable()1361export class StripeService implements PaymentGateway {1362charge(amount: number) { /* ... */ }1363}13641365@Injectable()1366export class OrdersService {1367// This WON'T work - PaymentGateway doesn't exist at runtime1368constructor(private payment: PaymentGateway) {}1369}1370```13711372**Correct (symbol tokens or abstract classes):**13731374```typescript1375// Option 1: String/Symbol tokens (most flexible)1376export const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY');13771378export interface PaymentGateway {1379charge(amount: number): Promise<PaymentResult>;1380}13811382@Injectable()1383export class StripeService implements PaymentGateway {1384async charge(amount: number): Promise<PaymentResult> {1385// Stripe implementation1386}1387}13881389@Injectable()1390export class MockPaymentService implements PaymentGateway {1391async charge(amount: number): Promise<PaymentResult> {1392return { success: true, id: 'mock-id' };1393}1394}13951396// Module registration1397@Module({1398providers: [1399{1400provide: PAYMENT_GATEWAY,1401useClass: process.env.NODE_ENV === 'test'1402? MockPaymentService1403: StripeService,1404},1405],1406exports: [PAYMENT_GATEWAY],1407})1408export class PaymentModule {}14091410// Injection1411@Injectable()1412export class OrdersService {1413constructor(1414@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,1415) {}14161417async createOrder(dto: CreateOrderDto) {1418await this.payment.charge(dto.amount);1419}1420}14211422// Option 2: Abstract class (carries runtime type info)1423export abstract class PaymentGateway {1424abstract charge(amount: number): Promise<PaymentResult>;1425}14261427@Injectable()1428export class StripeService extends PaymentGateway {1429async charge(amount: number): Promise<PaymentResult> {1430// Implementation1431}1432}14331434// No @Inject needed with abstract class1435@Injectable()1436export class OrdersService {1437constructor(private payment: PaymentGateway) {}1438}1439```14401441Reference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)14421443---14441445## 3. Error Handling14461447**Section Impact: HIGH**14481449### 3.1 Handle Async Errors Properly14501451**Impact: HIGH** — Prevents process crashes from unhandled rejections14521453NestJS automatically catches errors from async route handlers, but errors from background tasks, event handlers, and manually created promises can crash your application. Always handle async errors explicitly and use global handlers as a safety net.14541455**Incorrect (fire-and-forget without error handling):**14561457```typescript1458// Fire-and-forget without error handling1459@Injectable()1460export class UsersService {1461async createUser(dto: CreateUserDto): Promise<User> {1462const user = await this.repo.save(dto);14631464// Fire and forget - if this fails, error is unhandled!1465this.emailService.sendWelcome(user.email);14661467return user;1468}1469}14701471// Unhandled promise in event handler1472@Injectable()1473export class OrdersService {1474@OnEvent('order.created')1475handleOrderCreated(event: OrderCreatedEvent) {1476// This returns a promise but it's not awaited!1477this.processOrder(event);1478// Errors will crash the process1479}14801481private async processOrder(event: OrderCreatedEvent): Promise<void> {1482await this.inventoryService.reserve(event.items);1483await this.notificationService.send(event.userId);1484}1485}14861487// Missing try-catch in scheduled tasks1488@Cron('0 0 * * *')1489async dailyCleanup(): Promise<void> {1490await this.cleanupService.run();1491// If this throws, no error handling1492}1493```14941495**Correct (explicit async error handling):**14961497```typescript1498// Handle fire-and-forget with explicit catch1499@Injectable()1500export class UsersService {1501private readonly logger = new Logger(UsersService.name);15021503async createUser(dto: CreateUserDto): Promise<User> {1504const user = await this.repo.save(dto);15051506// Explicitly catch and log errors1507this.emailService.sendWelcome(user.email).catch((error) => {1508this.logger.error('Failed to send welcome email', error.stack);1509// Optionally queue for retry1510});15111512return user;1513}1514}15151516// Properly handle async event handlers1517@Injectable()1518export class OrdersService {1519private readonly logger = new Logger(OrdersService.name);15201521@OnEvent('order.created')1522async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {1523try {1524await this.processOrder(event);1525} catch (error) {1526this.logger.error('Failed to process order', { event, error });1527// Don't rethrow - would crash the process1528await this.deadLetterQueue.add('order.created', event);1529}1530}1531}15321533// Safe scheduled tasks1534@Injectable()1535export class CleanupService {1536private readonly logger = new Logger(CleanupService.name);15371538@Cron('0 0 * * *')1539async dailyCleanup(): Promise<void> {1540try {1541await this.cleanupService.run();1542this.logger.log('Daily cleanup completed');1543} catch (error) {1544this.logger.error('Daily cleanup failed', error.stack);1545// Alert or retry logic1546}1547}1548}15491550// Global unhandled rejection handler in main.ts1551async function bootstrap() {1552const app = await NestFactory.create(AppModule);1553const logger = new Logger('Bootstrap');15541555process.on('unhandledRejection', (reason, promise) => {1556logger.error('Unhandled Rejection at:', promise, 'reason:', reason);1557});15581559process.on('uncaughtException', (error) => {1560logger.error('Uncaught Exception:', error);1561process.exit(1);1562});15631564await app.listen(3000);1565}1566```15671568Reference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection)15691570---15711572### 3.2 Throw HTTP Exceptions from Services15731574**Impact: HIGH** — Keeps controllers thin and simplifies error handling15751576It's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes.15771578**Incorrect (return error objects instead of throwing):**15791580```typescript1581// Return error objects instead of throwing1582@Injectable()1583export class UsersService {1584async findById(id: string): Promise<{ user?: User; error?: string }> {1585const user = await this.repo.findOne({ where: { id } });1586if (!user) {1587return { error: 'User not found' }; // Controller must check this1588}1589return { user };1590}1591}15921593@Controller('users')1594export class UsersController {1595@Get(':id')1596async findOne(@Param('id') id: string) {1597const result = await this.usersService.findById(id);1598if (result.error) {1599throw new NotFoundException(result.error);1600}1601return result.user;1602}1603}1604```16051606**Correct (throw exceptions directly from service):**16071608```typescript1609// Throw exceptions directly from service1610@Injectable()1611export class UsersService {1612constructor(private readonly repo: UserRepository) {}16131614async findById(id: string): Promise<User> {1615const user = await this.repo.findOne({ where: { id } });1616if (!user) {1617throw new NotFoundException(`User #${id} not found`);1618}1619return user;1620}16211622async create(dto: CreateUserDto): Promise<User> {1623const existing = await this.repo.findOne({1624where: { email: dto.email },1625});1626if (existing) {1627throw new ConflictException('Email already registered');1628}1629return this.repo.save(dto);1630}16311632async update(id: string, dto: UpdateUserDto): Promise<User> {1633const user = await this.findById(id); // Throws if not found1634Object.assign(user, dto);1635return this.repo.save(user);1636}1637}16381639// Controller stays thin1640@Controller('users')1641export class UsersController {1642@Get(':id')1643findOne(@Param('id') id: string): Promise<User> {1644return this.usersService.findById(id);1645}16461647@Post()1648create(@Body() dto: CreateUserDto): Promise<User> {1649return this.usersService.create(dto);1650}1651}16521653// For layer-agnostic services, use domain exceptions1654export class EntityNotFoundException extends Error {1655constructor(1656public readonly entity: string,1657public readonly id: string,1658) {1659super(`${entity} with ID "${id}" not found`);1660}1661}16621663// Map to HTTP in exception filter1664@Catch(EntityNotFoundException)1665export class EntityNotFoundFilter implements ExceptionFilter {1666catch(exception: EntityNotFoundException, host: ArgumentsHost) {1667const ctx = host.switchToHttp();1668const response = ctx.getResponse<Response>();16691670response.status(404).json({1671statusCode: 404,1672message: exception.message,1673entity: exception.entity,1674id: exception.id,1675});1676}1677}1678```16791680Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)16811682---16831684### 3.3 Use Exception Filters for Error Handling16851686**Impact: HIGH** — Consistent, centralized error handling16871688Never catch exceptions and manually format error responses in controllers. Use NestJS exception filters to handle errors consistently across your application. Create custom exception filters for specific error types and a global filter for unhandled exceptions.16891690**Incorrect (manual error handling in controllers):**16911692```typescript1693// Manual error handling in controllers1694@Controller('users')1695export class UsersController {1696@Get(':id')1697async findOne(@Param('id') id: string, @Res() res: Response) {1698try {1699const user = await this.usersService.findById(id);1700if (!user) {1701return res.status(404).json({1702statusCode: 404,1703message: 'User not found',1704});1705}1706return res.json(user);1707} catch (error) {1708console.error(error);1709return res.status(500).json({1710statusCode: 500,1711message: 'Internal server error',1712});1713}1714}1715}1716```17171718**Correct (exception filters with consistent handling):**17191720```typescript1721// Use built-in and custom exceptions1722@Controller('users')1723export class UsersController {1724@Get(':id')1725async findOne(@Param('id') id: string): Promise<User> {1726const user = await this.usersService.findById(id);1727if (!user) {1728throw new NotFoundException(`User #${id} not found`);1729}1730return user;1731}1732}17331734// Custom domain exception1735export class UserNotFoundException extends NotFoundException {1736constructor(userId: string) {1737super({1738statusCode: 404,1739error: 'Not Found',1740message: `User with ID "${userId}" not found`,1741code: 'USER_NOT_FOUND',1742});1743}1744}17451746// Custom exception filter for domain errors1747@Catch(DomainException)1748export class DomainExceptionFilter implements ExceptionFilter {1749catch(exception: DomainException, host: ArgumentsHost) {1750const ctx = host.switchToHttp();1751const response = ctx.getResponse<Response>();1752const request = ctx.getRequest<Request>();17531754const status = exception.getStatus?.() || 400;17551756response.status(status).json({1757statusCode: status,1758code: exception.code,1759message: exception.message,1760timestamp: new Date().toISOString(),1761path: request.url,1762});1763}1764}17651766// Global exception filter for unhandled errors1767@Catch()1768export class AllExceptionsFilter implements ExceptionFilter {1769constructor(private readonly logger: Logger) {}17701771catch(exception: unknown, host: ArgumentsHost) {1772const ctx = host.switchToHttp();1773const response = ctx.getResponse<Response>();1774const request = ctx.getRequest<Request>();17751776const status =1777exception instanceof HttpException1778? exception.getStatus()1779: HttpStatus.INTERNAL_SERVER_ERROR;17801781const message =1782exception instanceof HttpException1783? exception.message1784: 'Internal server error';17851786this.logger.error(1787`${request.method} ${request.url}`,1788exception instanceof Error ? exception.stack : exception,1789);17901791response.status(status).json({1792statusCode: status,1793message,1794timestamp: new Date().toISOString(),1795path: request.url,1796});1797}1798}17991800// Register globally in main.ts1801app.useGlobalFilters(1802new AllExceptionsFilter(app.get(Logger)),1803new DomainExceptionFilter(),1804);18051806// Or via module1807@Module({1808providers: [1809{1810provide: APP_FILTER,1811useClass: AllExceptionsFilter,1812},1813],1814})1815export class AppModule {}1816```18171818Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)18191820---18211822## 4. Security18231824**Section Impact: HIGH**18251826### 4.1 Implement Secure JWT Authentication18271828**Impact: CRITICAL** — Essential for secure APIs18291830Use `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads.18311832**Incorrect (insecure JWT implementation):**18331834```typescript1835// Hardcode secrets1836@Module({1837imports: [1838JwtModule.register({1839secret: 'my-secret-key', // Exposed in code1840signOptions: { expiresIn: '7d' }, // Too long1841}),1842],1843})1844export class AuthModule {}18451846// Store sensitive data in JWT1847async login(user: User): Promise<{ accessToken: string }> {1848const payload = {1849sub: user.id,1850email: user.email,1851password: user.password, // NEVER include password!1852ssn: user.ssn, // NEVER include sensitive data!1853isAdmin: user.isAdmin, // Can be tampered if not verified1854};1855return { accessToken: this.jwtService.sign(payload) };1856}18571858// Skip token validation1859@Injectable()1860export class JwtStrategy extends PassportStrategy(Strategy) {1861constructor() {1862super({1863jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),1864secretOrKey: 'my-secret',1865});1866}18671868async validate(payload: any): Promise<any> {1869return payload; // No validation of user existence1870}1871}1872```18731874**Correct (secure JWT with refresh tokens):**18751876```typescript1877// Secure JWT configuration1878@Module({1879imports: [1880JwtModule.registerAsync({1881imports: [ConfigModule],1882inject: [ConfigService],1883useFactory: (config: ConfigService) => ({1884secret: config.get<string>('JWT_SECRET'),1885signOptions: {1886expiresIn: '15m', // Short-lived access tokens1887issuer: config.get<string>('JWT_ISSUER'),1888audience: config.get<string>('JWT_AUDIENCE'),1889},1890}),1891}),1892PassportModule.register({ defaultStrategy: 'jwt' }),1893],1894})1895export class AuthModule {}18961897// Minimal JWT payload1898@Injectable()1899export class AuthService {1900async login(user: User): Promise<TokenResponse> {1901// Only include necessary, non-sensitive data1902const payload: JwtPayload = {1903sub: user.id,1904email: user.email,1905roles: user.roles,1906iat: Math.floor(Date.now() / 1000),1907};19081909const accessToken = this.jwtService.sign(payload);1910const refreshToken = await this.createRefreshToken(user.id);19111912return { accessToken, refreshToken, expiresIn: 900 };1913}19141915private async createRefreshToken(userId: string): Promise<string> {1916const token = randomBytes(32).toString('hex');1917const hashedToken = await bcrypt.hash(token, 10);19181919await this.refreshTokenRepo.save({1920userId,1921token: hashedToken,1922expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days1923});19241925return token;1926}1927}19281929// Proper JWT strategy with validation1930@Injectable()1931export class JwtStrategy extends PassportStrategy(Strategy) {1932constructor(1933private config: ConfigService,1934private usersService: UsersService,1935) {1936super({1937jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),1938secretOrKey: config.get<string>('JWT_SECRET'),1939ignoreExpiration: false,1940issuer: config.get<string>('JWT_ISSUER'),1941audience: config.get<string>('JWT_AUDIENCE'),1942});1943}19441945async validate(payload: JwtPayload): Promise<User> {1946// Verify user still exists and is active1947const user = await this.usersService.findById(payload.sub);19481949if (!user || !user.isActive) {1950throw new UnauthorizedException('User not found or inactive');1951}19521953// Verify token wasn't issued before password change1954if (user.passwordChangedAt) {1955const tokenIssuedAt = new Date(payload.iat * 1000);1956if (tokenIssuedAt < user.passwordChangedAt) {1957throw new UnauthorizedException('Token invalidated by password change');1958}1959}19601961return user;1962}1963}1964```19651966Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)19671968---19691970### 4.2 Implement Rate Limiting19711972**Impact: HIGH** — Protects against abuse and ensures fair resource usage19731974Use `@nestjs/throttler` to limit request rates per client. Apply different limits for different endpoints - stricter for auth endpoints, more relaxed for read operations. Consider using Redis for distributed rate limiting in clustered deployments.19751976**Incorrect (no rate limiting on sensitive endpoints):**19771978```typescript1979// No rate limiting on sensitive endpoints1980@Controller('auth')1981export class AuthController {1982@Post('login')1983async login(@Body() dto: LoginDto): Promise<TokenResponse> {1984// Attackers can brute-force credentials1985return this.authService.login(dto);1986}19871988@Post('forgot-password')1989async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {1990// Can be abused to spam users with emails1991return this.authService.sendResetEmail(dto.email);1992}1993}19941995// Same limits for all endpoints1996@UseGuards(ThrottlerGuard)1997@Controller('api')1998export class ApiController {1999@Get('public-data')2000async getPublic() {} // Should allow more requests20012002@Post('process-payment')2003async payment() {} // Should be more restrictive2004}2005```20062007**Correct (configured throttler with endpoint-specific limits):**20082009```typescript2010// Configure throttler globally with multiple limits2011import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';20122013@Module({2014imports: [2015ThrottlerModule.forRoot([2016{2017name: 'short',2018ttl: 1000, // 1 second2019limit: 3, // 3 requests per second2020},2021{2022name: 'medium',2023ttl: 10000, // 10 seconds2024limit: 20, // 20 requests per 10 seconds2025},2026{2027name: 'long',2028ttl: 60000, // 1 minute2029limit: 100, // 100 requests per minute2030},2031]),2032],2033providers: [2034{2035provide: APP_GUARD,2036useClass: ThrottlerGuard,2037},2038],2039})2040export class AppModule {}20412042// Override limits per endpoint2043@Controller('auth')2044export class AuthController {2045@Post('login')2046@Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute2047async login(@Body() dto: LoginDto): Promise<TokenResponse> {2048return this.authService.login(dto);2049}20502051@Post('forgot-password')2052@Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour2053async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {2054return this.authService.sendResetEmail(dto.email);2055}2056}20572058// Skip throttling for certain routes2059@Controller('health')2060export class HealthController {2061@Get()2062@SkipThrottle()2063check(): string {2064return 'OK';2065}2066}20672068// Custom throttle per user type2069@Injectable()2070export class CustomThrottlerGuard extends ThrottlerGuard {2071protected async getTracker(req: Request): Promise<string> {2072// Use user ID if authenticated, IP otherwise2073return req.user?.id || req.ip;2074}20752076protected async getLimit(context: ExecutionContext): Promise<number> {2077const request = context.switchToHttp().getRequest();20782079// Higher limits for authenticated users2080if (request.user) {2081return request.user.isPremium ? 1000 : 200;2082}20832084return 50; // Anonymous users2085}2086}2087```20882089Reference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)20902091---20922093### 4.3 Sanitize Output to Prevent XSS20942095**Impact: HIGH** — XSS vulnerabilities can compromise user sessions and data20962097While NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers.20982099**Incorrect (storing raw HTML without sanitization):**21002101```typescript2102// Store raw HTML from users2103@Injectable()2104export class CommentsService {2105async create(dto: CreateCommentDto): Promise<Comment> {2106// User can inject: <script>steal(document.cookie)</script>2107return this.repo.save({2108content: dto.content, // Raw, unsanitized2109authorId: dto.authorId,2110});2111}2112}21132114// Return HTML without sanitization2115@Controller('pages')2116export class PagesController {2117@Get(':slug')2118@Header('Content-Type', 'text/html')2119async getPage(@Param('slug') slug: string): Promise<string> {2120const page = await this.pagesService.findBySlug(slug);2121// If page.content contains user input, XSS is possible2122return `<html><body>${page.content}</body></html>`;2123}2124}21252126// Reflect user input in errors2127@Get(':id')2128async findOne(@Param('id') id: string): Promise<User> {2129const user = await this.repo.findOne({ where: { id } });2130if (!user) {2131// XSS if id contains malicious content and error is rendered2132throw new NotFoundException(`User ${id} not found`);2133}2134return user;2135}2136```21372138**Correct (sanitize content and use proper headers):**21392140```typescript2141// Sanitize HTML content before storage2142import * as sanitizeHtml from 'sanitize-html';21432144@Injectable()2145export class CommentsService {2146private readonly sanitizeOptions: sanitizeHtml.IOptions = {2147allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],2148allowedAttributes: {2149a: ['href', 'title'],2150},2151allowedSchemes: ['http', 'https', 'mailto'],2152};21532154async create(dto: CreateCommentDto): Promise<Comment> {2155return this.repo.save({2156content: sanitizeHtml(dto.content, this.sanitizeOptions),2157authorId: dto.authorId,2158});2159}2160}21612162// Use validation pipe to strip HTML2163import { Transform } from 'class-transformer';21642165export class CreatePostDto {2166@IsString()2167@MaxLength(1000)2168@Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))2169title: string;21702171@IsString()2172@Transform(({ value }) =>2173sanitizeHtml(value, {2174allowedTags: ['p', 'br', 'b', 'i', 'a'],2175allowedAttributes: { a: ['href'] },2176}),2177)2178content: string;2179}21802181// Set proper Content-Type headers2182@Controller('api')2183export class ApiController {2184@Get('data')2185@Header('Content-Type', 'application/json')2186async getData(): Promise<DataResponse> {2187// JSON response - browser won't execute scripts2188return this.service.getData();2189}2190}21912192// Sanitize error messages2193@Get(':id')2194async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {2195const user = await this.repo.findOne({ where: { id } });2196if (!user) {2197// UUID validation ensures safe format2198throw new NotFoundException('User not found');2199}2200return user;2201}22022203// Use Helmet for CSP headers2204import helmet from 'helmet';22052206async function bootstrap() {2207const app = await NestFactory.create(AppModule);22082209app.use(2210helmet({2211contentSecurityPolicy: {2212directives: {2213defaultSrc: ["'self'"],2214scriptSrc: ["'self'"],2215styleSrc: ["'self'", "'unsafe-inline'"],2216imgSrc: ["'self'", 'data:', 'https:'],2217},2218},2219}),2220);22212222await app.listen(3000);2223}2224```22252226Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)22272228---22292230### 4.4 Use Guards for Authentication and Authorization22312232**Impact: HIGH** — Enforces access control before handlers execute22332234Guards determine whether a request should be handled based on authentication state, roles, permissions, or other conditions. They run after middleware but before pipes and interceptors, making them ideal for access control. Use guards instead of manual checks in controllers.22352236**Incorrect (manual auth checks in every handler):**22372238```typescript2239// Manual auth checks in every handler2240@Controller('admin')2241export class AdminController {2242@Get('users')2243async getUsers(@Request() req) {2244if (!req.user) {2245throw new UnauthorizedException();2246}2247if (!req.user.roles.includes('admin')) {2248throw new ForbiddenException();2249}2250return this.adminService.getUsers();2251}22522253@Delete('users/:id')2254async deleteUser(@Request() req, @Param('id') id: string) {2255if (!req.user) {2256throw new UnauthorizedException();2257}2258if (!req.user.roles.includes('admin')) {2259throw new ForbiddenException();2260}2261return this.adminService.deleteUser(id);2262}2263}2264```22652266**Correct (guards with declarative decorators):**22672268```typescript2269// JWT Auth Guard2270@Injectable()2271export class JwtAuthGuard implements CanActivate {2272constructor(2273private jwtService: JwtService,2274private reflector: Reflector,2275) {}22762277async canActivate(context: ExecutionContext): Promise<boolean> {2278// Check for @Public() decorator2279const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [2280context.getHandler(),2281context.getClass(),2282]);2283if (isPublic) return true;22842285const request = context.switchToHttp().getRequest();2286const token = this.extractToken(request);22872288if (!token) {2289throw new UnauthorizedException('No token provided');2290}22912292try {2293request.user = await this.jwtService.verifyAsync(token);2294return true;2295} catch {2296throw new UnauthorizedException('Invalid token');2297}2298}22992300private extractToken(request: Request): string | undefined {2301const [type, token] = request.headers.authorization?.split(' ') ?? [];2302return type === 'Bearer' ? token : undefined;2303}2304}23052306// Roles Guard2307@Injectable()2308export class RolesGuard implements CanActivate {2309constructor(private reflector: Reflector) {}23102311canActivate(context: ExecutionContext): boolean {2312const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [2313context.getHandler(),2314context.getClass(),2315]);23162317if (!requiredRoles) return true;23182319const { user } = context.switchToHttp().getRequest();2320return requiredRoles.some((role) => user.roles?.includes(role));2321}2322}23232324// Decorators2325export const Public = () => SetMetadata('isPublic', true);2326export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);23272328// Register guards globally2329@Module({2330providers: [2331{ provide: APP_GUARD, useClass: JwtAuthGuard },2332{ provide: APP_GUARD, useClass: RolesGuard },2333],2334})2335export class AppModule {}23362337// Clean controller2338@Controller('admin')2339@Roles(Role.Admin) // Applied to all routes2340export class AdminController {2341@Get('users')2342getUsers(): Promise<User[]> {2343return this.adminService.getUsers();2344}23452346@Delete('users/:id')2347deleteUser(@Param('id') id: string): Promise<void> {2348return this.adminService.deleteUser(id);2349}23502351@Public() // Override: no auth required2352@Get('health')2353health() {2354return { status: 'ok' };2355}2356}2357```23582359Reference: [NestJS Guards](https://docs.nestjs.com/guards)23602361---23622363### 4.5 Validate All Input with DTOs and Pipes23642365**Impact: HIGH** — First line of defense against attacks23662367Always validate incoming data using class-validator decorators on DTOs and the global ValidationPipe. Never trust user input. Validate all request bodies, query parameters, and route parameters before processing.23682369**Incorrect (trust raw input without validation):**23702371```typescript2372// Trust raw input without validation2373@Controller('users')2374export class UsersController {2375@Post()2376create(@Body() body: any) {2377// body could contain anything - SQL injection, XSS, etc.2378return this.usersService.create(body);2379}23802381@Get()2382findAll(@Query() query: any) {2383// query.limit could be "'; DROP TABLE users; --"2384return this.usersService.findAll(query.limit);2385}2386}23872388// DTOs without validation decorators2389export class CreateUserDto {2390name: string; // No validation2391email: string; // Could be "not-an-email"2392age: number; // Could be "abc" or -9992393}2394```23952396**Correct (validated DTOs with global ValidationPipe):**23972398```typescript2399// Enable ValidationPipe globally in main.ts2400async function bootstrap() {2401const app = await NestFactory.create(AppModule);24022403app.useGlobalPipes(2404new ValidationPipe({2405whitelist: true, // Strip unknown properties2406forbidNonWhitelisted: true, // Throw on unknown properties2407transform: true, // Auto-transform to DTO types2408transformOptions: {2409enableImplicitConversion: true,2410},2411}),2412);24132414await app.listen(3000);2415}24162417// Create well-validated DTOs2418import {2419IsString,2420IsEmail,2421IsInt,2422Min,2423Max,2424IsOptional,2425MinLength,2426MaxLength,2427Matches,2428IsNotEmpty,2429} from 'class-validator';2430import { Transform, Type } from 'class-transformer';24312432export class CreateUserDto {2433@IsString()2434@IsNotEmpty()2435@MinLength(2)2436@MaxLength(100)2437@Transform(({ value }) => value?.trim())2438name: string;24392440@IsEmail()2441@Transform(({ value }) => value?.toLowerCase().trim())2442email: string;24432444@IsInt()2445@Min(0)2446@Max(150)2447age: number;24482449@IsString()2450@MinLength(8)2451@MaxLength(100)2452@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {2453message: 'Password must contain uppercase, lowercase, and number',2454})2455password: string;2456}24572458// Query DTO with defaults and transformation2459export class FindUsersQueryDto {2460@IsOptional()2461@IsString()2462@MaxLength(100)2463search?: string;24642465@IsOptional()2466@Type(() => Number)2467@IsInt()2468@Min(1)2469@Max(100)2470limit: number = 20;24712472@IsOptional()2473@Type(() => Number)2474@IsInt()2475@Min(0)2476offset: number = 0;2477}24782479// Param validation2480export class UserIdParamDto {2481@IsUUID('4')2482id: string;2483}24842485@Controller('users')2486export class UsersController {2487@Post()2488create(@Body() dto: CreateUserDto): Promise<User> {2489// dto is guaranteed to be valid2490return this.usersService.create(dto);2491}24922493@Get()2494findAll(@Query() query: FindUsersQueryDto): Promise<User[]> {2495// query.limit is a number, query.search is sanitized2496return this.usersService.findAll(query);2497}24982499@Get(':id')2500findOne(@Param() params: UserIdParamDto): Promise<User> {2501// params.id is a valid UUID2502return this.usersService.findById(params.id);2503}2504}2505```25062507Reference: [NestJS Validation](https://docs.nestjs.com/techniques/validation)25082509---25102511## 5. Performance25122513**Section Impact: HIGH**25142515### 5.1 Use Async Lifecycle Hooks Correctly25162517**Impact: HIGH** — Improper async handling blocks application startup25182519NestJS lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.) support async operations. However, misusing them can block application startup or cause race conditions. Understand the lifecycle order and use hooks appropriately.25202521**Incorrect (fire-and-forget async without await):**25222523```typescript2524// Fire-and-forget async without await2525@Injectable()2526export class DatabaseService implements OnModuleInit {2527onModuleInit() {2528// This runs but doesn't block - app starts before DB is ready!2529this.connect();2530}25312532private async connect() {2533await this.pool.connect();2534console.log('Database connected');2535}2536}25372538// Heavy blocking operations in constructor2539@Injectable()2540export class ConfigService {2541private config: Config;25422543constructor() {2544// BLOCKS entire module instantiation synchronously2545this.config = fs.readFileSync('config.json');2546}2547}2548```25492550**Correct (return promises from async hooks):**25512552```typescript2553// Return promise from async hooks2554@Injectable()2555export class DatabaseService implements OnModuleInit {2556private pool: Pool;25572558async onModuleInit(): Promise<void> {2559// NestJS waits for this to complete before continuing2560await this.pool.connect();2561console.log('Database connected');2562}25632564async onModuleDestroy(): Promise<void> {2565// Clean up resources on shutdown2566await this.pool.end();2567console.log('Database disconnected');2568}2569}25702571// Use onApplicationBootstrap for cross-module dependencies2572@Injectable()2573export class CacheWarmerService implements OnApplicationBootstrap {2574constructor(2575private cache: CacheService,2576private products: ProductsService,2577) {}25782579async onApplicationBootstrap(): Promise<void> {2580// All modules are initialized, safe to warm cache2581const products = await this.products.findPopular();2582await this.cache.warmup(products);2583}2584}25852586// Heavy init in async hooks, not constructor2587@Injectable()2588export class ConfigService implements OnModuleInit {2589private config: Config;25902591constructor() {2592// Keep constructor synchronous and fast2593}25942595async onModuleInit(): Promise<void> {2596// Async loading in lifecycle hook2597this.config = await this.loadConfig();2598}25992600private async loadConfig(): Promise<Config> {2601const file = await fs.promises.readFile('config.json');2602return JSON.parse(file.toString());2603}26042605get<T>(key: string): T {2606return this.config[key];2607}2608}26092610// Enable shutdown hooks in main.ts2611async function bootstrap() {2612const app = await NestFactory.create(AppModule);2613app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling2614await app.listen(3000);2615}2616```26172618Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)26192620---26212622### 5.2 Use Lazy Loading for Large Modules26232624**Impact: MEDIUM** — Improves startup time for large applications26252626NestJS supports lazy-loading modules, which defers initialization until first use. This is valuable for large applications where some features are rarely used, serverless deployments where cold start time matters, or when certain modules have heavy initialization costs.26272628**Incorrect (loading everything eagerly):**26292630```typescript2631// Load everything eagerly in a large app2632@Module({2633imports: [2634UsersModule,2635OrdersModule,2636PaymentsModule,2637ReportsModule, // Heavy, rarely used2638AnalyticsModule, // Heavy, rarely used2639AdminModule, // Only admins use this2640LegacyModule, // Migration module, rarely used2641BulkImportModule, // Used once a month2642],2643})2644export class AppModule {}26452646// All modules initialize at startup, even if never used2647// Slow cold starts in serverless2648// Memory wasted on unused modules2649```26502651**Correct (lazy load rarely-used modules):**26522653```typescript2654// Use LazyModuleLoader for optional modules2655import { LazyModuleLoader } from '@nestjs/core';26562657@Injectable()2658export class ReportsService {2659constructor(private lazyModuleLoader: LazyModuleLoader) {}26602661async generateReport(type: string): Promise<Report> {2662// Load module only when needed2663const { ReportsModule } = await import('./reports/reports.module');2664const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule);26652666const reportsService = moduleRef.get(ReportsGeneratorService);2667return reportsService.generate(type);2668}2669}26702671// Lazy load admin features with caching2672@Injectable()2673export class AdminService {2674private adminModule: ModuleRef | null = null;26752676constructor(private lazyModuleLoader: LazyModuleLoader) {}26772678private async getAdminModule(): Promise<ModuleRef> {2679if (!this.adminModule) {2680const { AdminModule } = await import('./admin/admin.module');2681this.adminModule = await this.lazyModuleLoader.load(() => AdminModule);2682}2683return this.adminModule;2684}26852686async runAdminTask(task: string): Promise<void> {2687const moduleRef = await this.getAdminModule();2688const taskRunner = moduleRef.get(AdminTaskRunner);2689await taskRunner.run(task);2690}2691}26922693// Reusable lazy loader service2694@Injectable()2695export class ModuleLoaderService {2696private loadedModules = new Map<string, ModuleRef>();26972698constructor(private lazyModuleLoader: LazyModuleLoader) {}26992700async load<T>(2701key: string,2702importFn: () => Promise<{ default: Type<T> } | Type<T>>,2703): Promise<ModuleRef> {2704if (!this.loadedModules.has(key)) {2705const module = await importFn();2706const moduleType = 'default' in module ? module.default : module;2707const moduleRef = await this.lazyModuleLoader.load(() => moduleType);2708this.loadedModules.set(key, moduleRef);2709}2710return this.loadedModules.get(key)!;2711}2712}27132714// Preload modules in background after startup2715@Injectable()2716export class ModulePreloader implements OnApplicationBootstrap {2717constructor(private lazyModuleLoader: LazyModuleLoader) {}27182719async onApplicationBootstrap(): Promise<void> {2720setTimeout(async () => {2721await this.preloadModule(() => import('./reports/reports.module'));2722}, 5000); // 5 seconds after startup2723}27242725private async preloadModule(importFn: () => Promise<any>): Promise<void> {2726try {2727const module = await importFn();2728const moduleType = module.default || Object.values(module)[0];2729await this.lazyModuleLoader.load(() => moduleType);2730} catch (error) {2731console.warn('Failed to preload module', error);2732}2733}2734}2735```27362737Reference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules)27382739---27402741### 5.3 Optimize Database Queries27422743**Impact: HIGH** — Database queries are typically the largest source of latency27442745Select only needed columns, use proper indexes, avoid over-fetching relations, and consider query performance when designing your data access. Most API slowness traces back to inefficient database queries.27462747**Incorrect (over-fetching data and missing indexes):**27482749```typescript2750// Select everything when you need few fields2751@Injectable()2752export class UsersService {2753async findAllEmails(): Promise<string[]> {2754const users = await this.repo.find();2755// Fetches ALL columns for ALL users2756return users.map((u) => u.email);2757}27582759async getUserSummary(id: string): Promise<UserSummary> {2760const user = await this.repo.findOne({2761where: { id },2762relations: ['posts', 'posts.comments', 'posts.comments.author', 'followers'],2763});2764// Over-fetches massive relation tree2765return { name: user.name, postCount: user.posts.length };2766}2767}27682769// No indexes on frequently queried columns2770@Entity()2771export class Order {2772@Column()2773userId: string; // No index - full table scan on every lookup27742775@Column()2776status: string; // No index - slow status filtering2777}2778```27792780**Correct (select only needed data with proper indexes):**27812782```typescript2783// Select only needed columns2784@Injectable()2785export class UsersService {2786async findAllEmails(): Promise<string[]> {2787const users = await this.repo.find({2788select: ['email'], // Only fetch email column2789});2790return users.map((u) => u.email);2791}27922793// Use QueryBuilder for complex selections2794async getUserSummary(id: string): Promise<UserSummary> {2795return this.repo2796.createQueryBuilder('user')2797.select('user.name', 'name')2798.addSelect('COUNT(post.id)', 'postCount')2799.leftJoin('user.posts', 'post')2800.where('user.id = :id', { id })2801.groupBy('user.id')2802.getRawOne();2803}28042805// Fetch relations only when needed2806async getFullProfile(id: string): Promise<User> {2807return this.repo.findOne({2808where: { id },2809relations: ['posts'], // Only immediate relation2810select: {2811id: true,2812name: true,2813email: true,2814posts: {2815id: true,2816title: true,2817},2818},2819});2820}2821}28222823// Add indexes on frequently queried columns2824@Entity()2825@Index(['userId'])2826@Index(['status'])2827@Index(['createdAt'])2828@Index(['userId', 'status']) // Composite index for common query pattern2829export class Order {2830@PrimaryGeneratedColumn('uuid')2831id: string;28322833@Column()2834userId: string;28352836@Column()2837status: string;28382839@CreateDateColumn()2840createdAt: Date;2841}28422843// Always paginate large datasets2844@Injectable()2845export class OrdersService {2846async findAll(page = 1, limit = 20): Promise<PaginatedResult<Order>> {2847const [items, total] = await this.repo.findAndCount({2848skip: (page - 1) * limit,2849take: limit,2850order: { createdAt: 'DESC' },2851});28522853return {2854items,2855meta: {2856page,2857limit,2858total,2859totalPages: Math.ceil(total / limit),2860},2861};2862}2863}2864```28652866Reference: [TypeORM Query Builder](https://typeorm.io/select-query-builder)28672868---28692870### 5.4 Use Caching Strategically28712872**Impact: HIGH** — Dramatically reduces database load and response times28732874Implement caching for expensive operations, frequently accessed data, and external API calls. Use NestJS CacheModule with appropriate TTLs and cache invalidation strategies. Don't cache everything - focus on high-impact areas.28752876**Incorrect (no caching or caching everything):**28772878```typescript2879// No caching for expensive, repeated queries2880@Injectable()2881export class ProductsService {2882async getPopular(): Promise<Product[]> {2883// Runs complex aggregation query EVERY request2884return this.productsRepo2885.createQueryBuilder('p')2886.leftJoin('p.orders', 'o')2887.select('p.*, COUNT(o.id) as orderCount')2888.groupBy('p.id')2889.orderBy('orderCount', 'DESC')2890.limit(20)2891.getMany();2892}2893}28942895// Cache everything without thought2896@Injectable()2897export class UsersService {2898@CacheKey('users')2899@CacheTTL(3600)2900@UseInterceptors(CacheInterceptor)2901async findAll(): Promise<User[]> {2902// Caching user list for 1 hour is wrong if data changes frequently2903return this.usersRepo.find();2904}2905}2906```29072908**Correct (strategic caching with proper invalidation):**29092910```typescript2911// Setup caching module2912@Module({2913imports: [2914CacheModule.registerAsync({2915imports: [ConfigModule],2916inject: [ConfigService],2917useFactory: (config: ConfigService) => ({2918stores: [2919new KeyvRedis(config.get('REDIS_URL')),2920],2921ttl: 60 * 1000, // Default 60s2922}),2923}),2924],2925})2926export class AppModule {}29272928// Manual caching for granular control2929@Injectable()2930export class ProductsService {2931constructor(2932@Inject(CACHE_MANAGER) private cache: Cache,2933private productsRepo: ProductRepository,2934) {}29352936async getPopular(): Promise<Product[]> {2937const cacheKey = 'products:popular';29382939// Try cache first2940const cached = await this.cache.get<Product[]>(cacheKey);2941if (cached) return cached;29422943// Cache miss - fetch and cache2944const products = await this.fetchPopularProducts();2945await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL2946return products;2947}29482949// Invalidate cache on changes2950async updateProduct(id: string, dto: UpdateProductDto): Promise<Product> {2951const product = await this.productsRepo.save({ id, ...dto });2952await this.cache.del('products:popular'); // Invalidate2953return product;2954}2955}29562957// Decorator-based caching with auto-interceptor2958@Controller('categories')2959@UseInterceptors(CacheInterceptor)2960export class CategoriesController {2961@Get()2962@CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change2963findAll(): Promise<Category[]> {2964return this.categoriesService.findAll();2965}29662967@Get(':id')2968@CacheTTL(60 * 1000) // 1 minute2969@CacheKey('category')2970findOne(@Param('id') id: string): Promise<Category> {2971return this.categoriesService.findOne(id);2972}2973}29742975// Event-based cache invalidation2976@Injectable()2977export class CacheInvalidationService {2978constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}29792980@OnEvent('product.created')2981@OnEvent('product.updated')2982@OnEvent('product.deleted')2983async invalidateProductCaches(event: ProductEvent) {2984await Promise.all([2985this.cache.del('products:popular'),2986this.cache.del(`product:${event.productId}`),2987]);2988}2989}2990```29912992Reference: [NestJS Caching](https://docs.nestjs.com/techniques/caching)29932994---29952996## 6. Testing29972998**Section Impact: MEDIUM-HIGH**29993000### 6.1 Use Supertest for E2E Testing30013002**Impact: HIGH** — Validates the full request/response cycle30033004End-to-end tests use Supertest to make real HTTP requests against your NestJS application. They test the full stack including middleware, guards, pipes, and interceptors. E2E tests catch integration issues that unit tests miss.30053006**Incorrect (no proper E2E setup or teardown):**30073008```typescript3009// Only unit test controllers3010describe('UsersController', () => {3011it('should return users', async () => {3012const service = { findAll: jest.fn().mockResolvedValue([]) };3013const controller = new UsersController(service as any);30143015const result = await controller.findAll();30163017expect(result).toEqual([]);3018// Doesn't test: routes, guards, pipes, serialization3019});3020});30213022// E2E tests without proper setup/teardown3023describe('Users API', () => {3024it('should create user', async () => {3025const app = await NestFactory.create(AppModule);3026// No proper initialization3027// No cleanup after test3028// Hits real database3029});3030});3031```30323033**Correct (proper E2E setup with Supertest):**30343035```typescript3036// Proper E2E test setup3037import { Test, TestingModule } from '@nestjs/testing';3038import { INestApplication, ValidationPipe } from '@nestjs/common';3039import * as request from 'supertest';3040import { AppModule } from '../src/app.module';30413042describe('UsersController (e2e)', () => {3043let app: INestApplication;30443045beforeAll(async () => {3046const moduleFixture: TestingModule = await Test.createTestingModule({3047imports: [AppModule],3048}).compile();30493050app = moduleFixture.createNestApplication();30513052// Apply same config as production3053app.useGlobalPipes(3054new ValidationPipe({3055whitelist: true,3056transform: true,3057forbidNonWhitelisted: true,3058}),3059);30603061await app.init();3062});30633064afterAll(async () => {3065await app.close();3066});30673068describe('/users (POST)', () => {3069it('should create a user', () => {3070return request(app.getHttpServer())3071.post('/users')3072.send({ name: 'John', email: '[email protected]' })3073.expect(201)3074.expect((res) => {3075expect(res.body).toHaveProperty('id');3076expect(res.body.name).toBe('John');3077expect(res.body.email).toBe('[email protected]');3078});3079});30803081it('should return 400 for invalid email', () => {3082return request(app.getHttpServer())3083.post('/users')3084.send({ name: 'John', email: 'invalid-email' })3085.expect(400)3086.expect((res) => {3087expect(res.body.message).toContain('email');3088});3089});3090});30913092describe('/users/:id (GET)', () => {3093it('should return 404 for non-existent user', () => {3094return request(app.getHttpServer())3095.get('/users/non-existent-id')3096.expect(404);3097});3098});3099});31003101// Testing with authentication3102describe('Protected Routes (e2e)', () => {3103let app: INestApplication;3104let authToken: string;31053106beforeAll(async () => {3107const moduleFixture = await Test.createTestingModule({3108imports: [AppModule],3109}).compile();31103111app = moduleFixture.createNestApplication();3112app.useGlobalPipes(new ValidationPipe({ whitelist: true }));3113await app.init();31143115// Get auth token3116const loginResponse = await request(app.getHttpServer())3117.post('/auth/login')3118.send({ email: '[email protected]', password: 'password' });31193120authToken = loginResponse.body.accessToken;3121});31223123it('should return 401 without token', () => {3124return request(app.getHttpServer())3125.get('/users/me')3126.expect(401);3127});31283129it('should return user profile with valid token', () => {3130return request(app.getHttpServer())3131.get('/users/me')3132.set('Authorization', `Bearer ${authToken}`)3133.expect(200)3134.expect((res) => {3135expect(res.body.email).toBe('[email protected]');3136});3137});3138});31393140// Database isolation for E2E tests3141describe('Orders API (e2e)', () => {3142let app: INestApplication;3143let dataSource: DataSource;31443145beforeAll(async () => {3146const moduleFixture = await Test.createTestingModule({3147imports: [3148ConfigModule.forRoot({3149envFilePath: '.env.test', // Test database config3150}),3151AppModule,3152],3153}).compile();31543155app = moduleFixture.createNestApplication();3156dataSource = moduleFixture.get(DataSource);3157await app.init();3158});31593160beforeEach(async () => {3161// Clean database between tests3162await dataSource.synchronize(true);3163});31643165afterAll(async () => {3166await dataSource.destroy();3167await app.close();3168});3169});3170```31713172Reference: [NestJS E2E Testing](https://docs.nestjs.com/fundamentals/testing#end-to-end-testing)31733174---31753176### 6.2 Mock External Services in Tests31773178**Impact: HIGH** — Ensures fast, reliable, deterministic tests31793180Never call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors.31813182**Incorrect (calling real APIs and databases):**31833184```typescript3185// Call real APIs in tests3186describe('PaymentService', () => {3187it('should process payment', async () => {3188const service = new PaymentService(new StripeClient(realApiKey));3189// Hits real Stripe API!3190const result = await service.charge('tok_visa', 1000);3191// Slow, costs money, flaky3192});3193});31943195// Use real database3196describe('UsersService', () => {3197beforeEach(async () => {3198await connection.query('DELETE FROM users'); // Modifies real DB3199});32003201it('should create user', async () => {3202const user = await service.create({ email: '[email protected]' });3203// Side effects on shared database3204});3205});32063207// Incomplete mocks3208const mockHttpService = {3209get: jest.fn().mockResolvedValue({ data: {} }),3210// Missing error scenarios, missing other methods3211};3212```32133214**Correct (mock all external dependencies):**32153216```typescript3217// Mock HTTP service properly3218describe('WeatherService', () => {3219let service: WeatherService;3220let httpService: jest.Mocked<HttpService>;32213222beforeEach(async () => {3223const module = await Test.createTestingModule({3224providers: [3225WeatherService,3226{3227provide: HttpService,3228useValue: {3229get: jest.fn(),3230post: jest.fn(),3231},3232},3233],3234}).compile();32353236service = module.get(WeatherService);3237httpService = module.get(HttpService);3238});32393240it('should return weather data', async () => {3241const mockResponse = {3242data: { temperature: 72, humidity: 45 },3243status: 200,3244statusText: 'OK',3245headers: {},3246config: {},3247};32483249httpService.get.mockReturnValue(of(mockResponse));32503251const result = await service.getWeather('NYC');32523253expect(result).toEqual({ temperature: 72, humidity: 45 });3254});32553256it('should handle API timeout', async () => {3257httpService.get.mockReturnValue(3258throwError(() => new Error('ETIMEDOUT')),3259);32603261await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');3262});32633264it('should handle rate limiting', async () => {3265httpService.get.mockReturnValue(3266throwError(() => ({3267response: { status: 429, data: { message: 'Rate limited' } },3268})),3269);32703271await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);3272});3273});32743275// Mock repository instead of database3276describe('UsersService', () => {3277let service: UsersService;3278let repo: jest.Mocked<Repository<User>>;32793280beforeEach(async () => {3281const mockRepo = {3282find: jest.fn(),3283findOne: jest.fn(),3284save: jest.fn(),3285delete: jest.fn(),3286createQueryBuilder: jest.fn(),3287};32883289const module = await Test.createTestingModule({3290providers: [3291UsersService,3292{ provide: getRepositoryToken(User), useValue: mockRepo },3293],3294}).compile();32953296service = module.get(UsersService);3297repo = module.get(getRepositoryToken(User));3298});32993300it('should find user by id', async () => {3301const mockUser = { id: '1', name: 'John', email: '[email protected]' };3302repo.findOne.mockResolvedValue(mockUser);33033304const result = await service.findById('1');33053306expect(result).toEqual(mockUser);3307expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });3308});3309});33103311// Create mock factory for complex SDKs3312function createMockStripe(): jest.Mocked<Stripe> {3313return {3314paymentIntents: {3315create: jest.fn(),3316retrieve: jest.fn(),3317confirm: jest.fn(),3318cancel: jest.fn(),3319},3320customers: {3321create: jest.fn(),3322retrieve: jest.fn(),3323},3324} as any;3325}33263327// Mock time for time-dependent tests3328describe('TokenService', () => {3329beforeEach(() => {3330jest.useFakeTimers();3331jest.setSystemTime(new Date('2024-01-15'));3332});33333334afterEach(() => {3335jest.useRealTimers();3336});33373338it('should expire token after 1 hour', async () => {3339const token = await service.createToken();33403341// Fast-forward time3342jest.advanceTimersByTime(61 * 60 * 1000);33433344expect(await service.isValid(token)).toBe(false);3345});3346});3347```33483349Reference: [Jest Mocking](https://jestjs.io/docs/mock-functions)33503351---33523353### 6.3 Use Testing Module for Unit Tests33543355**Impact: HIGH** — Enables proper isolated testing with mocked dependencies33563357Use `@nestjs/testing` module to create isolated test environments with mocked dependencies. This ensures your tests run fast, don't depend on external services, and properly test your business logic in isolation.33583359**Incorrect (manual instantiation bypassing DI):**33603361```typescript3362// Instantiate services manually without DI3363describe('UsersService', () => {3364it('should create user', async () => {3365// Manual instantiation bypasses DI3366const repo = new UserRepository(); // Real repo!3367const service = new UsersService(repo);33683369const user = await service.create({ name: 'Test' });3370// This hits the real database!3371});3372});33733374// Test implementation details3375describe('UsersController', () => {3376it('should call service', async () => {3377const service = { create: jest.fn() };3378const controller = new UsersController(service as any);33793380await controller.create({ name: 'Test' });33813382expect(service.create).toHaveBeenCalled(); // Tests implementation, not behavior3383});3384});3385```33863387**Correct (use Test.createTestingModule with mocked dependencies):**33883389```typescript3390// Use Test.createTestingModule for proper DI3391import { Test, TestingModule } from '@nestjs/testing';33923393describe('UsersService', () => {3394let service: UsersService;3395let repo: jest.Mocked<UserRepository>;33963397beforeEach(async () => {3398const module: TestingModule = await Test.createTestingModule({3399providers: [3400UsersService,3401{3402provide: UserRepository,3403useValue: {3404save: jest.fn(),3405findOne: jest.fn(),3406find: jest.fn(),3407},3408},3409],3410}).compile();34113412service = module.get<UsersService>(UsersService);3413repo = module.get(UserRepository);3414});34153416afterEach(() => {3417jest.clearAllMocks();3418});34193420describe('create', () => {3421it('should save and return user', async () => {3422const dto = { name: 'John', email: '[email protected]' };3423const expectedUser = { id: '1', ...dto };34243425repo.save.mockResolvedValue(expectedUser);34263427const result = await service.create(dto);34283429expect(result).toEqual(expectedUser);3430expect(repo.save).toHaveBeenCalledWith(dto);3431});34323433it('should throw on duplicate email', async () => {3434repo.findOne.mockResolvedValue({ id: '1', email: '[email protected]' });34353436await expect(3437service.create({ name: 'Test', email: '[email protected]' }),3438).rejects.toThrow(ConflictException);3439});3440});34413442describe('findById', () => {3443it('should return user when found', async () => {3444const user = { id: '1', name: 'John' };3445repo.findOne.mockResolvedValue(user);34463447const result = await service.findById('1');34483449expect(result).toEqual(user);3450});34513452it('should throw NotFoundException when not found', async () => {3453repo.findOne.mockResolvedValue(null);34543455await expect(service.findById('999')).rejects.toThrow(NotFoundException);3456});3457});3458});34593460// Testing guards and interceptors3461describe('RolesGuard', () => {3462let guard: RolesGuard;3463let reflector: Reflector;34643465beforeEach(async () => {3466const module = await Test.createTestingModule({3467providers: [RolesGuard, Reflector],3468}).compile();34693470guard = module.get<RolesGuard>(RolesGuard);3471reflector = module.get<Reflector>(Reflector);3472});34733474it('should allow when no roles required', () => {3475const context = createMockExecutionContext({ user: { roles: [] } });3476jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);34773478expect(guard.canActivate(context)).toBe(true);3479});34803481it('should allow admin for admin-only route', () => {3482const context = createMockExecutionContext({ user: { roles: ['admin'] } });3483jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);34843485expect(guard.canActivate(context)).toBe(true);3486});3487});34883489function createMockExecutionContext(request: Partial<Request>): ExecutionContext {3490return {3491switchToHttp: () => ({3492getRequest: () => request,3493}),3494getHandler: () => jest.fn(),3495getClass: () => jest.fn(),3496} as ExecutionContext;3497}3498```34993500Reference: [NestJS Testing](https://docs.nestjs.com/fundamentals/testing)35013502---35033504## 7. Database & ORM35053506**Section Impact: MEDIUM-HIGH**35073508### 7.1 Avoid N+1 Query Problems35093510**Impact: HIGH** — N+1 queries are one of the most common performance killers35113512N+1 queries occur when you fetch a list of entities, then make an additional query for each entity to load related data. Use eager loading with `relations`, query builder joins, or DataLoader to batch queries efficiently.35133514**Incorrect (lazy loading in loops causes N+1):**35153516```typescript3517// Lazy loading in loops causes N+13518@Injectable()3519export class OrdersService {3520async getOrdersWithItems(userId: string): Promise<Order[]> {3521const orders = await this.orderRepo.find({ where: { userId } });3522// 1 query for orders35233524for (const order of orders) {3525// N additional queries - one per order!3526order.items = await this.itemRepo.find({ where: { orderId: order.id } });3527}35283529return orders;3530}3531}35323533// Accessing lazy relations without loading3534@Controller('users')3535export class UsersController {3536@Get()3537async findAll(): Promise<User[]> {3538const users = await this.userRepo.find();3539// If User.posts is lazy-loaded, serializing triggers N queries3540return users; // Each user.posts access = 1 query3541}3542}3543```35443545**Correct (use relations for eager loading):**35463547```typescript3548// Use relations option for eager loading3549@Injectable()3550export class OrdersService {3551async getOrdersWithItems(userId: string): Promise<Order[]> {3552// Single query with JOIN3553return this.orderRepo.find({3554where: { userId },3555relations: ['items', 'items.product'],3556});3557}3558}35593560// Use QueryBuilder for complex joins3561@Injectable()3562export class UsersService {3563async getUsersWithPostCounts(): Promise<UserWithPostCount[]> {3564return this.userRepo3565.createQueryBuilder('user')3566.leftJoin('user.posts', 'post')3567.select('user.id', 'id')3568.addSelect('user.name', 'name')3569.addSelect('COUNT(post.id)', 'postCount')3570.groupBy('user.id')3571.getRawMany();3572}35733574async getActiveUsersWithPosts(): Promise<User[]> {3575return this.userRepo3576.createQueryBuilder('user')3577.leftJoinAndSelect('user.posts', 'post')3578.leftJoinAndSelect('post.comments', 'comment')3579.where('user.isActive = :active', { active: true })3580.andWhere('post.status = :status', { status: 'published' })3581.getMany();3582}3583}35843585// Use find options for specific fields3586async getOrderSummaries(userId: string): Promise<OrderSummary[]> {3587return this.orderRepo.find({3588where: { userId },3589relations: ['items'],3590select: {3591id: true,3592total: true,3593status: true,3594items: {3595id: true,3596quantity: true,3597price: true,3598},3599},3600});3601}36023603// Use DataLoader for GraphQL to batch and cache queries3604import DataLoader from 'dataloader';36053606@Injectable({ scope: Scope.REQUEST })3607export class PostsLoader {3608constructor(private postsService: PostsService) {}36093610readonly batchPosts = new DataLoader<string, Post[]>(async (userIds) => {3611// Single query for all users' posts3612const posts = await this.postsService.findByUserIds([...userIds]);36133614// Group by userId3615const postsMap = new Map<string, Post[]>();3616for (const post of posts) {3617const userPosts = postsMap.get(post.userId) || [];3618userPosts.push(post);3619postsMap.set(post.userId, userPosts);3620}36213622// Return in same order as input3623return userIds.map((id) => postsMap.get(id) || []);3624});3625}36263627// In resolver3628@ResolveField()3629async posts(@Parent() user: User): Promise<Post[]> {3630// DataLoader batches multiple calls into single query3631return this.postsLoader.batchPosts.load(user.id);3632}36333634// Enable query logging in development to detect N+13635TypeOrmModule.forRoot({3636logging: ['query', 'error'],3637logger: 'advanced-console',3638});3639```36403641Reference: [TypeORM Relations](https://typeorm.io/relations)36423643---36443645### 7.2 Use Database Migrations36463647**Impact: HIGH** — Enables safe, repeatable database schema changes36483649Never use `synchronize: true` in production. Use migrations for all schema changes. Migrations provide version control for your database, enable safe rollbacks, and ensure consistency across all environments.36503651**Incorrect (using synchronize or manual SQL):**36523653```typescript3654// Use synchronize in production3655TypeOrmModule.forRoot({3656type: 'postgres',3657synchronize: true, // DANGEROUS in production!3658// Can drop columns, tables, or data3659});36603661// Manual SQL in production3662@Injectable()3663export class DatabaseService {3664async addColumn(): Promise<void> {3665await this.dataSource.query('ALTER TABLE users ADD COLUMN age INT');3666// No version control, no rollback, inconsistent across envs3667}3668}36693670// Modify entities without migration3671@Entity()3672export class User {3673@Column()3674email: string;36753676@Column() // Added without migration3677newField: string; // Will crash in production if synchronize is false3678}3679```36803681**Correct (use migrations for all schema changes):**36823683```typescript3684// Configure TypeORM for migrations3685// data-source.ts3686export const dataSource = new DataSource({3687type: 'postgres',3688host: process.env.DB_HOST,3689port: parseInt(process.env.DB_PORT),3690username: process.env.DB_USERNAME,3691password: process.env.DB_PASSWORD,3692database: process.env.DB_NAME,3693entities: ['dist/**/*.entity.js'],3694migrations: ['dist/migrations/*.js'],3695synchronize: false, // Always false in production3696migrationsRun: true, // Run migrations on startup3697});36983699// app.module.ts3700TypeOrmModule.forRootAsync({3701inject: [ConfigService],3702useFactory: (config: ConfigService) => ({3703type: 'postgres',3704host: config.get('DB_HOST'),3705synchronize: config.get('NODE_ENV') === 'development', // Only in dev3706migrations: ['dist/migrations/*.js'],3707migrationsRun: true,3708}),3709});37103711// migrations/1705312800000-AddUserAge.ts3712import { MigrationInterface, QueryRunner } from 'typeorm';37133714export class AddUserAge1705312800000 implements MigrationInterface {3715name = 'AddUserAge1705312800000';37163717public async up(queryRunner: QueryRunner): Promise<void> {3718// Add column with default to handle existing rows3719await queryRunner.query(`3720ALTER TABLE "users" ADD "age" integer DEFAULT 03721`);37223723// Add index for frequently queried columns3724await queryRunner.query(`3725CREATE INDEX "IDX_users_age" ON "users" ("age")3726`);3727}37283729public async down(queryRunner: QueryRunner): Promise<void> {3730// Always implement down for rollback3731await queryRunner.query(`DROP INDEX "IDX_users_age"`);3732await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "age"`);3733}3734}37353736// Safe column rename (two-step)3737export class RenameNameToFullName1705312900000 implements MigrationInterface {3738public async up(queryRunner: QueryRunner): Promise<void> {3739// Step 1: Add new column3740await queryRunner.query(`3741ALTER TABLE "users" ADD "full_name" varchar(255)3742`);37433744// Step 2: Copy data3745await queryRunner.query(`3746UPDATE "users" SET "full_name" = "name"3747`);37483749// Step 3: Add NOT NULL constraint3750await queryRunner.query(`3751ALTER TABLE "users" ALTER COLUMN "full_name" SET NOT NULL3752`);37533754// Step 4: Drop old column (after verifying app works)3755await queryRunner.query(`3756ALTER TABLE "users" DROP COLUMN "name"3757`);3758}37593760public async down(queryRunner: QueryRunner): Promise<void> {3761await queryRunner.query(`ALTER TABLE "users" ADD "name" varchar(255)`);3762await queryRunner.query(`UPDATE "users" SET "name" = "full_name"`);3763await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "full_name"`);3764}3765}3766```37673768Reference: [TypeORM Migrations](https://typeorm.io/migrations)37693770---37713772### 7.3 Use Transactions for Multi-Step Operations37733774**Impact: HIGH** — Ensures data consistency in multi-step operations37753776When multiple database operations must succeed or fail together, wrap them in a transaction. This prevents partial updates that leave your data in an inconsistent state. Use TypeORM's transaction APIs or the DataSource query runner for complex scenarios.37773778**Incorrect (multiple saves without transaction):**37793780```typescript3781// Multiple saves without transaction3782@Injectable()3783export class OrdersService {3784async createOrder(userId: string, items: OrderItem[]): Promise<Order> {3785// If any step fails, data is inconsistent3786const order = await this.orderRepo.save({ userId, status: 'pending' });37873788for (const item of items) {3789await this.orderItemRepo.save({ orderId: order.id, ...item });3790await this.inventoryRepo.decrement({ productId: item.productId }, 'stock', item.quantity);3791}37923793await this.paymentService.charge(order.id);3794// If payment fails, order and inventory are already modified!37953796return order;3797}3798}3799```38003801**Correct (use DataSource.transaction for automatic rollback):**38023803```typescript3804// Use DataSource.transaction() for automatic rollback3805@Injectable()3806export class OrdersService {3807constructor(private dataSource: DataSource) {}38083809async createOrder(userId: string, items: OrderItem[]): Promise<Order> {3810return this.dataSource.transaction(async (manager) => {3811// All operations use the same transactional manager3812const order = await manager.save(Order, { userId, status: 'pending' });38133814for (const item of items) {3815await manager.save(OrderItem, { orderId: order.id, ...item });3816await manager.decrement(3817Inventory,3818{ productId: item.productId },3819'stock',3820item.quantity,3821);3822}38233824// If this throws, everything rolls back3825await this.paymentService.chargeWithManager(manager, order.id);38263827return order;3828});3829}3830}38313832// QueryRunner for manual transaction control3833@Injectable()3834export class TransferService {3835constructor(private dataSource: DataSource) {}38363837async transfer(fromId: string, toId: string, amount: number): Promise<void> {3838const queryRunner = this.dataSource.createQueryRunner();3839await queryRunner.connect();3840await queryRunner.startTransaction();38413842try {3843// Debit source account3844await queryRunner.manager.decrement(3845Account,3846{ id: fromId },3847'balance',3848amount,3849);38503851// Verify sufficient funds3852const source = await queryRunner.manager.findOne(Account, {3853where: { id: fromId },3854});3855if (source.balance < 0) {3856throw new BadRequestException('Insufficient funds');3857}38583859// Credit destination account3860await queryRunner.manager.increment(3861Account,3862{ id: toId },3863'balance',3864amount,3865);38663867// Log the transaction3868await queryRunner.manager.save(TransactionLog, {3869fromId,3870toId,3871amount,3872timestamp: new Date(),3873});38743875await queryRunner.commitTransaction();3876} catch (error) {3877await queryRunner.rollbackTransaction();3878throw error;3879} finally {3880await queryRunner.release();3881}3882}3883}38843885// Repository method with transaction support3886@Injectable()3887export class UsersRepository {3888constructor(3889@InjectRepository(User) private repo: Repository<User>,3890private dataSource: DataSource,3891) {}38923893async createWithProfile(3894userData: CreateUserDto,3895profileData: CreateProfileDto,3896): Promise<User> {3897return this.dataSource.transaction(async (manager) => {3898const user = await manager.save(User, userData);3899await manager.save(Profile, { ...profileData, userId: user.id });3900return user;3901});3902}3903}3904```39053906Reference: [TypeORM Transactions](https://typeorm.io/transactions)39073908---39093910## 8. API Design39113912**Section Impact: MEDIUM**39133914### 8.1 Use DTOs and Serialization for API Responses39153916**Impact: MEDIUM** — Response DTOs prevent accidental data exposure and ensure consistency39173918Never return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract.39193920**Incorrect (returning entities directly or manual spreading):**39213922```typescript3923// Return entities directly3924@Controller('users')3925export class UsersController {3926@Get(':id')3927async findOne(@Param('id') id: string): Promise<User> {3928return this.usersService.findById(id);3929// Returns: { id, email, passwordHash, ssn, internalNotes, ... }3930// Exposes sensitive data!3931}3932}39333934// Manual object spreading (error-prone)3935@Get(':id')3936async findOne(@Param('id') id: string) {3937const user = await this.usersService.findById(id);3938return {3939id: user.id,3940email: user.email,3941name: user.name,3942// Easy to forget to exclude sensitive fields3943// Hard to maintain across endpoints3944};3945}3946```39473948**Correct (use class-transformer with @Exclude and response DTOs):**39493950```typescript3951// Enable class-transformer globally3952async function bootstrap() {3953const app = await NestFactory.create(AppModule);3954app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));3955await app.listen(3000);3956}39573958// Entity with serialization control3959@Entity()3960export class User {3961@PrimaryGeneratedColumn('uuid')3962id: string;39633964@Column()3965email: string;39663967@Column()3968name: string;39693970@Column()3971@Exclude() // Never include in responses3972passwordHash: string;39733974@Column({ nullable: true })3975@Exclude()3976ssn: string;39773978@Column({ default: false })3979@Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests3980isAdmin: boolean;39813982@CreateDateColumn()3983createdAt: Date;39843985@Column()3986@Exclude()3987internalNotes: string;3988}39893990// Now returning entity is safe3991@Controller('users')3992export class UsersController {3993@Get(':id')3994async findOne(@Param('id') id: string): Promise<User> {3995return this.usersService.findById(id);3996// Returns: { id, email, name, createdAt }3997// Sensitive fields excluded automatically3998}3999}40004001// For different response shapes, use explicit DTOs4002export class UserResponseDto {4003@Expose()4004id: string;40054006@Expose()4007email: string;40084009@Expose()4010name: string;40114012@Expose()4013@Transform(({ obj }) => obj.posts?.length || 0)4014postCount: number;40154016constructor(partial: Partial<User>) {4017Object.assign(this, partial);4018}4019}40204021export class UserDetailResponseDto extends UserResponseDto {4022@Expose()4023createdAt: Date;40244025@Expose()4026@Type(() => PostResponseDto)4027posts: PostResponseDto[];4028}40294030// Controller with explicit DTOs4031@Controller('users')4032export class UsersController {4033@Get()4034@SerializeOptions({ type: UserResponseDto })4035async findAll(): Promise<UserResponseDto[]> {4036const users = await this.usersService.findAll();4037return users.map(u => plainToInstance(UserResponseDto, u));4038}40394040@Get(':id')4041async findOne(@Param('id') id: string): Promise<UserDetailResponseDto> {4042const user = await this.usersService.findByIdWithPosts(id);4043return plainToInstance(UserDetailResponseDto, user, {4044excludeExtraneousValues: true,4045});4046}4047}40484049// Groups for conditional serialization4050export class UserDto {4051@Expose()4052id: string;40534054@Expose()4055name: string;40564057@Expose({ groups: ['admin'] })4058email: string;40594060@Expose({ groups: ['admin'] })4061createdAt: Date;40624063@Expose({ groups: ['admin', 'owner'] })4064settings: UserSettings;4065}40664067@Controller('users')4068export class UsersController {4069@Get()4070@SerializeOptions({ groups: ['public'] })4071async findAllPublic(): Promise<UserDto[]> {4072// Returns: { id, name }4073}40744075@Get('admin')4076@UseGuards(AdminGuard)4077@SerializeOptions({ groups: ['admin'] })4078async findAllAdmin(): Promise<UserDto[]> {4079// Returns: { id, name, email, createdAt }4080}40814082@Get('me')4083@SerializeOptions({ groups: ['owner'] })4084async getProfile(@CurrentUser() user: User): Promise<UserDto> {4085// Returns: { id, name, settings }4086}4087}4088```40894090Reference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization)40914092---40934094### 8.2 Use Interceptors for Cross-Cutting Concerns40954096**Impact: MEDIUM-HIGH** — Interceptors provide clean separation for cross-cutting logic40974098Interceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams.40994100**Incorrect (logging and transformation in every method):**41014102```typescript4103// Logging in every controller method4104@Controller('users')4105export class UsersController {4106@Get()4107async findAll(): Promise<User[]> {4108const start = Date.now();4109this.logger.log('findAll called');41104111const users = await this.usersService.findAll();41124113this.logger.log(`findAll completed in ${Date.now() - start}ms`);4114return users;4115}41164117@Get(':id')4118async findOne(@Param('id') id: string): Promise<User> {4119const start = Date.now();4120this.logger.log(`findOne called with id: ${id}`);41214122const user = await this.usersService.findOne(id);41234124this.logger.log(`findOne completed in ${Date.now() - start}ms`);4125return user;4126}4127// Repeated in every method!4128}41294130// Manual response wrapping4131@Get()4132async findAll(): Promise<{ data: User[]; meta: Meta }> {4133const users = await this.usersService.findAll();4134return {4135data: users,4136meta: { timestamp: new Date(), count: users.length },4137};4138}4139```41404141**Correct (use interceptors for cross-cutting concerns):**41424143```typescript4144// Logging interceptor4145@Injectable()4146export class LoggingInterceptor implements NestInterceptor {4147private readonly logger = new Logger('HTTP');41484149intercept(context: ExecutionContext, next: CallHandler): Observable<any> {4150const request = context.switchToHttp().getRequest();4151const { method, url, body } = request;4152const now = Date.now();41534154return next.handle().pipe(4155tap({4156next: (data) => {4157const response = context.switchToHttp().getResponse();4158this.logger.log(4159`${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`,4160);4161},4162error: (error) => {4163this.logger.error(4164`${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`,4165error.stack,4166);4167},4168}),4169);4170}4171}41724173// Response transformation interceptor4174@Injectable()4175export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {4176intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {4177return next.handle().pipe(4178map((data) => ({4179data,4180meta: {4181timestamp: new Date().toISOString(),4182path: context.switchToHttp().getRequest().url,4183},4184})),4185);4186}4187}41884189// Timeout interceptor4190@Injectable()4191export class TimeoutInterceptor implements NestInterceptor {4192intercept(context: ExecutionContext, next: CallHandler): Observable<any> {4193return next.handle().pipe(4194timeout(5000),4195catchError((err) => {4196if (err instanceof TimeoutError) {4197throw new RequestTimeoutException('Request timed out');4198}4199throw err;4200}),4201);4202}4203}42044205// Apply globally or per-controller4206@Module({4207providers: [4208{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },4209{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },4210],4211})4212export class AppModule {}42134214// Or per-controller4215@Controller('users')4216@UseInterceptors(LoggingInterceptor)4217export class UsersController {4218@Get()4219async findAll(): Promise<User[]> {4220// Clean business logic only4221return this.usersService.findAll();4222}4223}42244225// Custom cache interceptor with TTL4226@Injectable()4227export class HttpCacheInterceptor implements NestInterceptor {4228constructor(4229private cacheManager: Cache,4230private reflector: Reflector,4231) {}42324233async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {4234const request = context.switchToHttp().getRequest();42354236// Only cache GET requests4237if (request.method !== 'GET') {4238return next.handle();4239}42404241const cacheKey = this.generateKey(request);4242const ttl = this.reflector.get<number>('cacheTTL', context.getHandler()) || 300;42434244const cached = await this.cacheManager.get(cacheKey);4245if (cached) {4246return of(cached);4247}42484249return next.handle().pipe(4250tap((response) => {4251this.cacheManager.set(cacheKey, response, ttl);4252}),4253);4254}42554256private generateKey(request: Request): string {4257return `cache:${request.url}:${JSON.stringify(request.query)}`;4258}4259}42604261// Usage with custom TTL4262@Get()4263@SetMetadata('cacheTTL', 600)4264@UseInterceptors(HttpCacheInterceptor)4265async findAll(): Promise<User[]> {4266return this.usersService.findAll();4267}42684269// Error mapping interceptor4270@Injectable()4271export class ErrorMappingInterceptor implements NestInterceptor {4272intercept(context: ExecutionContext, next: CallHandler): Observable<any> {4273return next.handle().pipe(4274catchError((error) => {4275if (error instanceof EntityNotFoundError) {4276throw new NotFoundException(error.message);4277}4278if (error instanceof QueryFailedError) {4279if (error.message.includes('duplicate')) {4280throw new ConflictException('Resource already exists');4281}4282}4283throw error;4284}),4285);4286}4287}4288```42894290Reference: [NestJS Interceptors](https://docs.nestjs.com/interceptors)42914292---42934294### 8.3 Use Pipes for Input Transformation42954296**Impact: MEDIUM** — Pipes ensure clean, validated data reaches your handlers42974298Use built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers.42994300**Incorrect (manual type parsing in handlers):**43014302```typescript4303// Manual type parsing in handlers4304@Controller('users')4305export class UsersController {4306@Get(':id')4307async findOne(@Param('id') id: string): Promise<User> {4308// Manual validation in every handler4309const uuid = id.trim();4310if (!isUUID(uuid)) {4311throw new BadRequestException('Invalid UUID');4312}4313return this.usersService.findOne(uuid);4314}43154316@Get()4317async findAll(4318@Query('page') page: string,4319@Query('limit') limit: string,4320): Promise<User[]> {4321// Manual parsing and defaults4322const pageNum = parseInt(page) || 1;4323const limitNum = parseInt(limit) || 10;4324return this.usersService.findAll(pageNum, limitNum);4325}4326}43274328// Type coercion without validation4329@Get()4330async search(@Query('price') price: string): Promise<Product[]> {4331const priceNum = +price; // NaN if invalid, no error4332return this.productsService.findByPrice(priceNum);4333}4334```43354336**Correct (use built-in and custom pipes):**43374338```typescript4339// Use built-in pipes for common transformations4340@Controller('users')4341export class UsersController {4342@Get(':id')4343async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {4344// id is guaranteed to be a valid UUID4345return this.usersService.findOne(id);4346}43474348@Get()4349async findAll(4350@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,4351@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,4352): Promise<User[]> {4353// Automatic defaults and type conversion4354return this.usersService.findAll(page, limit);4355}43564357@Get('by-status/:status')4358async findByStatus(4359@Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus,4360): Promise<User[]> {4361return this.usersService.findByStatus(status);4362}4363}43644365// Custom pipe for business logic4366@Injectable()4367export class ParseDatePipe implements PipeTransform<string, Date> {4368transform(value: string): Date {4369const date = new Date(value);4370if (isNaN(date.getTime())) {4371throw new BadRequestException('Invalid date format');4372}4373return date;4374}4375}43764377@Get('reports')4378async getReports(4379@Query('from', ParseDatePipe) from: Date,4380@Query('to', ParseDatePipe) to: Date,4381): Promise<Report[]> {4382return this.reportsService.findBetween(from, to);4383}43844385// Custom transformation pipes4386@Injectable()4387export class NormalizeEmailPipe implements PipeTransform<string, string> {4388transform(value: string): string {4389if (!value) return value;4390return value.trim().toLowerCase();4391}4392}43934394// Parse comma-separated values4395@Injectable()4396export class ParseArrayPipe implements PipeTransform<string, string[]> {4397transform(value: string): string[] {4398if (!value) return [];4399return value.split(',').map((v) => v.trim()).filter(Boolean);4400}4401}44024403@Get('products')4404async findProducts(4405@Query('ids', ParseArrayPipe) ids: string[],4406@Query('email', NormalizeEmailPipe) email: string,4407): Promise<Product[]> {4408// ids is already an array, email is normalized4409return this.productsService.findByIds(ids);4410}44114412// Sanitize HTML input4413@Injectable()4414export class SanitizeHtmlPipe implements PipeTransform<string, string> {4415transform(value: string): string {4416if (!value) return value;4417return sanitizeHtml(value, { allowedTags: [] });4418}4419}44204421// Global validation pipe with transformation4422app.useGlobalPipes(4423new ValidationPipe({4424whitelist: true, // Strip non-DTO properties4425transform: true, // Auto-transform to DTO types4426transformOptions: {4427enableImplicitConversion: true, // Convert query strings to numbers4428},4429forbidNonWhitelisted: true, // Throw on extra properties4430}),4431);44324433// DTO with transformation decorators4434export class FindProductsDto {4435@IsOptional()4436@Type(() => Number)4437@IsInt()4438@Min(1)4439page?: number = 1;44404441@IsOptional()4442@Type(() => Number)4443@IsInt()4444@Min(1)4445@Max(100)4446limit?: number = 10;44474448@IsOptional()4449@Transform(({ value }) => value?.toLowerCase())4450@IsString()4451search?: string;44524453@IsOptional()4454@Transform(({ value }) => value?.split(','))4455@IsArray()4456@IsString({ each: true })4457categories?: string[];4458}44594460@Get()4461async findAll(@Query() dto: FindProductsDto): Promise<Product[]> {4462// dto is already transformed and validated4463return this.productsService.findAll(dto);4464}44654466// Pipe error customization4467@Injectable()4468export class CustomParseIntPipe extends ParseIntPipe {4469constructor() {4470super({4471exceptionFactory: (error) =>4472new BadRequestException(`${error} must be a valid integer`),4473});4474}4475}44764477// Or use options on built-in pipes4478@Get(':id')4479async findOne(4480@Param(4481'id',4482new ParseIntPipe({4483errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,4484exceptionFactory: () => new NotAcceptableException('ID must be numeric'),4485}),4486)4487id: number,4488): Promise<Item> {4489return this.itemsService.findOne(id);4490}4491```44924493Reference: [NestJS Pipes](https://docs.nestjs.com/pipes)44944495---44964497### 8.4 Use API Versioning for Breaking Changes44984499**Impact: MEDIUM** — Versioning allows you to evolve APIs without breaking existing clients45004501Use NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints.45024503**Incorrect (breaking changes without versioning):**45044505```typescript4506// Breaking changes without versioning4507@Controller('users')4508export class UsersController {4509@Get(':id')4510async findOne(@Param('id') id: string): Promise<User> {4511// Original response: { id, name, email }4512// Later changed to: { id, firstName, lastName, emailAddress }4513// Old clients break!4514return this.usersService.findOne(id);4515}4516}45174518// Manual versioning in routes4519@Controller('v1/users')4520export class UsersV1Controller {}45214522@Controller('v2/users')4523export class UsersV2Controller {}4524// Inconsistent, error-prone, hard to maintain4525```45264527**Correct (use NestJS built-in versioning):**45284529```typescript4530// Enable versioning in main.ts4531async function bootstrap() {4532const app = await NestFactory.create(AppModule);45334534// URI versioning: /v1/users, /v2/users4535app.enableVersioning({4536type: VersioningType.URI,4537defaultVersion: '1',4538});45394540// Or header versioning: X-API-Version: 14541app.enableVersioning({4542type: VersioningType.HEADER,4543header: 'X-API-Version',4544defaultVersion: '1',4545});45464547// Or media type: Accept: application/json;v=14548app.enableVersioning({4549type: VersioningType.MEDIA_TYPE,4550key: 'v=',4551defaultVersion: '1',4552});45534554await app.listen(3000);4555}45564557// Version-specific controllers4558@Controller('users')4559@Version('1')4560export class UsersV1Controller {4561@Get(':id')4562async findOne(@Param('id') id: string): Promise<UserV1Response> {4563const user = await this.usersService.findOne(id);4564// V1 response format4565return {4566id: user.id,4567name: user.name,4568email: user.email,4569};4570}4571}45724573@Controller('users')4574@Version('2')4575export class UsersV2Controller {4576@Get(':id')4577async findOne(@Param('id') id: string): Promise<UserV2Response> {4578const user = await this.usersService.findOne(id);4579// V2 response format with breaking changes4580return {4581id: user.id,4582firstName: user.firstName,4583lastName: user.lastName,4584emailAddress: user.email,4585createdAt: user.createdAt,4586};4587}4588}45894590// Per-route versioning - different versions for different routes4591@Controller('users')4592export class UsersController {4593@Get()4594@Version('1')4595findAllV1(): Promise<UserV1Response[]> {4596return this.usersService.findAllV1();4597}45984599@Get()4600@Version('2')4601findAllV2(): Promise<UserV2Response[]> {4602return this.usersService.findAllV2();4603}46044605@Get(':id')4606@Version(['1', '2']) // Same handler for multiple versions4607findOne(@Param('id') id: string): Promise<User> {4608return this.usersService.findOne(id);4609}46104611@Post()4612@Version(VERSION_NEUTRAL) // Available in all versions4613create(@Body() dto: CreateUserDto): Promise<User> {4614return this.usersService.create(dto);4615}4616}46174618// Shared service with version-specific logic4619@Injectable()4620export class UsersService {4621async findOne(id: string, version: string): Promise<any> {4622const user = await this.repo.findOne({ where: { id } });46234624if (version === '1') {4625return this.toV1Response(user);4626}4627return this.toV2Response(user);4628}46294630private toV1Response(user: User): UserV1Response {4631return {4632id: user.id,4633name: `${user.firstName} ${user.lastName}`,4634email: user.email,4635};4636}46374638private toV2Response(user: User): UserV2Response {4639return {4640id: user.id,4641firstName: user.firstName,4642lastName: user.lastName,4643emailAddress: user.email,4644createdAt: user.createdAt,4645};4646}4647}46484649// Controller extracts version4650@Controller('users')4651export class UsersController {4652@Get(':id')4653async findOne(4654@Param('id') id: string,4655@Headers('X-API-Version') version: string = '1',4656): Promise<any> {4657return this.usersService.findOne(id, version);4658}4659}46604661// Deprecation strategy - mark old versions as deprecated4662@Controller('users')4663@Version('1')4664@UseInterceptors(DeprecationInterceptor)4665export class UsersV1Controller {4666// All V1 routes will include deprecation warning4667}46684669@Injectable()4670export class DeprecationInterceptor implements NestInterceptor {4671intercept(context: ExecutionContext, next: CallHandler): Observable<any> {4672const response = context.switchToHttp().getResponse();4673response.setHeader('Deprecation', 'true');4674response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT');4675response.setHeader('Link', '</v2/users>; rel="successor-version"');46764677return next.handle();4678}4679}4680```46814682Reference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning)46834684---46854686## 9. Microservices46874688**Section Impact: MEDIUM**46894690### 9.1 Implement Health Checks for Microservices46914692**Impact: MEDIUM-HIGH** — Health checks enable orchestrators to manage service lifecycle46934694Implement liveness and readiness probes using `@nestjs/terminus`. Liveness checks determine if the service should be restarted. Readiness checks determine if the service can accept traffic. Proper health checks enable Kubernetes and load balancers to route traffic correctly.46954696**Incorrect (simple ping that doesn't check dependencies):**46974698```typescript4699// Simple ping that doesn't check dependencies4700@Controller('health')4701export class HealthController {4702@Get()4703check(): string {4704return 'OK'; // Service might be unhealthy but returns OK4705}4706}47074708// Health check that blocks on slow dependencies4709@Controller('health')4710export class HealthController {4711@Get()4712async check(): Promise<string> {4713// If database is slow, health check times out4714await this.userRepo.findOne({ where: { id: '1' } });4715await this.redis.ping();4716await this.externalApi.healthCheck();4717return 'OK';4718}4719}4720```47214722**Correct (use @nestjs/terminus for comprehensive health checks):**47234724```typescript4725// Use @nestjs/terminus for comprehensive health checks4726import {4727HealthCheckService,4728HttpHealthIndicator,4729TypeOrmHealthIndicator,4730HealthCheck,4731DiskHealthIndicator,4732MemoryHealthIndicator,4733} from '@nestjs/terminus';47344735@Controller('health')4736export class HealthController {4737constructor(4738private health: HealthCheckService,4739private http: HttpHealthIndicator,4740private db: TypeOrmHealthIndicator,4741private disk: DiskHealthIndicator,4742private memory: MemoryHealthIndicator,4743) {}47444745// Liveness probe - is the service alive?4746@Get('live')4747@HealthCheck()4748liveness() {4749return this.health.check([4750// Basic checks only4751() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB4752]);4753}47544755// Readiness probe - can the service handle traffic?4756@Get('ready')4757@HealthCheck()4758readiness() {4759return this.health.check([4760() => this.db.pingCheck('database'),4761() =>4762this.http.pingCheck('redis', 'http://redis:6379', { timeout: 1000 }),4763() =>4764this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),4765]);4766}47674768// Deep health check for debugging4769@Get('deep')4770@HealthCheck()4771deepCheck() {4772return this.health.check([4773() => this.db.pingCheck('database'),4774() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),4775() => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),4776() =>4777this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),4778() =>4779this.http.pingCheck('external-api', 'https://api.example.com/health'),4780]);4781}4782}47834784// Custom indicator for business-specific health4785@Injectable()4786export class QueueHealthIndicator extends HealthIndicator {4787constructor(private queueService: QueueService) {4788super();4789}47904791async isHealthy(key: string): Promise<HealthIndicatorResult> {4792const queueStats = await this.queueService.getStats();47934794const isHealthy = queueStats.failedCount < 100;4795const result = this.getStatus(key, isHealthy, {4796waiting: queueStats.waitingCount,4797active: queueStats.activeCount,4798failed: queueStats.failedCount,4799});48004801if (!isHealthy) {4802throw new HealthCheckError('Queue unhealthy', result);4803}48044805return result;4806}4807}48084809// Redis health indicator4810@Injectable()4811export class RedisHealthIndicator extends HealthIndicator {4812constructor(@InjectRedis() private redis: Redis) {4813super();4814}48154816async isHealthy(key: string): Promise<HealthIndicatorResult> {4817try {4818const pong = await this.redis.ping();4819return this.getStatus(key, pong === 'PONG');4820} catch (error) {4821throw new HealthCheckError('Redis check failed', this.getStatus(key, false));4822}4823}4824}48254826// Use custom indicators4827@Get('ready')4828@HealthCheck()4829readiness() {4830return this.health.check([4831() => this.db.pingCheck('database'),4832() => this.redis.isHealthy('redis'),4833() => this.queue.isHealthy('job-queue'),4834]);4835}48364837// Graceful shutdown handling4838@Injectable()4839export class GracefulShutdownService implements OnApplicationShutdown {4840private isShuttingDown = false;48414842isShutdown(): boolean {4843return this.isShuttingDown;4844}48454846async onApplicationShutdown(signal: string): Promise<void> {4847this.isShuttingDown = true;4848console.log(`Shutting down on ${signal}`);48494850// Wait for in-flight requests4851await new Promise((resolve) => setTimeout(resolve, 5000));4852}4853}48544855// Health check respects shutdown state4856@Get('ready')4857@HealthCheck()4858readiness() {4859if (this.shutdownService.isShutdown()) {4860throw new ServiceUnavailableException('Shutting down');4861}48624863return this.health.check([4864() => this.db.pingCheck('database'),4865]);4866}4867```48684869### Kubernetes Configuration48704871```yaml4872# Kubernetes deployment with probes4873apiVersion: apps/v14874kind: Deployment4875metadata:4876name: api-service4877spec:4878template:4879spec:4880containers:4881- name: api4882image: api-service:latest4883ports:4884- containerPort: 30004885livenessProbe:4886httpGet:4887path: /health/live4888port: 30004889initialDelaySeconds: 304890periodSeconds: 104891timeoutSeconds: 54892failureThreshold: 34893readinessProbe:4894httpGet:4895path: /health/ready4896port: 30004897initialDelaySeconds: 54898periodSeconds: 54899timeoutSeconds: 34900failureThreshold: 34901startupProbe:4902httpGet:4903path: /health/live4904port: 30004905initialDelaySeconds: 04906periodSeconds: 54907failureThreshold: 304908```49094910Reference: [NestJS Terminus](https://docs.nestjs.com/recipes/terminus)49114912---49134914### 9.2 Use Message and Event Patterns Correctly49154916**Impact: MEDIUM** — Proper patterns ensure reliable microservice communication49174918NestJS microservices support two communication patterns: request-response (MessagePattern) and event-based (EventPattern). Use MessagePattern when you need a response, and EventPattern for fire-and-forget notifications. Understanding the difference prevents communication bugs.49194920**Incorrect (using wrong pattern for use case):**49214922```typescript4923// Use @MessagePattern for fire-and-forget4924@Controller()4925export class NotificationsController {4926@MessagePattern('user.created')4927async handleUserCreated(data: UserCreatedEvent) {4928// This WAITS for response, blocking the sender4929await this.emailService.sendWelcome(data.email);4930// If email fails, sender gets an error (coupling!)4931}4932}49334934// Use @EventPattern expecting a response4935@Controller()4936export class OrdersController {4937@EventPattern('inventory.check')4938async checkInventory(data: CheckInventoryDto) {4939const available = await this.inventory.check(data);4940return available; // This return value is IGNORED with @EventPattern!4941}4942}49434944// Tight coupling in client4945@Injectable()4946export class UsersService {4947async createUser(dto: CreateUserDto): Promise<User> {4948const user = await this.repo.save(dto);49494950// Blocks until notification service responds4951await this.client.send('user.created', user).toPromise();4952// If notification service is down, user creation fails!49534954return user;4955}4956}4957```49584959**Correct (use MessagePattern for request-response, EventPattern for fire-and-forget):**49604961```typescript4962// MessagePattern: Request-Response (when you NEED a response)4963@Controller()4964export class InventoryController {4965@MessagePattern({ cmd: 'check_inventory' })4966async checkInventory(data: CheckInventoryDto): Promise<InventoryResult> {4967const result = await this.inventoryService.check(data.productId, data.quantity);4968return result; // Response sent back to caller4969}4970}49714972// Client expects response4973@Injectable()4974export class OrdersService {4975async createOrder(dto: CreateOrderDto): Promise<Order> {4976// Check inventory - we NEED this response to proceed4977const inventory = await firstValueFrom(4978this.inventoryClient.send<InventoryResult>(4979{ cmd: 'check_inventory' },4980{ productId: dto.productId, quantity: dto.quantity },4981),4982);49834984if (!inventory.available) {4985throw new BadRequestException('Insufficient inventory');4986}49874988return this.repo.save(dto);4989}4990}49914992// EventPattern: Fire-and-Forget (for notifications, side effects)4993@Controller()4994export class NotificationsController {4995@EventPattern('user.created')4996async handleUserCreated(data: UserCreatedEvent): Promise<void> {4997// No return value needed - just process the event4998await this.emailService.sendWelcome(data.email);4999await this.analyticsService.track('user_signup', data);5000// If this fails, it doesn't affect the sender5001}5002}50035004// Client emits event without waiting5005@Injectable()5006export class UsersService {5007async createUser(dto: CreateUserDto): Promise<User> {5008const user = await this.repo.save(dto);50095010// Fire and forget - doesn't block, doesn't wait5011this.eventClient.emit('user.created', {5012userId: user.id,5013email: user.email,5014timestamp: new Date(),5015});50165017return user; // User creation succeeds regardless of event handling5018}5019}50205021// Hybrid pattern for critical events5022@Injectable()5023export class OrdersService {5024async createOrder(dto: CreateOrderDto): Promise<Order> {5025const order = await this.repo.save(dto);50265027// Critical: inventory reservation (use MessagePattern)5028const reserved = await firstValueFrom(5029this.inventoryClient.send({ cmd: 'reserve_inventory' }, {5030orderId: order.id,5031items: dto.items,5032}),5033);50345035if (!reserved.success) {5036await this.repo.delete(order.id);5037throw new BadRequestException('Could not reserve inventory');5038}50395040// Non-critical: notifications (use EventPattern)5041this.eventClient.emit('order.created', {5042orderId: order.id,5043userId: dto.userId,5044total: dto.total,5045});50465047return order;5048}5049}50505051// Error handling patterns5052// MessagePattern errors propagate to caller5053@MessagePattern({ cmd: 'get_user' })5054async getUser(userId: string): Promise<User> {5055const user = await this.repo.findOne({ where: { id: userId } });5056if (!user) {5057throw new RpcException('User not found'); // Received by caller5058}5059return user;5060}50615062// EventPattern errors should be handled locally5063@EventPattern('order.created')5064async handleOrderCreated(data: OrderCreatedEvent): Promise<void> {5065try {5066await this.processOrder(data);5067} catch (error) {5068// Log and potentially retry - don't throw5069this.logger.error('Failed to process order event', error);5070await this.deadLetterQueue.add(data);5071}5072}5073```50745075Reference: [NestJS Microservices](https://docs.nestjs.com/microservices/basics)50765077---50785079### 9.3 Use Message Queues for Background Jobs50805081**Impact: MEDIUM-HIGH** — Queues enable reliable background processing50825083Use `@nestjs/bullmq` for background job processing. Queues decouple long-running tasks from HTTP requests, enable retry logic, and distribute workload across workers. Use them for emails, file processing, notifications, and any task that shouldn't block user requests.50845085**Incorrect (long-running tasks in HTTP handlers):**50865087```typescript5088// Long-running tasks in HTTP handlers5089@Controller('reports')5090export class ReportsController {5091@Post()5092async generate(@Body() dto: GenerateReportDto): Promise<Report> {5093// This blocks the request for potentially minutes5094const data = await this.fetchLargeDataset(dto);5095const report = await this.processData(data); // Slow!5096await this.sendEmail(dto.email, report); // Can fail!5097return report; // Client times out5098}5099}51005101// Fire-and-forget without retry5102@Injectable()5103export class EmailService {5104async sendWelcome(email: string): Promise<void> {5105// If this fails, email is never sent5106await this.mailer.send({ to: email, template: 'welcome' });5107// No retry, no tracking, no visibility5108}5109}51105111// Use setInterval for scheduled tasks5112setInterval(async () => {5113await cleanupOldRecords();5114}, 60000); // No error handling, memory leaks5115```51165117**Correct (use BullMQ for background processing):**51185119```typescript5120// Configure BullMQ5121import { BullModule } from '@nestjs/bullmq';51225123@Module({5124imports: [5125BullModule.forRoot({5126connection: {5127host: 'localhost',5128port: 6379,5129},5130defaultJobOptions: {5131removeOnComplete: 1000,5132removeOnFail: 5000,5133attempts: 3,5134backoff: {5135type: 'exponential',5136delay: 1000,5137},5138},5139}),5140BullModule.registerQueue(5141{ name: 'email' },5142{ name: 'reports' },5143{ name: 'notifications' },5144),5145],5146})5147export class QueueModule {}51485149// Producer: Add jobs to queue5150@Injectable()5151export class ReportsService {5152constructor(5153@InjectQueue('reports') private reportsQueue: Queue,5154) {}51555156async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {5157// Return immediately, process in background5158const job = await this.reportsQueue.add('generate', dto, {5159priority: dto.urgent ? 1 : 10,5160delay: dto.scheduledFor ? Date.parse(dto.scheduledFor) - Date.now() : 0,5161});51625163return { jobId: job.id };5164}51655166async getJobStatus(jobId: string): Promise<JobStatus> {5167const job = await this.reportsQueue.getJob(jobId);5168return {5169status: await job.getState(),5170progress: job.progress,5171result: job.returnvalue,5172};5173}5174}51755176// Consumer: Process jobs5177@Processor('reports')5178export class ReportsProcessor {5179private readonly logger = new Logger(ReportsProcessor.name);51805181@Process('generate')5182async generateReport(job: Job<GenerateReportDto>): Promise<Report> {5183this.logger.log(`Processing report job ${job.id}`);51845185// Update progress5186await job.updateProgress(10);51875188const data = await this.fetchData(job.data);5189await job.updateProgress(50);51905191const report = await this.processData(data);5192await job.updateProgress(90);51935194await this.saveReport(report);5195await job.updateProgress(100);51965197return report;5198}51995200@OnQueueActive()5201onActive(job: Job) {5202this.logger.log(`Processing job ${job.id}`);5203}52045205@OnQueueCompleted()5206onCompleted(job: Job, result: any) {5207this.logger.log(`Job ${job.id} completed`);5208}52095210@OnQueueFailed()5211onFailed(job: Job, error: Error) {5212this.logger.error(`Job ${job.id} failed: ${error.message}`);5213}5214}52155216// Email queue with retry5217@Processor('email')5218export class EmailProcessor {5219@Process('send')5220async sendEmail(job: Job<SendEmailDto>): Promise<void> {5221const { to, template, data } = job.data;52225223try {5224await this.mailer.send({5225to,5226template,5227context: data,5228});5229} catch (error) {5230// BullMQ will retry based on job options5231throw error;5232}5233}5234}52355236// Usage5237@Injectable()5238export class NotificationService {5239constructor(@InjectQueue('email') private emailQueue: Queue) {}52405241async sendWelcome(user: User): Promise<void> {5242await this.emailQueue.add(5243'send',5244{5245to: user.email,5246template: 'welcome',5247data: { name: user.name },5248},5249{5250attempts: 5,5251backoff: { type: 'exponential', delay: 5000 },5252},5253);5254}5255}52565257// Scheduled jobs5258@Injectable()5259export class ScheduledJobsService implements OnModuleInit {5260constructor(@InjectQueue('maintenance') private queue: Queue) {}52615262async onModuleInit(): Promise<void> {5263// Clean up old reports daily at midnight5264await this.queue.add(5265'cleanup',5266{},5267{5268repeat: { cron: '0 0 * * *' },5269jobId: 'daily-cleanup', // Prevent duplicates5270},5271);52725273// Send digest every hour5274await this.queue.add(5275'digest',5276{},5277{5278repeat: { every: 60 * 60 * 1000 },5279jobId: 'hourly-digest',5280},5281);5282}5283}52845285@Processor('maintenance')5286export class MaintenanceProcessor {5287@Process('cleanup')5288async cleanup(): Promise<void> {5289await this.cleanupOldReports();5290await this.cleanupExpiredSessions();5291}52925293@Process('digest')5294async sendDigest(): Promise<void> {5295const users = await this.getUsersForDigest();5296for (const user of users) {5297await this.emailQueue.add('send', { to: user.email, template: 'digest' });5298}5299}5300}53015302// Queue monitoring with Bull Board5303import { BullBoardModule } from '@bull-board/nestjs';5304import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';53055306@Module({5307imports: [5308BullBoardModule.forRoot({5309route: '/admin/queues',5310adapter: ExpressAdapter,5311}),5312BullBoardModule.forFeature({5313name: 'email',5314adapter: BullMQAdapter,5315}),5316BullBoardModule.forFeature({5317name: 'reports',5318adapter: BullMQAdapter,5319}),5320],5321})5322export class AdminModule {}5323```53245325Reference: [NestJS Queues](https://docs.nestjs.com/techniques/queues)53265327---53285329## 10. DevOps & Deployment53305331**Section Impact: LOW-MEDIUM**53325333### 10.1 Implement Graceful Shutdown53345335**Impact: MEDIUM-HIGH** — Proper shutdown handling ensures zero-downtime deployments53365337Handle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments.53385339**Incorrect (ignoring shutdown signals):**53405341```typescript5342// Ignore shutdown signals5343async function bootstrap() {5344const app = await NestFactory.create(AppModule);5345await app.listen(3000);5346// App crashes immediately on SIGTERM5347// In-flight requests fail5348// Database connections are abruptly closed5349}53505351// Long-running tasks without cancellation5352@Injectable()5353export class ProcessingService {5354async processLargeFile(file: File): Promise<void> {5355// No way to interrupt this during shutdown5356for (let i = 0; i < file.chunks.length; i++) {5357await this.processChunk(file.chunks[i]);5358// May run for minutes, blocking shutdown5359}5360}5361}5362```53635364**Correct (enable shutdown hooks and handle cleanup):**53655366```typescript5367// Enable shutdown hooks in main.ts5368async function bootstrap() {5369const app = await NestFactory.create(AppModule);53705371// Enable shutdown hooks5372app.enableShutdownHooks();53735374// Optional: Add timeout for forced shutdown5375const server = await app.listen(3000);5376server.setTimeout(30000); // 30 second timeout53775378// Handle graceful shutdown5379const signals = ['SIGTERM', 'SIGINT'];5380signals.forEach((signal) => {5381process.on(signal, async () => {5382console.log(`Received ${signal}, starting graceful shutdown...`);53835384// Stop accepting new connections5385server.close(async () => {5386console.log('HTTP server closed');5387await app.close();5388process.exit(0);5389});53905391// Force exit after timeout5392setTimeout(() => {5393console.error('Forced shutdown after timeout');5394process.exit(1);5395}, 30000);5396});5397});5398}53995400// Lifecycle hooks for cleanup5401@Injectable()5402export class DatabaseService implements OnApplicationShutdown {5403private readonly connections: Connection[] = [];54045405async onApplicationShutdown(signal?: string): Promise<void> {5406console.log(`Database service shutting down on ${signal}`);54075408// Close all connections gracefully5409await Promise.all(5410this.connections.map((conn) => conn.close()),5411);54125413console.log('All database connections closed');5414}5415}54165417// Queue processor with graceful shutdown5418@Injectable()5419export class QueueService implements OnApplicationShutdown, OnModuleDestroy {5420private isShuttingDown = false;54215422onModuleDestroy(): void {5423this.isShuttingDown = true;5424}54255426async onApplicationShutdown(): Promise<void> {5427// Wait for current jobs to complete5428await this.queue.close();5429}54305431async processJob(job: Job): Promise<void> {5432if (this.isShuttingDown) {5433throw new Error('Service is shutting down');5434}5435await this.doWork(job);5436}5437}54385439// WebSocket gateway cleanup5440@WebSocketGateway()5441export class EventsGateway implements OnApplicationShutdown {5442@WebSocketServer()5443server: Server;54445445async onApplicationShutdown(): Promise<void> {5446// Notify all connected clients5447this.server.emit('shutdown', { message: 'Server is shutting down' });54485449// Close all connections5450this.server.disconnectSockets();5451}5452}54535454// Health check integration5455@Injectable()5456export class ShutdownService {5457private isShuttingDown = false;54585459startShutdown(): void {5460this.isShuttingDown = true;5461}54625463isShutdown(): boolean {5464return this.isShuttingDown;5465}5466}54675468@Controller('health')5469export class HealthController {5470constructor(private shutdownService: ShutdownService) {}54715472@Get('ready')5473@HealthCheck()5474readiness(): Promise<HealthCheckResult> {5475// Return 503 during shutdown - k8s stops sending traffic5476if (this.shutdownService.isShutdown()) {5477throw new ServiceUnavailableException('Shutting down');5478}54795480return this.health.check([5481() => this.db.pingCheck('database'),5482]);5483}5484}54855486// Integrate with shutdown5487@Injectable()5488export class AppShutdownService implements OnApplicationShutdown {5489constructor(private shutdownService: ShutdownService) {}54905491async onApplicationShutdown(): Promise<void> {5492// Mark as unhealthy first5493this.shutdownService.startShutdown();54945495// Wait for k8s to update endpoints5496await this.sleep(5000);54975498// Then proceed with cleanup5499}5500}55015502// Request tracking for in-flight requests5503@Injectable()5504export class RequestTracker implements NestMiddleware, OnApplicationShutdown {5505private activeRequests = 0;5506private isShuttingDown = false;5507private shutdownPromise: Promise<void> | null = null;5508private resolveShutdown: (() => void) | null = null;55095510use(req: Request, res: Response, next: NextFunction): void {5511if (this.isShuttingDown) {5512res.status(503).send('Service Unavailable');5513return;5514}55155516this.activeRequests++;55175518res.on('finish', () => {5519this.activeRequests--;5520if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) {5521this.resolveShutdown();5522}5523});55245525next();5526}55275528async onApplicationShutdown(): Promise<void> {5529this.isShuttingDown = true;55305531if (this.activeRequests > 0) {5532console.log(`Waiting for ${this.activeRequests} requests to complete`);5533this.shutdownPromise = new Promise((resolve) => {5534this.resolveShutdown = resolve;5535});55365537// Wait with timeout5538await Promise.race([5539this.shutdownPromise,5540new Promise((resolve) => setTimeout(resolve, 30000)),5541]);5542}55435544console.log('All requests completed');5545}5546}5547```55485549Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)55505551---55525553### 10.2 Use ConfigModule for Environment Configuration55545555**Impact: LOW-MEDIUM** — Proper configuration prevents deployment failures55565557Use `@nestjs/config` for environment-based configuration. Validate configuration at startup to fail fast on misconfigurations. Use namespaced configuration for organization and type safety.55585559**Incorrect (accessing process.env directly):**55605561```typescript5562// Access process.env directly5563@Injectable()5564export class DatabaseService {5565constructor() {5566// No validation, can fail at runtime5567this.connection = new Pool({5568host: process.env.DB_HOST,5569port: parseInt(process.env.DB_PORT), // NaN if missing5570password: process.env.DB_PASSWORD, // undefined if missing5571});5572}5573}55745575// Scattered env access5576@Injectable()5577export class EmailService {5578sendEmail() {5579// Different services access env differently5580const apiKey = process.env.SENDGRID_API_KEY || 'default';5581// Typos go unnoticed: process.env.SENDGRID_API_KY5582}5583}5584```55855586**Correct (use @nestjs/config with validation):**55875588```typescript5589// Setup validated configuration5590import { ConfigModule, ConfigService, registerAs } from '@nestjs/config';5591import * as Joi from 'joi';55925593// config/database.config.ts5594export const databaseConfig = registerAs('database', () => ({5595host: process.env.DB_HOST,5596port: parseInt(process.env.DB_PORT, 10),5597username: process.env.DB_USERNAME,5598password: process.env.DB_PASSWORD,5599database: process.env.DB_NAME,5600}));56015602// config/app.config.ts5603export const appConfig = registerAs('app', () => ({5604port: parseInt(process.env.PORT, 10) || 3000,5605environment: process.env.NODE_ENV || 'development',5606apiPrefix: process.env.API_PREFIX || 'api',5607}));56085609// config/validation.schema.ts5610export const validationSchema = Joi.object({5611NODE_ENV: Joi.string()5612.valid('development', 'production', 'test')5613.default('development'),5614PORT: Joi.number().default(3000),5615DB_HOST: Joi.string().required(),5616DB_PORT: Joi.number().default(5432),5617DB_USERNAME: Joi.string().required(),5618DB_PASSWORD: Joi.string().required(),5619DB_NAME: Joi.string().required(),5620JWT_SECRET: Joi.string().min(32).required(),5621REDIS_URL: Joi.string().uri().required(),5622});56235624// app.module.ts5625@Module({5626imports: [5627ConfigModule.forRoot({5628isGlobal: true, // Available everywhere without importing5629load: [databaseConfig, appConfig],5630validationSchema,5631validationOptions: {5632abortEarly: true, // Stop on first error5633allowUnknown: true, // Allow other env vars5634},5635}),5636TypeOrmModule.forRootAsync({5637inject: [ConfigService],5638useFactory: (config: ConfigService) => ({5639type: 'postgres',5640host: config.get('database.host'),5641port: config.get('database.port'),5642username: config.get('database.username'),5643password: config.get('database.password'),5644database: config.get('database.database'),5645autoLoadEntities: true,5646}),5647}),5648],5649})5650export class AppModule {}56515652// Type-safe configuration access5653export interface AppConfig {5654port: number;5655environment: 'development' | 'production' | 'test';5656apiPrefix: string;5657}56585659export interface DatabaseConfig {5660host: string;5661port: number;5662username: string;5663password: string;5664database: string;5665}56665667// Type-safe access5668@Injectable()5669export class AppService {5670constructor(private config: ConfigService) {}56715672getPort(): number {5673// Type-safe with generic5674return this.config.get<number>('app.port');5675}56765677getDatabaseConfig(): DatabaseConfig {5678return this.config.get<DatabaseConfig>('database');5679}5680}56815682// Inject namespaced config directly5683@Injectable()5684export class DatabaseService {5685constructor(5686@Inject(databaseConfig.KEY)5687private dbConfig: ConfigType<typeof databaseConfig>,5688) {5689// Full type inference!5690const host = this.dbConfig.host; // string5691const port = this.dbConfig.port; // number5692}5693}56945695// Environment files support5696ConfigModule.forRoot({5697envFilePath: [5698`.env.${process.env.NODE_ENV}.local`,5699`.env.${process.env.NODE_ENV}`,5700'.env.local',5701'.env',5702],5703});57045705// .env.development5706// DB_HOST=localhost5707// DB_PORT=543257085709// .env.production5710// DB_HOST=prod-db.example.com5711// DB_PORT=54325712```57135714Reference: [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)57155716---57175718### 10.3 Use Structured Logging57195720**Impact: MEDIUM-HIGH** — Structured logging enables effective debugging and monitoring57215722Use NestJS Logger with structured JSON output in production. Include contextual information (request ID, user ID, operation) to trace requests across services. Avoid console.log and implement proper log levels.57235724**Incorrect (using console.log in production):**57255726```typescript5727// Use console.log in production5728@Injectable()5729export class UsersService {5730async createUser(dto: CreateUserDto): Promise<User> {5731console.log('Creating user:', dto);5732// Not structured, no levels, lost in production logs57335734try {5735const user = await this.repo.save(dto);5736console.log('User created:', user.id);5737return user;5738} catch (error) {5739console.log('Error:', error); // Using log for errors5740throw error;5741}5742}5743}57445745// Log sensitive data5746console.log('Login attempt:', { email, password }); // SECURITY RISK!57475748// Inconsistent log format5749logger.log('User ' + userId + ' created at ' + new Date());5750// Hard to parse, no structure5751```57525753**Correct (use structured logging with context):**57545755```typescript5756// Configure logger in main.ts5757async function bootstrap() {5758const app = await NestFactory.create(AppModule, {5759logger:5760process.env.NODE_ENV === 'production'5761? ['error', 'warn', 'log']5762: ['error', 'warn', 'log', 'debug', 'verbose'],5763});5764}57655766// Use NestJS Logger with context5767@Injectable()5768export class UsersService {5769private readonly logger = new Logger(UsersService.name);57705771async createUser(dto: CreateUserDto): Promise<User> {5772this.logger.log('Creating user', { email: dto.email });57735774try {5775const user = await this.repo.save(dto);5776this.logger.log('User created', { userId: user.id });5777return user;5778} catch (error) {5779this.logger.error('Failed to create user', error.stack, {5780email: dto.email,5781});5782throw error;5783}5784}5785}57865787// Custom logger for JSON output5788@Injectable()5789export class JsonLogger implements LoggerService {5790log(message: string, context?: object): void {5791console.log(5792JSON.stringify({5793level: 'info',5794timestamp: new Date().toISOString(),5795message,5796...context,5797}),5798);5799}58005801error(message: string, trace?: string, context?: object): void {5802console.error(5803JSON.stringify({5804level: 'error',5805timestamp: new Date().toISOString(),5806message,5807trace,5808...context,5809}),5810);5811}58125813warn(message: string, context?: object): void {5814console.warn(5815JSON.stringify({5816level: 'warn',5817timestamp: new Date().toISOString(),5818message,5819...context,5820}),5821);5822}58235824debug(message: string, context?: object): void {5825console.debug(5826JSON.stringify({5827level: 'debug',5828timestamp: new Date().toISOString(),5829message,5830...context,5831}),5832);5833}5834}58355836// Request context logging with ClsModule5837import { ClsModule, ClsService } from 'nestjs-cls';58385839@Module({5840imports: [5841ClsModule.forRoot({5842global: true,5843middleware: {5844mount: true,5845generateId: true,5846},5847}),5848],5849})5850export class AppModule {}58515852// Middleware to set request context5853@Injectable()5854export class RequestContextMiddleware implements NestMiddleware {5855constructor(private cls: ClsService) {}58565857use(req: Request, res: Response, next: NextFunction): void {5858const requestId = req.headers['x-request-id'] || randomUUID();5859this.cls.set('requestId', requestId);5860this.cls.set('userId', req.user?.id);58615862res.setHeader('x-request-id', requestId);5863next();5864}5865}58665867// Logger that includes request context5868@Injectable()5869export class ContextLogger {5870constructor(private cls: ClsService) {}58715872log(message: string, data?: object): void {5873console.log(5874JSON.stringify({5875level: 'info',5876timestamp: new Date().toISOString(),5877requestId: this.cls.get('requestId'),5878userId: this.cls.get('userId'),5879message,5880...data,5881}),5882);5883}58845885error(message: string, error: Error, data?: object): void {5886console.error(5887JSON.stringify({5888level: 'error',5889timestamp: new Date().toISOString(),5890requestId: this.cls.get('requestId'),5891userId: this.cls.get('userId'),5892message,5893error: error.message,5894stack: error.stack,5895...data,5896}),5897);5898}5899}59005901// Pino integration for high-performance logging5902import { LoggerModule } from 'nestjs-pino';59035904@Module({5905imports: [5906LoggerModule.forRoot({5907pinoHttp: {5908level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',5909transport:5910process.env.NODE_ENV !== 'production'5911? { target: 'pino-pretty' }5912: undefined,5913redact: ['req.headers.authorization', 'req.body.password'],5914serializers: {5915req: (req) => ({5916method: req.method,5917url: req.url,5918query: req.query,5919}),5920res: (res) => ({5921statusCode: res.statusCode,5922}),5923},5924},5925}),5926],5927})5928export class AppModule {}59295930// Usage with Pino5931@Injectable()5932export class UsersService {5933constructor(private logger: PinoLogger) {5934this.logger.setContext(UsersService.name);5935}59365937async findOne(id: string): Promise<User> {5938this.logger.info({ userId: id }, 'Finding user');5939// Pino uses first arg for data, second for message5940}5941}5942```59435944Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)59455946---59475948## References59495950- https://docs.nestjs.com5951- https://github.com/nestjs/nest5952- https://typeorm.io5953- https://github.com/typestack/class-validator5954- https://github.com/goldbergyoni/nodebestpractices59555956---59575958*Generated by build-agents.ts on 2026-01-16*5959