git:20260218.048cbae to git:20260223.38c6795

4708 added, 4708 removed, shown as one replacement because the pair was too large to align, first 3,000 lines shown. Audit B to B.

# NestJS Best Practices
**Version 1.1.0**
NestJS Best Practices
January 2026
> **Note:**
> This document is mainly for agents and LLMs to follow when maintaining,
> generating, or refactoring NestJS codebases. Humans may also find it
> useful, but guidance here is optimized for automation and consistency
> by AI-assisted workflows.
---
## Abstract
Comprehensive 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.
---
## Table of Contents
1. [Architecture](#1-architecture) — **CRITICAL**
- 1.1 [Avoid Circular Dependencies](#11-avoid-circular-dependencies)
- 1.2 [Organize by Feature Modules](#12-organize-by-feature-modules)
- 1.3 [Use Proper Module Sharing Patterns](#13-use-proper-module-sharing-patterns)
- 1.4 [Single Responsibility for Services](#14-single-responsibility-for-services)
- 1.5 [Use Event-Driven Architecture for Decoupling](#15-use-event-driven-architecture-for-decoupling)
- 1.6 [Use Repository Pattern for Data Access](#16-use-repository-pattern-for-data-access)
2. [Dependency Injection](#2-dependency-injection) — **CRITICAL**
- 2.1 [Avoid Service Locator Anti-Pattern](#21-avoid-service-locator-anti-pattern)
- 2.2 [Apply Interface Segregation Principle](#22-apply-interface-segregation-principle)
- 2.3 [Honor Liskov Substitution Principle](#23-honor-liskov-substitution-principle)
- 2.4 [Prefer Constructor Injection](#24-prefer-constructor-injection)
- 2.5 [Understand Provider Scopes](#25-understand-provider-scopes)
- 2.6 [Use Injection Tokens for Interfaces](#26-use-injection-tokens-for-interfaces)
3. [Error Handling](#3-error-handling) — **HIGH**
- 3.1 [Handle Async Errors Properly](#31-handle-async-errors-properly)
- 3.2 [Throw HTTP Exceptions from Services](#32-throw-http-exceptions-from-services)
- 3.3 [Use Exception Filters for Error Handling](#33-use-exception-filters-for-error-handling)
4. [Security](#4-security) — **HIGH**
- 4.1 [Implement Secure JWT Authentication](#41-implement-secure-jwt-authentication)
- 4.2 [Implement Rate Limiting](#42-implement-rate-limiting)
- 4.3 [Sanitize Output to Prevent XSS](#43-sanitize-output-to-prevent-xss)
- 4.4 [Use Guards for Authentication and Authorization](#44-use-guards-for-authentication-and-authorization)
- 4.5 [Validate All Input with DTOs and Pipes](#45-validate-all-input-with-dtos-and-pipes)
5. [Performance](#5-performance) — **HIGH**
- 5.1 [Use Async Lifecycle Hooks Correctly](#51-use-async-lifecycle-hooks-correctly)
- 5.2 [Use Lazy Loading for Large Modules](#52-use-lazy-loading-for-large-modules)
- 5.3 [Optimize Database Queries](#53-optimize-database-queries)
- 5.4 [Use Caching Strategically](#54-use-caching-strategically)
6. [Testing](#6-testing) — **MEDIUM-HIGH**
- 6.1 [Use Supertest for E2E Testing](#61-use-supertest-for-e2e-testing)
- 6.2 [Mock External Services in Tests](#62-mock-external-services-in-tests)
- 6.3 [Use Testing Module for Unit Tests](#63-use-testing-module-for-unit-tests)
7. [Database & ORM](#7-database-orm) — **MEDIUM-HIGH**
- 7.1 [Avoid N+1 Query Problems](#71-avoid-n-1-query-problems)
- 7.2 [Use Database Migrations](#72-use-database-migrations)
- 7.3 [Use Transactions for Multi-Step Operations](#73-use-transactions-for-multi-step-operations)
8. [API Design](#8-api-design) — **MEDIUM**
- 8.1 [Use DTOs and Serialization for API Responses](#81-use-dtos-and-serialization-for-api-responses)
- 8.2 [Use Interceptors for Cross-Cutting Concerns](#82-use-interceptors-for-cross-cutting-concerns)
- 8.3 [Use Pipes for Input Transformation](#83-use-pipes-for-input-transformation)
- 8.4 [Use API Versioning for Breaking Changes](#84-use-api-versioning-for-breaking-changes)
9. [Microservices](#9-microservices) — **MEDIUM**
- 9.1 [Implement Health Checks for Microservices](#91-implement-health-checks-for-microservices)
- 9.2 [Use Message and Event Patterns Correctly](#92-use-message-and-event-patterns-correctly)
- 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs)
10. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM**
- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)
- 10.3 [Use Structured Logging](#103-use-structured-logging)
---
## 1. Architecture
**Section Impact: CRITICAL**
### 1.1 Avoid Circular Dependencies
**Impact: CRITICAL** — "#1 cause of runtime crashes"
Circular 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.
**Incorrect (circular module imports):**
```typescript
// users.module.ts
@Module({
imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// orders.module.ts
@Module({
imports: [UsersModule], // Circular dependency!
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}
```
**Correct (extract shared logic or use events):**
```typescript
// Option 1: Extract shared logic to a third module
// shared.module.ts
@Module({
providers: [SharedService],
exports: [SharedService],
})
export class SharedModule {}
// users.module.ts
@Module({
imports: [SharedModule],
providers: [UsersService],
})
export class UsersModule {}
// orders.module.ts
@Module({
imports: [SharedModule],
providers: [OrdersService],
})
export class OrdersModule {}
// Option 2: Use events for decoupled communication
// users.service.ts
@Injectable()
export class UsersService {
constructor(private eventEmitter: EventEmitter2) {}
async createUser(data: CreateUserDto) {
const user = await this.userRepo.save(data);
this.eventEmitter.emit("user.created", user);
return user;
}
}
// orders.service.ts
@Injectable()
export class OrdersService {
@OnEvent("user.created")
handleUserCreated(user: User) {
// React to user creation without direct dependency
}
}
```
Reference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency)
---
### 1.2 Organize by Feature Modules
**Impact: CRITICAL** — "3-5x faster onboarding and development"
Organize 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.
**Incorrect (technical layer organization):**
```typescript
// Technical layer organization (anti-pattern)
src/
├── controllers/
│ ├── users.controller.ts
│ ├── orders.controller.ts
│ └── products.controller.ts
├── services/
│ ├── users.service.ts
│ ├── orders.service.ts
│ └── products.service.ts
├── entities/
│ ├── user.entity.ts
│ ├── order.entity.ts
│ └── product.entity.ts
└── app.module.ts // Imports everything directly
```
**Correct (feature module organization):**
```typescript
// Feature module organization
src/
├── users/
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── users.repository.ts
│ └── users.module.ts
├── orders/
│ ├── dto/
│ ├── entities/
│ ├── orders.controller.ts
│ ├── orders.service.ts
│ └── orders.module.ts
├── shared/
│ ├── guards/
│ ├── interceptors/
│ ├── filters/
│ └── shared.module.ts
└── app.module.ts
// users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService], // Only export what others need
})
export class UsersModule {}
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot(),
UsersModule,
OrdersModule,
SharedModule,
],
})
export class AppModule {}
```
Reference: [NestJS Modules](https://docs.nestjs.com/modules)
---
### 1.3 Use Proper Module Sharing Patterns
**Impact: CRITICAL** — Prevents duplicate instances, memory leaks, and state inconsistency
NestJS 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.
**Incorrect (service provided in multiple modules):**
```typescript
// StorageService provided directly in multiple modules - WRONG
// storage.service.ts
@Injectable()
export class StorageService {
private cache = new Map(); // Each instance has separate state!
store(key: string, value: any) {
this.cache.set(key, value);
}
}
// app.module.ts
@Module({
providers: [StorageService], // Instance #1
controllers: [AppController],
})
export class AppModule {}
// videos.module.ts
@Module({
providers: [StorageService], // Instance #2 - different from AppModule!
controllers: [VideosController],
})
export class VideosModule {}
// Problems:
// 1. Two separate StorageService instances exist
// 2. cache.set() in VideosModule doesn't affect AppModule's cache
// 3. Memory wasted on duplicate instances
// 4. Debugging nightmares when state doesn't sync
```
**Correct (dedicated module with exports):**
```typescript
// storage/storage.module.ts
@Module({
providers: [StorageService],
exports: [StorageService], // Make available to importers
})
export class StorageModule {}
// videos/videos.module.ts
@Module({
imports: [StorageModule], // Import the module, not the service
controllers: [VideosController],
providers: [VideosService],
})
export class VideosModule {}
// channels/channels.module.ts
@Module({
imports: [StorageModule], // Same instance shared
controllers: [ChannelsController],
providers: [ChannelsService],
})
export class ChannelsModule {}
// app.module.ts
@Module({
imports: [
StorageModule, // Only if AppModule itself needs StorageService
VideosModule,
ChannelsModule,
],
})
export class AppModule {}
// Now all modules share the SAME StorageService instance
```
**When to use @Global() (sparingly):**
```typescript
// ONLY for truly cross-cutting concerns
@Global()
@Module({
providers: [ConfigService, LoggerService],
exports: [ConfigService, LoggerService],
})
export class CoreModule {}
// Import once in AppModule
@Module({
imports: [CoreModule], // Registered globally, available everywhere
})
export class AppModule {}
// Other modules don't need to import CoreModule
@Module({
controllers: [UsersController],
providers: [UsersService], // Can inject ConfigService without importing
})
export class UsersModule {}
// WARNING: Don't make everything global!
// - Hides dependencies (can't see what a module needs from imports)
// - Makes testing harder
// - Reserve for: config, logging, database connections
```
**Module re-exporting pattern:**
```typescript
// common.module.ts - shared utilities
@Module({
providers: [DateService, ValidationService],
exports: [DateService, ValidationService],
})
export class CommonModule {}
// core.module.ts - re-exports common for convenience
@Module({
imports: [CommonModule, DatabaseModule],
exports: [CommonModule, DatabaseModule], // Re-export for consumers
})
export class CoreModule {}
// feature.module.ts - imports CoreModule, gets both
@Module({
imports: [CoreModule], // Gets CommonModule + DatabaseModule
controllers: [FeatureController],
})
export class FeatureModule {}
```
Reference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules)
---
### 1.4 Single Responsibility for Services
**Impact: CRITICAL** — "40%+ improvement in testability"
Each 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%+.
**Incorrect (god service anti-pattern):**
```typescript
// God service anti-pattern
@Injectable()
export class UserAndOrderService {
constructor(
private userRepo: UserRepository,
private orderRepo: OrderRepository,
private mailer: MailService,
private payment: PaymentService,
) {}
async createUser(dto: CreateUserDto) {
const user = await this.userRepo.save(dto);
await this.mailer.sendWelcome(user);
return user;
}
async createOrder(userId: string, dto: CreateOrderDto) {
const order = await this.orderRepo.save({ userId, ...dto });
await this.payment.charge(order);
await this.mailer.sendOrderConfirmation(order);
return order;
}
async calculateOrderStats(userId: string) {
// Stats logic mixed in
}
async validatePayment(orderId: string) {
// Payment logic mixed in
}
}
```
**Correct (focused services with single responsibility):**
```typescript
// Focused services with single responsibility
@Injectable()
export class UsersService {
constructor(private userRepo: UserRepository) {}
async create(dto: CreateUserDto): Promise<User> {
return this.userRepo.save(dto);
}
async findById(id: string): Promise<User> {
return this.userRepo.findOneOrFail({ where: { id } });
}
}
@Injectable()
export class OrdersService {
constructor(private orderRepo: OrderRepository) {}
async create(userId: string, dto: CreateOrderDto): Promise<Order> {
return this.orderRepo.save({ userId, ...dto });
}
async findByUser(userId: string): Promise<Order[]> {
return this.orderRepo.find({ where: { userId } });
}
}
@Injectable()
export class OrderStatsService {
constructor(private orderRepo: OrderRepository) {}
async calculateForUser(userId: string): Promise<OrderStats> {
// Focused stats calculation
}
}
// Orchestration in controller or dedicated orchestrator
@Controller("orders")
export class OrdersController {
constructor(
private orders: OrdersService,
private payment: PaymentService,
private notifications: NotificationService,
) {}
@Post()
async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) {
const order = await this.orders.create(user.id, dto);
await this.payment.charge(order);
await this.notifications.sendOrderConfirmation(order);
return order;
}
}
```
Reference: [NestJS Providers](https://docs.nestjs.com/providers)
---
### 1.5 Use Event-Driven Architecture for Decoupling
**Impact: MEDIUM-HIGH** — Enables async processing and modularity
Use `@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.
**Incorrect (direct service coupling):**
```typescript
// Direct service coupling
@Injectable()
export class OrdersService {
constructor(
private inventoryService: InventoryService,
private emailService: EmailService,
private analyticsService: AnalyticsService,
private notificationService: NotificationService,
private loyaltyService: LoyaltyService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Tight coupling - OrdersService knows about all consumers
await this.inventoryService.reserve(order.items);
await this.emailService.sendConfirmation(order);
await this.analyticsService.track("order_created", order);
await this.notificationService.push(order.userId, "Order placed");
await this.loyaltyService.addPoints(order.userId, order.total);
// Adding new behavior requires modifying this service
return order;
}
}
```
**Correct (event-driven decoupling):**
```typescript
// Use EventEmitter for decoupling
import { EventEmitter2 } from "@nestjs/event-emitter";
// Define event
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly items: OrderItem[],
public readonly total: number,
) {}
}
// Service emits events
@Injectable()
export class OrdersService {
constructor(
private eventEmitter: EventEmitter2,
private repo: Repository<Order>,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Emit event - no knowledge of consumers
this.eventEmitter.emit(
"order.created",
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
);
return order;
}
}
// Listeners in separate modules
@Injectable()
export class InventoryListener {
@OnEvent("order.created")
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.inventoryService.reserve(event.items);
}
}
@Injectable()
export class EmailListener {
@OnEvent("order.created")
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.emailService.sendConfirmation(event.orderId);
}
}
@Injectable()
export class AnalyticsListener {
@OnEvent("order.created")
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.analyticsService.track("order_created", {
orderId: event.orderId,
total: event.total,
});
}
}
```
Reference: [NestJS Events](https://docs.nestjs.com/techniques/events)
---
### 1.6 Use Repository Pattern for Data Access
**Impact: HIGH** — Decouples business logic from database
Create 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.
**Incorrect (complex queries in services):**
```typescript
// Complex queries in services
@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic
return this.repo
.createQueryBuilder("user")
.leftJoinAndSelect("user.orders", "order")
.where("user.isActive = :active", { active: true })
.andWhere("user.deletedAt IS NULL")
.groupBy("user.id")
.having("COUNT(order.id) >= :min", { min: minOrders })
.orderBy("user.createdAt", "DESC")
.getMany();
}
// Service becomes bloated with query logic
}
```
**Correct (custom repository with encapsulated queries):**
```typescript
// Custom repository with encapsulated queries
@Injectable()
export class UsersRepository {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.repo.findOne({ where: { email } });
}
async findActiveWithMinOrders(minOrders: number): Promise<User[]> {
return this.repo
.createQueryBuilder("user")
.leftJoinAndSelect("user.orders", "order")
.where("user.isActive = :active", { active: true })
.andWhere("user.deletedAt IS NULL")
.groupBy("user.id")
.having("COUNT(order.id) >= :min", { min: minOrders })
.orderBy("user.createdAt", "DESC")
.getMany();
}
async save(user: User): Promise<User> {
return this.repo.save(user);
}
}
// Clean service with business logic only
@Injectable()
export class UsersService {
constructor(private usersRepo: UsersRepository) {}
async getActiveUsersWithOrders(): Promise<User[]> {
return this.usersRepo.findActiveWithMinOrders(1);
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.usersRepo.findByEmail(dto.email);
if (existing) {
throw new ConflictException("Email already registered");
}
const user = new User();
user.email = dto.email;
user.name = dto.name;
return this.usersRepo.save(user);
}
}
```
Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)
---
## 2. Dependency Injection
**Section Impact: CRITICAL**
### 2.1 Avoid Service Locator Anti-Pattern
**Impact: HIGH** — Hides dependencies and breaks testability
Avoid 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.
**Incorrect (service locator anti-pattern):**
```typescript
// Use ModuleRef to get dependencies dynamically
@Injectable()
export class OrdersService {
constructor(private moduleRef: ModuleRef) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
// Dependencies are hidden - not visible in constructor
const usersService = this.moduleRef.get(UsersService);
const inventoryService = this.moduleRef.get(InventoryService);
const paymentService = this.moduleRef.get(PaymentService);
const user = await usersService.findOne(dto.userId);
// ... rest of logic
}
}
// Global singleton container
class ServiceContainer {
private static instance: ServiceContainer;
private services = new Map<string, any>();
static getInstance(): ServiceContainer {
if (!this.instance) {
this.instance = new ServiceContainer();
}
return this.instance;
}
get<T>(key: string): T {
return this.services.get(key);
}
}
```
**Correct (constructor injection with explicit dependencies):**
```typescript
// Use constructor injection - dependencies are explicit
@Injectable()
export class OrdersService {
constructor(
private usersService: UsersService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const user = await this.usersService.findOne(dto.userId);
const inventory = await this.inventoryService.check(dto.items);
// Dependencies are clear and testable
}
}
// Easy to test with mocks
describe("OrdersService", () => {
let service: OrdersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
OrdersService,
{ provide: UsersService, useValue: mockUsersService },
{ provide: InventoryService, useValue: mockInventoryService },
{ provide: PaymentService, useValue: mockPaymentService },
],
}).compile();
service = module.get(OrdersService);
});
});
// VALID: Factory pattern for dynamic instantiation
@Injectable()
export class HandlerFactory {
constructor(private moduleRef: ModuleRef) {}
getHandler(type: string): Handler {
switch (type) {
case "email":
return this.moduleRef.get(EmailHandler);
case "sms":
return this.moduleRef.get(SmsHandler);
default:
return this.moduleRef.get(DefaultHandler);
}
}
}
```
Reference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref)
---
### 2.2 Apply Interface Segregation Principle
**Impact: HIGH** — Reduces coupling and improves testability by 30-50%
Clients 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.
**Incorrect (fat interface forcing unused dependencies):**
```typescript
// Fat interface - forces all consumers to depend on everything
interface NotificationService {
sendEmail(to: string, subject: string, body: string): Promise<void>;
sendSms(phone: string, message: string): Promise<void>;
sendPush(userId: string, notification: PushPayload): Promise<void>;
sendSlack(channel: string, message: string): Promise<void>;
logNotification(type: string, payload: any): Promise<void>;
getDeliveryStatus(id: string): Promise<DeliveryStatus>;
retryFailed(id: string): Promise<void>;
scheduleNotification(dto: ScheduleDto): Promise<string>;
}
// Consumer only needs email, but must mock everything for tests
@Injectable()
export class OrdersService {
constructor(
private notifications: NotificationService, // Depends on 8 methods, uses 1
) {}
async confirmOrder(order: Order): Promise<void> {
await this.notifications.sendEmail(
order.customer.email,
"Order Confirmed",
`Your order ${order.id} has been confirmed.`,
);
}
}
// Testing is painful - must mock unused methods
const mockNotificationService = {
sendEmail: jest.fn(),
sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required
getDeliveryStatus: jest.fn(), // Never used, but required
retryFailed: jest.fn(), // Never used, but required
scheduleNotification: jest.fn(), // Never used, but required
};
```
**Correct (segregated interfaces by capability):**
```typescript
// Segregated interfaces - each focused on one capability
interface EmailSender {
sendEmail(to: string, subject: string, body: string): Promise<void>;
}
interface SmsSender {
sendSms(phone: string, message: string): Promise<void>;
}
interface PushSender {
sendPush(userId: string, notification: PushPayload): Promise<void>;
}
interface NotificationLogger {
logNotification(type: string, payload: any): Promise<void>;
}
interface NotificationScheduler {
scheduleNotification(dto: ScheduleDto): Promise<string>;
}
// Implementation can implement multiple interfaces
@Injectable()
export class NotificationService implements EmailSender, SmsSender, PushSender {
async sendEmail(to: string, subject: string, body: string): Promise<void> {
// Email implementation
}
async sendSms(phone: string, message: string): Promise<void> {
// SMS implementation
}
async sendPush(userId: string, notification: PushPayload): Promise<void> {
// Push implementation
}
}
// Or separate implementations
@Injectable()
- export class SendGridEmailService implements EmailSender {
- async sendEmail(to: string, subject: string, body: string): Promise<void> {
- // SendGrid-specific implementation
- }
- }
-
- // Consumer depends only on what it needs
- @Injectable()
- export class OrdersService {
- constructor(
- @Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency
- ) {}
-
- async confirmOrder(order: Order): Promise<void> {
- await this.emailSender.sendEmail(
- order.customer.email,
- "Order Confirmed",
- `Your order ${order.id} has been confirmed.`,
- );
- }
- }
-
- // Testing is simple - only mock what's used
- const mockEmailSender: EmailSender = {
- sendEmail: jest.fn(),
- };
-
- // Module registration with tokens
- export const EMAIL_SENDER = Symbol("EMAIL_SENDER");
- export const SMS_SENDER = Symbol("SMS_SENDER");
-
- @Module({
- providers: [
- { provide: EMAIL_SENDER, useClass: SendGridEmailService },
- { provide: SMS_SENDER, useClass: TwilioSmsService },
- ],
- exports: [EMAIL_SENDER, SMS_SENDER],
- })
- export class NotificationModule {}
- ```
-
- **Combining interfaces when needed:**
-
- ```typescript
- // Sometimes a consumer legitimately needs multiple capabilities
- interface EmailAndSmsSender extends EmailSender, SmsSender {}
-
- // Or use intersection types
- type MultiChannelSender = EmailSender & SmsSender & PushSender;
-
- // Consumer that genuinely needs multiple channels
- @Injectable()
- export class AlertService {
- constructor(
- @Inject(MULTI_CHANNEL_SENDER)
- private sender: EmailSender & SmsSender,
- ) {}
-
- async sendCriticalAlert(user: User, message: string): Promise<void> {
- await Promise.all([
- this.sender.sendEmail(user.email, "Critical Alert", message),
- this.sender.sendSms(user.phone, message),
- ]);
- }
- }
- ```
-
- Reference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)
-
- ---
-
- ### 2.3 Honor Liskov Substitution Principle
-
- **Impact: HIGH** — Ensures implementations are truly interchangeable without breaking callers
-
- Subtypes 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.
-
- **Incorrect (implementation violates the contract):**
-
- ```typescript
- // Base interface with clear contract
- interface PaymentGateway {
- /**
- * Charges the specified amount.
- * @returns PaymentResult on success
- * @throws PaymentFailedException on payment failure
- */
- charge(amount: number, currency: string): Promise<PaymentResult>;
- }
-
- // Production implementation - follows the contract
- @Injectable()
- export class StripeService implements PaymentGateway {
- async charge(amount: number, currency: string): Promise<PaymentResult> {
- const response = await this.stripe.charges.create({ amount, currency });
- return { success: true, transactionId: response.id, amount };
- }
- }
-
- // Mock that violates LSP - different behavior!
- @Injectable()
- export class MockPaymentService implements PaymentGateway {
- async charge(amount: number, currency: string): Promise<PaymentResult> {
- // VIOLATION 1: Throws for valid input (contract says return PaymentResult)
- if (amount > 1000) {
- throw new Error("Mock does not support large amounts");
- }
-
- // VIOLATION 2: Returns null instead of PaymentResult
- if (currency !== "USD") {
- return null as any; // Real service would convert or reject properly
- }
-
- // VIOLATION 3: Missing required field
- return { success: true } as PaymentResult; // Missing transactionId!
- }
- }
-
- // Consumer trusts the contract
- @Injectable()
- export class OrdersService {
- constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
-
- async checkout(order: Order): Promise<void> {
- const result = await this.payment.charge(order.total, order.currency);
- // These fail with MockPaymentService:
- await this.saveTransaction(result.transactionId); // undefined!
- await this.sendReceipt(result); // might be null!
- }
- }
- ```
-
- **Correct (implementations honor the contract):**
-
- ```typescript
- // Well-defined interface with documented behavior
- interface PaymentGateway {
- /**
- * Charges the specified amount.
- * @param amount - Amount in smallest currency unit (cents)
- * @param currency - ISO 4217 currency code
- * @returns PaymentResult with transactionId, success status, and amount
- * @throws PaymentFailedException if charge is declined
- * @throws InvalidCurrencyException if currency is not supported
- */
- charge(amount: number, currency: string): Promise<PaymentResult>;
-
- /**
- * Refunds a previous charge.
- * @throws TransactionNotFoundException if transactionId is invalid
- */
- refund(transactionId: string, amount?: number): Promise<RefundResult>;
- }
-
- // Production implementation
- @Injectable()
- export class StripeService implements PaymentGateway {
- async charge(amount: number, currency: string): Promise<PaymentResult> {
- try {
- const response = await this.stripe.charges.create({ amount, currency });
- return {
- success: true,
- transactionId: response.id,
- amount: response.amount,
- };
- } catch (error) {
- if (error.type === "card_error") {
- throw new PaymentFailedException(error.message);
- }
- throw error;
- }
- }
-
- async refund(transactionId: string, amount?: number): Promise<RefundResult> {
- // Implementation...
- }
- }
-
- // Mock that honors LSP - same contract, same behavior shape
- @Injectable()
- export class MockPaymentService implements PaymentGateway {
- private transactions = new Map<string, PaymentResult>();
-
- async charge(amount: number, currency: string): Promise<PaymentResult> {
- // Honor the contract: validate currency like real service would
- if (!["USD", "EUR", "GBP"].includes(currency)) {
- throw new InvalidCurrencyException(`Unsupported currency: ${currency}`);
- }
-
- // Simulate decline for specific test scenarios
- if (amount === 99999) {
- throw new PaymentFailedException("Card declined (test scenario)");
- }
-
- // Return same shape as production
- const result: PaymentResult = {
- success: true,
- transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`,
- amount,
- };
-
- this.transactions.set(result.transactionId, result);
- return result;
- }
-
- async refund(transactionId: string, amount?: number): Promise<RefundResult> {
- // Honor the contract: throw if transaction not found
- if (!this.transactions.has(transactionId)) {
- throw new TransactionNotFoundException(transactionId);
- }
-
- return {
- success: true,
- refundId: `refund_${transactionId}`,
- amount: amount ?? this.transactions.get(transactionId)!.amount,
- };
- }
- }
-
- // Consumer can swap implementations safely
- @Injectable()
- export class OrdersService {
- constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
-
- async checkout(order: Order): Promise<Order> {
- try {
- const result = await this.payment.charge(order.total, order.currency);
- // Works with both StripeService and MockPaymentService
- order.transactionId = result.transactionId;
- order.status = "paid";
- return order;
- } catch (error) {
- if (error instanceof PaymentFailedException) {
- order.status = "payment_failed";
- return order;
- }
- throw error;
- }
- }
- }
- ```
-
- **Testing LSP compliance:**
-
- ```typescript
- // Shared test suite that any implementation must pass
- function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
- describe("PaymentGateway contract", () => {
- let gateway: PaymentGateway;
-
- beforeEach(() => {
- gateway = createGateway();
- });
-
- it("returns PaymentResult with all required fields", async () => {
- const result = await gateway.charge(1000, "USD");
- expect(result).toHaveProperty("success");
- expect(result).toHaveProperty("transactionId");
- expect(result).toHaveProperty("amount");
- expect(typeof result.transactionId).toBe("string");
- });
-
- it("throws InvalidCurrencyException for unsupported currency", async () => {
- await expect(gateway.charge(1000, "INVALID")).rejects.toThrow(
- InvalidCurrencyException,
- );
- });
-
- it("throws TransactionNotFoundException for invalid refund", async () => {
- await expect(gateway.refund("nonexistent")).rejects.toThrow(
- TransactionNotFoundException,
- );
- });
- });
- }
-
- // Run against all implementations
- describe("StripeService", () => {
- testPaymentGatewayContract(() => new StripeService(mockStripeClient));
- });
-
- describe("MockPaymentService", () => {
- testPaymentGatewayContract(() => new MockPaymentService());
- });
- ```
-
- Reference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)
-
- ---
-
- ### 2.4 Prefer Constructor Injection
-
- **Impact: CRITICAL** — Required for proper DI and testing
-
- Always 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.
-
- **Incorrect (property injection with hidden dependencies):**
-
- ```typescript
- // Property injection - avoid unless necessary
- @Injectable()
- export class UsersService {
- @Inject()
- private userRepo: UserRepository; // Hidden dependency
-
- @Inject("CONFIG")
- private config: ConfigType; // Also hidden
-
- async findAll() {
- return this.userRepo.find();
- }
- }
-
- // Problems:
- // 1. Dependencies not visible in constructor
- // 2. Service can be instantiated without dependencies in tests
- // 3. TypeScript can't enforce dependency types at instantiation
- ```
-
- **Correct (constructor injection with explicit dependencies):**
-
- ```typescript
- // Constructor injection - explicit and testable
- @Injectable()
- export class UsersService {
- constructor(
- private readonly userRepo: UserRepository,
- @Inject("CONFIG") private readonly config: ConfigType,
- ) {}
-
- async findAll(): Promise<User[]> {
- return this.userRepo.find();
- }
- }
-
- // Testing is straightforward
- describe("UsersService", () => {
- let service: UsersService;
- let mockRepo: jest.Mocked<UserRepository>;
-
- beforeEach(() => {
- mockRepo = {
- find: jest.fn(),
- save: jest.fn(),
- } as any;
-
- service = new UsersService(mockRepo, { dbUrl: "test" });
- });
-
- it("should find all users", async () => {
- mockRepo.find.mockResolvedValue([{ id: "1", name: "Test" }]);
- const result = await service.findAll();
- expect(result).toHaveLength(1);
- });
- });
-
- // Only use property injection for optional dependencies
- @Injectable()
- export class LoggingService {
- @Optional()
- @Inject("ANALYTICS")
- private analytics?: AnalyticsService;
-
- log(message: string) {
- console.log(message);
- this.analytics?.track("log", message); // Optional enhancement
- }
- }
- ```
-
- Reference: [NestJS Providers](https://docs.nestjs.com/providers)
-
- ---
-
- ### 2.5 Understand Provider Scopes
-
- **Impact: CRITICAL** — Prevents data leaks and performance issues
-
- NestJS 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.
-
- **Incorrect (wrong scope usage):**
-
- ```typescript
- // Request-scoped when not needed (performance hit)
- @Injectable({ scope: Scope.REQUEST })
- export class UsersService {
- // This creates a new instance for EVERY request
- // All dependencies also become request-scoped
- async findAll() {
- return this.userRepo.find();
- }
- }
-
- // Singleton with mutable request state
- @Injectable() // Default: singleton
- export class RequestContextService {
- private userId: string; // DANGER: Shared across all requests!
-
- setUser(userId: string) {
- this.userId = userId; // Overwrites for all concurrent requests
- }
-
- getUser() {
- return this.userId; // Returns wrong user!
- }
- }
- ```
-
- **Correct (appropriate scope for each use case):**
-
- ```typescript
- // Singleton for stateless services (default, most common)
- @Injectable()
- export class UsersService {
- constructor(private readonly userRepo: UserRepository) {}
-
- async findById(id: string): Promise<User> {
- return this.userRepo.findOne({ where: { id } });
- }
- }
-
- // Request-scoped ONLY when you need request context
- @Injectable({ scope: Scope.REQUEST })
- export class RequestContextService {
- private userId: string;
-
- setUser(userId: string) {
- this.userId = userId;
- }
-
- getUser(): string {
- return this.userId;
- }
- }
-
- // Better: Use NestJS built-in request context
- import { REQUEST } from "@nestjs/core";
- import { Request } from "express";
-
- @Injectable({ scope: Scope.REQUEST })
- export class AuditService {
- constructor(@Inject(REQUEST) private request: Request) {}
-
- log(action: string) {
- console.log(`User ${this.request.user?.id} performed ${action}`);
- }
- }
-
- // Best: Use ClsModule for async context (no scope bubble-up)
- import { ClsService } from "nestjs-cls";
-
- @Injectable() // Stays singleton!
- export class AuditService {
- constructor(private cls: ClsService) {}
-
- log(action: string) {
- const userId = this.cls.get("userId");
- console.log(`User ${userId} performed ${action}`);
- }
- }
- ```
-
- Reference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes)
-
- ---
-
- ### 2.6 Use Injection Tokens for Interfaces
-
- **Impact: HIGH** — Enables interface-based DI at runtime
-
- TypeScript 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.
-
- **Incorrect (interface can't be used as token):**
-
- ```typescript
- // Interface can't be used as injection token
- interface PaymentGateway {
- charge(amount: number): Promise<PaymentResult>;
- }
-
- @Injectable()
- export class StripeService implements PaymentGateway {
- charge(amount: number) {
- /* ... */
- }
- }
-
- @Injectable()
- export class OrdersService {
- // This WON'T work - PaymentGateway doesn't exist at runtime
- constructor(private payment: PaymentGateway) {}
- }
- ```
-
- **Correct (symbol tokens or abstract classes):**
-
- ```typescript
- // Option 1: String/Symbol tokens (most flexible)
- export const PAYMENT_GATEWAY = Symbol("PAYMENT_GATEWAY");
-
- export interface PaymentGateway {
- charge(amount: number): Promise<PaymentResult>;
- }
-
- @Injectable()
- export class StripeService implements PaymentGateway {
- async charge(amount: number): Promise<PaymentResult> {
- // Stripe implementation
- }
- }
-
- @Injectable()
- export class MockPaymentService implements PaymentGateway {
- async charge(amount: number): Promise<PaymentResult> {
- return { success: true, id: "mock-id" };
- }
- }
-
- // Module registration
- @Module({
- providers: [
- {
- provide: PAYMENT_GATEWAY,
- useClass:
- process.env.NODE_ENV === "test" ? MockPaymentService : StripeService,
- },
- ],
- exports: [PAYMENT_GATEWAY],
- })
- export class PaymentModule {}
-
- // Injection
- @Injectable()
- export class OrdersService {
- constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
-
- async createOrder(dto: CreateOrderDto) {
- await this.payment.charge(dto.amount);
- }
- }
-
- // Option 2: Abstract class (carries runtime type info)
- export abstract class PaymentGateway {
- abstract charge(amount: number): Promise<PaymentResult>;
- }
-
- @Injectable()
- export class StripeService extends PaymentGateway {
- async charge(amount: number): Promise<PaymentResult> {
- // Implementation
- }
- }
-
- // No @Inject needed with abstract class
- @Injectable()
- export class OrdersService {
- constructor(private payment: PaymentGateway) {}
- }
- ```
-
- Reference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)
-
- ---
-
- ## 3. Error Handling
-
- **Section Impact: HIGH**
-
- ### 3.1 Handle Async Errors Properly
-
- **Impact: HIGH** — Prevents process crashes from unhandled rejections
-
- NestJS 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.
-
- **Incorrect (fire-and-forget without error handling):**
-
- ```typescript
- // Fire-and-forget without error handling
- @Injectable()
- export class UsersService {
- async createUser(dto: CreateUserDto): Promise<User> {
- const user = await this.repo.save(dto);
-
- // Fire and forget - if this fails, error is unhandled!
- this.emailService.sendWelcome(user.email);
-
- return user;
- }
- }
-
- // Unhandled promise in event handler
- @Injectable()
- export class OrdersService {
- @OnEvent('order.created')
- handleOrderCreated(event: OrderCreatedEvent) {
- // This returns a promise but it's not awaited!
- this.processOrder(event);
- // Errors will crash the process
- }
-
- private async processOrder(event: OrderCreatedEvent): Promise<void> {
- await this.inventoryService.reserve(event.items);
- await this.notificationService.send(event.userId);
- }
- }
-
- // Missing try-catch in scheduled tasks
- @Cron('0 0 * * *')
- async dailyCleanup(): Promise<void> {
- await this.cleanupService.run();
- // If this throws, no error handling
- }
- ```
-
- **Correct (explicit async error handling):**
-
- ```typescript
- // Handle fire-and-forget with explicit catch
- @Injectable()
- export class UsersService {
- private readonly logger = new Logger(UsersService.name);
-
- async createUser(dto: CreateUserDto): Promise<User> {
- const user = await this.repo.save(dto);
-
- // Explicitly catch and log errors
- this.emailService.sendWelcome(user.email).catch((error) => {
- this.logger.error("Failed to send welcome email", error.stack);
- // Optionally queue for retry
- });
-
- return user;
- }
- }
-
- // Properly handle async event handlers
- @Injectable()
- export class OrdersService {
- private readonly logger = new Logger(OrdersService.name);
-
- @OnEvent("order.created")
- async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
- try {
- await this.processOrder(event);
- } catch (error) {
- this.logger.error("Failed to process order", { event, error });
- // Don't rethrow - would crash the process
- await this.deadLetterQueue.add("order.created", event);
- }
- }
- }
-
- // Safe scheduled tasks
- @Injectable()
- export class CleanupService {
- private readonly logger = new Logger(CleanupService.name);
-
- @Cron("0 0 * * *")
- async dailyCleanup(): Promise<void> {
- try {
- await this.cleanupService.run();
- this.logger.log("Daily cleanup completed");
- } catch (error) {
- this.logger.error("Daily cleanup failed", error.stack);
- // Alert or retry logic
- }
- }
- }
-
- // Global unhandled rejection handler in main.ts
- async function bootstrap() {
- const app = await NestFactory.create(AppModule);
- const logger = new Logger("Bootstrap");
-
- process.on("unhandledRejection", (reason, promise) => {
- logger.error("Unhandled Rejection at:", promise, "reason:", reason);
- });
-
- process.on("uncaughtException", (error) => {
- logger.error("Uncaught Exception:", error);
- process.exit(1);
- });
-
- await app.listen(3000);
- }
- ```
-
- Reference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection)
-
- ---
-
- ### 3.2 Throw HTTP Exceptions from Services
-
- **Impact: HIGH** — Keeps controllers thin and simplifies error handling
-
- It'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.
-
- **Incorrect (return error objects instead of throwing):**
-
- ```typescript
- // Return error objects instead of throwing
- @Injectable()
- export class UsersService {
- async findById(id: string): Promise<{ user?: User; error?: string }> {
- const user = await this.repo.findOne({ where: { id } });
- if (!user) {
- return { error: "User not found" }; // Controller must check this
- }
- return { user };
- }
- }
-
- @Controller("users")
- export class UsersController {
- @Get(":id")
- async findOne(@Param("id") id: string) {
- const result = await this.usersService.findById(id);
- if (result.error) {
- throw new NotFoundException(result.error);
- }
- return result.user;
- }
- }
- ```
-
- **Correct (throw exceptions directly from service):**
-
- ```typescript
- // Throw exceptions directly from service
- @Injectable()
- export class UsersService {
- constructor(private readonly repo: UserRepository) {}
-
- async findById(id: string): Promise<User> {
- const user = await this.repo.findOne({ where: { id } });
- if (!user) {
- throw new NotFoundException(`User #${id} not found`);
- }
- return user;
- }
-
- async create(dto: CreateUserDto): Promise<User> {
- const existing = await this.repo.findOne({
- where: { email: dto.email },
- });
- if (existing) {
- throw new ConflictException("Email already registered");
- }
- return this.repo.save(dto);
- }
-
- async update(id: string, dto: UpdateUserDto): Promise<User> {
- const user = await this.findById(id); // Throws if not found
- Object.assign(user, dto);
- return this.repo.save(user);
- }
- }
-
- // Controller stays thin
- @Controller("users")
- export class UsersController {
- @Get(":id")
- findOne(@Param("id") id: string): Promise<User> {
- return this.usersService.findById(id);
- }
-
- @Post()
- create(@Body() dto: CreateUserDto): Promise<User> {
- return this.usersService.create(dto);
- }
- }
-
- // For layer-agnostic services, use domain exceptions
- export class EntityNotFoundException extends Error {
- constructor(
- public readonly entity: string,
- public readonly id: string,
- ) {
- super(`${entity} with ID "${id}" not found`);
- }
- }
-
- // Map to HTTP in exception filter
- @Catch(EntityNotFoundException)
- export class EntityNotFoundFilter implements ExceptionFilter {
- catch(exception: EntityNotFoundException, host: ArgumentsHost) {
- const ctx = host.switchToHttp();
- const response = ctx.getResponse<Response>();
-
- response.status(404).json({
- statusCode: 404,
- message: exception.message,
- entity: exception.entity,
- id: exception.id,
- });
- }
- }
- ```
-
- Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)
-
- ---
-
- ### 3.3 Use Exception Filters for Error Handling
-
- **Impact: HIGH** — Consistent, centralized error handling
-
- Never 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.
-
- **Incorrect (manual error handling in controllers):**
-
- ```typescript
- // Manual error handling in controllers
- @Controller("users")
- export class UsersController {
- @Get(":id")
- async findOne(@Param("id") id: string, @Res() res: Response) {
- try {
- const user = await this.usersService.findById(id);
- if (!user) {
- return res.status(404).json({
- statusCode: 404,
- message: "User not found",
- });
- }
- return res.json(user);
- } catch (error) {
- console.error(error);
- return res.status(500).json({
- statusCode: 500,
- message: "Internal server error",
- });
- }
- }
- }
- ```
-
- **Correct (exception filters with consistent handling):**
-
- ```typescript
- // Use built-in and custom exceptions
- @Controller("users")
- export class UsersController {
- @Get(":id")
- async findOne(@Param("id") id: string): Promise<User> {
- const user = await this.usersService.findById(id);
- if (!user) {
- throw new NotFoundException(`User #${id} not found`);
- }
- return user;
- }
- }
-
- // Custom domain exception
- export class UserNotFoundException extends NotFoundException {
- constructor(userId: string) {
- super({
- statusCode: 404,
- error: "Not Found",
- message: `User with ID "${userId}" not found`,
- code: "USER_NOT_FOUND",
- });
- }
- }
-
- // Custom exception filter for domain errors
- @Catch(DomainException)
- export class DomainExceptionFilter implements ExceptionFilter {
- catch(exception: DomainException, host: ArgumentsHost) {
- const ctx = host.switchToHttp();
- const response = ctx.getResponse<Response>();
- const request = ctx.getRequest<Request>();
-
- const status = exception.getStatus?.() || 400;
-
- response.status(status).json({
- statusCode: status,
- code: exception.code,
- message: exception.message,
- timestamp: new Date().toISOString(),
- path: request.url,
- });
- }
- }
-
- // Global exception filter for unhandled errors
- @Catch()
- export class AllExceptionsFilter implements ExceptionFilter {
- constructor(private readonly logger: Logger) {}
-
- catch(exception: unknown, host: ArgumentsHost) {
- const ctx = host.switchToHttp();
- const response = ctx.getResponse<Response>();
- const request = ctx.getRequest<Request>();
-
- const status =
- exception instanceof HttpException
- ? exception.getStatus()
- : HttpStatus.INTERNAL_SERVER_ERROR;
-
- const message =
- exception instanceof HttpException
- ? exception.message
- : "Internal server error";
-
- this.logger.error(
- `${request.method} ${request.url}`,
- exception instanceof Error ? exception.stack : exception,
- );
-
- response.status(status).json({
- statusCode: status,
- message,
- timestamp: new Date().toISOString(),
- path: request.url,
- });
- }
- }
-
- // Register globally in main.ts
- app.useGlobalFilters(
- new AllExceptionsFilter(app.get(Logger)),
- new DomainExceptionFilter(),
- );
-
- // Or via module
- @Module({
- providers: [
- {
- provide: APP_FILTER,
- useClass: AllExceptionsFilter,
- },
- ],
- })
- export class AppModule {}
- ```
-
- Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)
-
- ---
-
- ## 4. Security
-
- **Section Impact: HIGH**
-
- ### 4.1 Implement Secure JWT Authentication
-
- **Impact: CRITICAL** — Essential for secure APIs
-
- Use `@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.
-
- **Incorrect (insecure JWT implementation):**
-
- ```typescript
- // Hardcode secrets
- @Module({
- imports: [
- JwtModule.register({
- secret: 'my-secret-key', // Exposed in code
- signOptions: { expiresIn: '7d' }, // Too long
- }),
- ],
- })
- export class AuthModule {}
-
- // Store sensitive data in JWT
- async login(user: User): Promise<{ accessToken: string }> {
- const payload = {
- sub: user.id,
- email: user.email,
- password: user.password, // NEVER include password!
- ssn: user.ssn, // NEVER include sensitive data!
- isAdmin: user.isAdmin, // Can be tampered if not verified
- };
- return { accessToken: this.jwtService.sign(payload) };
- }
-
- // Skip token validation
- @Injectable()
- export class JwtStrategy extends PassportStrategy(Strategy) {
- constructor() {
- super({
- jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
- secretOrKey: 'my-secret',
- });
- }
-
- async validate(payload: any): Promise<any> {
- return payload; // No validation of user existence
- }
- }
- ```
-
- **Correct (secure JWT with refresh tokens):**
-
- ```typescript
- // Secure JWT configuration
- @Module({
- imports: [
- JwtModule.registerAsync({
- imports: [ConfigModule],
- inject: [ConfigService],
- useFactory: (config: ConfigService) => ({
- secret: config.get<string>("JWT_SECRET"),
- signOptions: {
- expiresIn: "15m", // Short-lived access tokens
- issuer: config.get<string>("JWT_ISSUER"),
- audience: config.get<string>("JWT_AUDIENCE"),
- },
- }),
- }),
- PassportModule.register({ defaultStrategy: "jwt" }),
- ],
- })
- export class AuthModule {}
-
- // Minimal JWT payload
- @Injectable()
- export class AuthService {
- async login(user: User): Promise<TokenResponse> {
- // Only include necessary, non-sensitive data
- const payload: JwtPayload = {
- sub: user.id,
- email: user.email,
- roles: user.roles,
- iat: Math.floor(Date.now() / 1000),
- };
-
- const accessToken = this.jwtService.sign(payload);
- const refreshToken = await this.createRefreshToken(user.id);
-
- return { accessToken, refreshToken, expiresIn: 900 };
- }
-
- private async createRefreshToken(userId: string): Promise<string> {
- const token = randomBytes(32).toString("hex");
- const hashedToken = await bcrypt.hash(token, 10);
-
- await this.refreshTokenRepo.save({
- userId,
- token: hashedToken,
- expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
- });
-
- return token;
- }
- }
-
- // Proper JWT strategy with validation
- @Injectable()
- export class JwtStrategy extends PassportStrategy(Strategy) {
- constructor(
- private config: ConfigService,
- private usersService: UsersService,
- ) {
- super({
- jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
- secretOrKey: config.get<string>("JWT_SECRET"),
- ignoreExpiration: false,
- issuer: config.get<string>("JWT_ISSUER"),
- audience: config.get<string>("JWT_AUDIENCE"),
- });
- }
-
- async validate(payload: JwtPayload): Promise<User> {
- // Verify user still exists and is active
- const user = await this.usersService.findById(payload.sub);
-
- if (!user || !user.isActive) {
- throw new UnauthorizedException("User not found or inactive");
- }
-
- // Verify token wasn't issued before password change
- if (user.passwordChangedAt) {
- const tokenIssuedAt = new Date(payload.iat * 1000);
- if (tokenIssuedAt < user.passwordChangedAt) {
- throw new UnauthorizedException("Token invalidated by password change");
- }
- }
-
- return user;
- }
- }
- ```
-
- Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)
-
- ---
-
- ### 4.2 Implement Rate Limiting
-
- **Impact: HIGH** — Protects against abuse and ensures fair resource usage
-
- Use `@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.
-
- **Incorrect (no rate limiting on sensitive endpoints):**
-
- ```typescript
- // No rate limiting on sensitive endpoints
- @Controller("auth")
- export class AuthController {
- @Post("login")
- async login(@Body() dto: LoginDto): Promise<TokenResponse> {
- // Attackers can brute-force credentials
- return this.authService.login(dto);
- }
-
- @Post("forgot-password")
- async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {
- // Can be abused to spam users with emails
- return this.authService.sendResetEmail(dto.email);
- }
- }
-
- // Same limits for all endpoints
- @UseGuards(ThrottlerGuard)
- @Controller("api")
- export class ApiController {
- @Get("public-data")
- async getPublic() {} // Should allow more requests
-
- @Post("process-payment")
- async payment() {} // Should be more restrictive
- }
- ```
-
- **Correct (configured throttler with endpoint-specific limits):**
-
- ```typescript
- // Configure throttler globally with multiple limits
- import { ThrottlerModule, ThrottlerGuard } from "@nestjs/throttler";
-
- @Module({
- imports: [
- ThrottlerModule.forRoot([
- {
- name: "short",
- ttl: 1000, // 1 second
- limit: 3, // 3 requests per second
- },
- {
- name: "medium",
- ttl: 10000, // 10 seconds
- limit: 20, // 20 requests per 10 seconds
- },
- {
- name: "long",
- ttl: 60000, // 1 minute
- limit: 100, // 100 requests per minute
- },
- ]),
- ],
- providers: [
- {
- provide: APP_GUARD,
- useClass: ThrottlerGuard,
- },
- ],
- })
- export class AppModule {}
-
- // Override limits per endpoint
- @Controller("auth")
- export class AuthController {
- @Post("login")
- @Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute
- async login(@Body() dto: LoginDto): Promise<TokenResponse> {
- return this.authService.login(dto);
- }
-
- @Post("forgot-password")
- @Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour
- async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {
- return this.authService.sendResetEmail(dto.email);
- }
- }
-
- // Skip throttling for certain routes
- @Controller("health")
- export class HealthController {
- @Get()
- @SkipThrottle()
- check(): string {
- return "OK";
- }
- }
-
- // Custom throttle per user type
- @Injectable()
- export class CustomThrottlerGuard extends ThrottlerGuard {
- protected async getTracker(req: Request): Promise<string> {
- // Use user ID if authenticated, IP otherwise
- return req.user?.id || req.ip;
- }
-
- protected async getLimit(context: ExecutionContext): Promise<number> {
- const request = context.switchToHttp().getRequest();
-
- // Higher limits for authenticated users
- if (request.user) {
- return request.user.isPremium ? 1000 : 200;
- }
-
- return 50; // Anonymous users
- }
- }
- ```
-
- Reference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)
-
- ---
-
- ### 4.3 Sanitize Output to Prevent XSS
-
- **Impact: HIGH** — XSS vulnerabilities can compromise user sessions and data
-
- While 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.
-
- **Incorrect (storing raw HTML without sanitization):**
-
- ```typescript
- // Store raw HTML from users
- @Injectable()
- export class CommentsService {
- async create(dto: CreateCommentDto): Promise<Comment> {
- // User can inject: <script>steal(document.cookie)</script>
- return this.repo.save({
- content: dto.content, // Raw, unsanitized
- authorId: dto.authorId,
- });
- }
- }
-
- // Return HTML without sanitization
- @Controller('pages')
- export class PagesController {
- @Get(':slug')
- @Header('Content-Type', 'text/html')
- async getPage(@Param('slug') slug: string): Promise<string> {
- const page = await this.pagesService.findBySlug(slug);
- // If page.content contains user input, XSS is possible
- return `<html><body>${page.content}</body></html>`;
- }
- }
-
- // Reflect user input in errors
- @Get(':id')
- async findOne(@Param('id') id: string): Promise<User> {
- const user = await this.repo.findOne({ where: { id } });
- if (!user) {
- // XSS if id contains malicious content and error is rendered
- throw new NotFoundException(`User ${id} not found`);
- }
- return user;
- }
- ```
-
- **Correct (sanitize content and use proper headers):**
-
- ```typescript
- // Sanitize HTML content before storage
- import * as sanitizeHtml from 'sanitize-html';
-
- @Injectable()
- export class CommentsService {
- private readonly sanitizeOptions: sanitizeHtml.IOptions = {
- allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
- allowedAttributes: {
- a: ['href', 'title'],
- },
- allowedSchemes: ['http', 'https', 'mailto'],
- };
-
- async create(dto: CreateCommentDto): Promise<Comment> {
- return this.repo.save({
- content: sanitizeHtml(dto.content, this.sanitizeOptions),
- authorId: dto.authorId,
- });
- }
- }
-
- // Use validation pipe to strip HTML
- import { Transform } from 'class-transformer';
-
- export class CreatePostDto {
- @IsString()
- @MaxLength(1000)
- @Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))
- title: string;
-
- @IsString()
- @Transform(({ value }) =>
- sanitizeHtml(value, {
- allowedTags: ['p', 'br', 'b', 'i', 'a'],
- allowedAttributes: { a: ['href'] },
- }),
- )
- content: string;
- }
-
- // Set proper Content-Type headers
- @Controller('api')
- export class ApiController {
- @Get('data')
- @Header('Content-Type', 'application/json')
- async getData(): Promise<DataResponse> {
- // JSON response - browser won't execute scripts
- return this.service.getData();
- }
- }
-
- // Sanitize error messages
- @Get(':id')
- async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
- const user = await this.repo.findOne({ where: { id } });
- if (!user) {
- // UUID validation ensures safe format
- throw new NotFoundException('User not found');
- }
- return user;
- }
-
- // Use Helmet for CSP headers
- import helmet from 'helmet';
-
- async function bootstrap() {
- const app = await NestFactory.create(AppModule);
-
- app.use(
- helmet({
- contentSecurityPolicy: {
- directives: {
- defaultSrc: ["'self'"],
- scriptSrc: ["'self'"],
- styleSrc: ["'self'", "'unsafe-inline'"],
- imgSrc: ["'self'", 'data:', 'https:'],
- },
- },
- }),
- );
-
- await app.listen(3000);
- }
- ```
-
- Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
-
- ---
-
- ### 4.4 Use Guards for Authentication and Authorization
-
- **Impact: HIGH** — Enforces access control before handlers execute
-
- Guards 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.
-
- **Incorrect (manual auth checks in every handler):**
-
- ```typescript
- // Manual auth checks in every handler
- @Controller("admin")
- export class AdminController {
- @Get("users")
- async getUsers(@Request() req) {
- if (!req.user) {
- throw new UnauthorizedException();
- }
- if (!req.user.roles.includes("admin")) {
- throw new ForbiddenException();
- }
- return this.adminService.getUsers();
- }
-
- @Delete("users/:id")
- async deleteUser(@Request() req, @Param("id") id: string) {
- if (!req.user) {
- throw new UnauthorizedException();
- }
- if (!req.user.roles.includes("admin")) {
- throw new ForbiddenException();
- }
- return this.adminService.deleteUser(id);
- }
- }
- ```
-
- **Correct (guards with declarative decorators):**
-
- ```typescript
- // JWT Auth Guard
- @Injectable()
- export class JwtAuthGuard implements CanActivate {
- constructor(
- private jwtService: JwtService,
- private reflector: Reflector,
- ) {}
-
- async canActivate(context: ExecutionContext): Promise<boolean> {
- // Check for @Public() decorator
- const isPublic = this.reflector.getAllAndOverride<boolean>("isPublic", [
- context.getHandler(),
- context.getClass(),
- ]);
- if (isPublic) return true;
-
- const request = context.switchToHttp().getRequest();
- const token = this.extractToken(request);
-
- if (!token) {
- throw new UnauthorizedException("No token provided");
- }
-
- try {
- request.user = await this.jwtService.verifyAsync(token);
- return true;
- } catch {
- throw new UnauthorizedException("Invalid token");
- }
- }
-
- private extractToken(request: Request): string | undefined {
- const [type, token] = request.headers.authorization?.split(" ") ?? [];
- return type === "Bearer" ? token : undefined;
- }
- }
-
- // Roles Guard
- @Injectable()
- export class RolesGuard implements CanActivate {
- constructor(private reflector: Reflector) {}
-
- canActivate(context: ExecutionContext): boolean {
- const requiredRoles = this.reflector.getAllAndOverride<Role[]>("roles", [
- context.getHandler(),
- context.getClass(),
- ]);
-
- if (!requiredRoles) return true;
-
- const { user } = context.switchToHttp().getRequest();
- return requiredRoles.some((role) => user.roles?.includes(role));
- }
- }
-
- // Decorators
- export const Public = () => SetMetadata("isPublic", true);
- export const Roles = (...roles: Role[]) => SetMetadata("roles", roles);
-
- // Register guards globally
- @Module({
- providers: [
- { provide: APP_GUARD, useClass: JwtAuthGuard },
- { provide: APP_GUARD, useClass: RolesGuard },
- ],
- })
- export class AppModule {}
-
- // Clean controller
- @Controller("admin")
- @Roles(Role.Admin) // Applied to all routes
- export class AdminController {
- @Get("users")
- getUsers(): Promise<User[]> {
- return this.adminService.getUsers();
- }
-
- @Delete("users/:id")
- deleteUser(@Param("id") id: string): Promise<void> {
- return this.adminService.deleteUser(id);
- }
-
- @Public() // Override: no auth required
- @Get("health")
- health() {
- return { status: "ok" };
- }
- }
- ```
-
- Reference: [NestJS Guards](https://docs.nestjs.com/guards)
-
- ---
-
- ### 4.5 Validate All Input with DTOs and Pipes
-
- **Impact: HIGH** — First line of defense against attacks
-
- Always 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.
-
- **Incorrect (trust raw input without validation):**
-
- ```typescript
- // Trust raw input without validation
- @Controller("users")
- export class UsersController {
- @Post()
- create(@Body() body: any) {
- // body could contain anything - SQL injection, XSS, etc.
- return this.usersService.create(body);
- }
-
- @Get()
- findAll(@Query() query: any) {
- // query.limit could be "'; DROP TABLE users; --"
- return this.usersService.findAll(query.limit);
- }
- }
-
- // DTOs without validation decorators
- export class CreateUserDto {
- name: string; // No validation
- email: string; // Could be "not-an-email"
- age: number; // Could be "abc" or -999
- }
- ```
-
- **Correct (validated DTOs with global ValidationPipe):**
-
- ```typescript
- // Enable ValidationPipe globally in main.ts
- async function bootstrap() {
- const app = await NestFactory.create(AppModule);
-
- app.useGlobalPipes(
- new ValidationPipe({
- whitelist: true, // Strip unknown properties
- forbidNonWhitelisted: true, // Throw on unknown properties
- transform: true, // Auto-transform to DTO types
- transformOptions: {
- enableImplicitConversion: true,
- },
- }),
- );
-
- await app.listen(3000);
- }
-
- // Create well-validated DTOs
- import {
- IsString,
- IsEmail,
- IsInt,
- Min,
- Max,
- IsOptional,
- MinLength,
- MaxLength,
- Matches,
- IsNotEmpty,
- } from "class-validator";
- import { Transform, Type } from "class-transformer";
-
- export class CreateUserDto {
- @IsString()
- @IsNotEmpty()
- @MinLength(2)
- @MaxLength(100)
- @Transform(({ value }) => value?.trim())
- name: string;
-
- @IsEmail()
- @Transform(({ value }) => value?.toLowerCase().trim())
- email: string;
-
- @IsInt()
- @Min(0)
- @Max(150)
- age: number;
-
- @IsString()
- @MinLength(8)
- @MaxLength(100)
- @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
- message: "Password must contain uppercase, lowercase, and number",
- })
- password: string;
- }
-
- // Query DTO with defaults and transformation
- export class FindUsersQueryDto {
- @IsOptional()
- @IsString()
- @MaxLength(100)
- search?: string;
-
- @IsOptional()
- @Type(() => Number)
- @IsInt()
- @Min(1)
- @Max(100)
- limit: number = 20;
-
- @IsOptional()
- @Type(() => Number)
- @IsInt()
- @Min(0)
- offset: number = 0;
- }
-
- // Param validation
- export class UserIdParamDto {
- @IsUUID("4")
- id: string;
- }
-
- @Controller("users")
- export class UsersController {
- @Post()
- create(@Body() dto: CreateUserDto): Promise<User> {
- // dto is guaranteed to be valid
- return this.usersService.create(dto);
- }
-
- @Get()
- findAll(@Query() query: FindUsersQueryDto): Promise<User[]> {
- // query.limit is a number, query.search is sanitized
- return this.usersService.findAll(query);
- }
-
- @Get(":id")
- findOne(@Param() params: UserIdParamDto): Promise<User> {
- // params.id is a valid UUID
- return this.usersService.findById(params.id);
- }
- }
- ```
-
- Reference: [NestJS Validation](https://docs.nestjs.com/techniques/validation)
-
- ---
-
- ## 5. Performance
-
- **Section Impact: HIGH**
-
- ### 5.1 Use Async Lifecycle Hooks Correctly
-
- **Impact: HIGH** — Improper async handling blocks application startup
-
- NestJS 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.
-
- **Incorrect (fire-and-forget async without await):**
-
- ```typescript
- // Fire-and-forget async without await
- @Injectable()
- export class DatabaseService implements OnModuleInit {
- onModuleInit() {
- // This runs but doesn't block - app starts before DB is ready!
- this.connect();
- }
-
- private async connect() {
- await this.pool.connect();
- console.log("Database connected");
- }
- }
-
- // Heavy blocking operations in constructor
- @Injectable()
- export class ConfigService {
- private config: Config;
-
- constructor() {
- // BLOCKS entire module instantiation synchronously
- this.config = fs.readFileSync("config.json");
- }
- }
- ```
-
- **Correct (return promises from async hooks):**
-
- ```typescript
- // Return promise from async hooks
- @Injectable()
- export class DatabaseService implements OnModuleInit {
- private pool: Pool;
-
- async onModuleInit(): Promise<void> {
- // NestJS waits for this to complete before continuing
- await this.pool.connect();
- console.log("Database connected");
- }
-
- async onModuleDestroy(): Promise<void> {
- // Clean up resources on shutdown
- await this.pool.end();
- console.log("Database disconnected");
- }
- }
-
- // Use onApplicationBootstrap for cross-module dependencies
- @Injectable()
- export class CacheWarmerService implements OnApplicationBootstrap {
- constructor(
- private cache: CacheService,
- private products: ProductsService,
- ) {}
-
- async onApplicationBootstrap(): Promise<void> {
- // All modules are initialized, safe to warm cache
- const products = await this.products.findPopular();
- await this.cache.warmup(products);
- }
- }
-
- // Heavy init in async hooks, not constructor
- @Injectable()
- export class ConfigService implements OnModuleInit {
- private config: Config;
-
- constructor() {
- // Keep constructor synchronous and fast
- }
-
- async onModuleInit(): Promise<void> {
- // Async loading in lifecycle hook
- this.config = await this.loadConfig();
- }
-
- private async loadConfig(): Promise<Config> {
- const file = await fs.promises.readFile("config.json");
- return JSON.parse(file.toString());
- }
-
- get<T>(key: string): T {
- return this.config[key];
- }
- }
-
- // Enable shutdown hooks in main.ts
- async function bootstrap() {
- const app = await NestFactory.create(AppModule);
- app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling
- await app.listen(3000);
- }
- ```
-
- Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)
-
- ---
-
- ### 5.2 Use Lazy Loading for Large Modules
-
- **Impact: MEDIUM** — Improves startup time for large applications
-
- NestJS 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.
-
- **Incorrect (loading everything eagerly):**
-
- ```typescript
- // Load everything eagerly in a large app
- @Module({
- imports: [
- UsersModule,
- OrdersModule,
- PaymentsModule,
- ReportsModule, // Heavy, rarely used
- AnalyticsModule, // Heavy, rarely used
- AdminModule, // Only admins use this
- LegacyModule, // Migration module, rarely used
- BulkImportModule, // Used once a month
- ],
- })
- export class AppModule {}
-
- // All modules initialize at startup, even if never used
- // Slow cold starts in serverless
- // Memory wasted on unused modules
- ```
-
- **Correct (lazy load rarely-used modules):**
-
- ```typescript
- // Use LazyModuleLoader for optional modules
- import { LazyModuleLoader } from "@nestjs/core";
-
- @Injectable()
- export class ReportsService {
- constructor(private lazyModuleLoader: LazyModuleLoader) {}
-
- async generateReport(type: string): Promise<Report> {
- // Load module only when needed
- const { ReportsModule } = await import("./reports/reports.module");
- const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule);
-
- const reportsService = moduleRef.get(ReportsGeneratorService);
- return reportsService.generate(type);
- }
- }
-
- // Lazy load admin features with caching
- @Injectable()
- export class AdminService {
- private adminModule: ModuleRef | null = null;
-
- constructor(private lazyModuleLoader: LazyModuleLoader) {}
-
- private async getAdminModule(): Promise<ModuleRef> {
- if (!this.adminModule) {
- const { AdminModule } = await import("./admin/admin.module");
- this.adminModule = await this.lazyModuleLoader.load(() => AdminModule);
- }
- return this.adminModule;
- }
-
- async runAdminTask(task: string): Promise<void> {
- const moduleRef = await this.getAdminModule();
- const taskRunner = moduleRef.get(AdminTaskRunner);
- await taskRunner.run(task);
- }
- }
-
- // Reusable lazy loader service
- @Injectable()
- export class ModuleLoaderService {
- private loadedModules = new Map<string, ModuleRef>();
-
- constructor(private lazyModuleLoader: LazyModuleLoader) {}
-
- async load<T>(
- key: string,
- importFn: () => Promise<{ default: Type<T> } | Type<T>>,
- ): Promise<ModuleRef> {
- if (!this.loadedModules.has(key)) {
- const module = await importFn();
- const moduleType = "default" in module ? module.default : module;
- const moduleRef = await this.lazyModuleLoader.load(() => moduleType);
- this.loadedModules.set(key, moduleRef);
- }
- return this.loadedModules.get(key)!;
- }
- }
-
- // Preload modules in background after startup
- @Injectable()
- export class ModulePreloader implements OnApplicationBootstrap {
- constructor(private lazyModuleLoader: LazyModuleLoader) {}
-
- async onApplicationBootstrap(): Promise<void> {
- setTimeout(async () => {
- await this.preloadModule(() => import("./reports/reports.module"));
- }, 5000); // 5 seconds after startup
- }
-
- private async preloadModule(importFn: () => Promise<any>): Promise<void> {
- try {
- const module = await importFn();
- const moduleType = module.default || Object.values(module)[0];
- await this.lazyModuleLoader.load(() => moduleType);
- } catch (error) {
- console.warn("Failed to preload module", error);
- }
- }
- }
- ```
-
- Reference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules)
-
- ---
-
- ### 5.3 Optimize Database Queries
-
- **Impact: HIGH** — Database queries are typically the largest source of latency
-
- Select 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.
-
- **Incorrect (over-fetching data and missing indexes):**
-
- ```typescript
- // Select everything when you need few fields
- @Injectable()
- export class UsersService {
- async findAllEmails(): Promise<string[]> {
- const users = await this.repo.find();
- // Fetches ALL columns for ALL users
- return users.map((u) => u.email);
- }
-
- async getUserSummary(id: string): Promise<UserSummary> {
- const user = await this.repo.findOne({
- where: { id },
- relations: [
- "posts",
- "posts.comments",
- "posts.comments.author",
- "followers",
- ],
- });
- // Over-fetches massive relation tree
- return { name: user.name, postCount: user.posts.length };
- }
- }
-
- // No indexes on frequently queried columns
- @Entity()
- export class Order {
- @Column()
- userId: string; // No index - full table scan on every lookup
-
- @Column()
- status: string; // No index - slow status filtering
- }
- ```
-
- **Correct (select only needed data with proper indexes):**
-
- ```typescript
- // Select only needed columns
- @Injectable()
- export class UsersService {
- async findAllEmails(): Promise<string[]> {
- const users = await this.repo.find({
- select: ["email"], // Only fetch email column
- });
- return users.map((u) => u.email);
- }
-
- // Use QueryBuilder for complex selections
- async getUserSummary(id: string): Promise<UserSummary> {
- return this.repo
- .createQueryBuilder("user")
- .select("user.name", "name")
- .addSelect("COUNT(post.id)", "postCount")
- .leftJoin("user.posts", "post")
- .where("user.id = :id", { id })
- .groupBy("user.id")
- .getRawOne();
- }
-
- // Fetch relations only when needed
- async getFullProfile(id: string): Promise<User> {
- return this.repo.findOne({
- where: { id },
- relations: ["posts"], // Only immediate relation
- select: {
- id: true,
- name: true,
- email: true,
- posts: {
- id: true,
- title: true,
- },
- },
- });
- }
- }
-
- // Add indexes on frequently queried columns
- @Entity()
- @Index(["userId"])
- @Index(["status"])
- @Index(["createdAt"])
- @Index(["userId", "status"]) // Composite index for common query pattern
- export class Order {
- @PrimaryGeneratedColumn("uuid")
- id: string;
-
- @Column()
- userId: string;
-
- @Column()
- status: string;
-
- @CreateDateColumn()
- createdAt: Date;
- }
-
- // Always paginate large datasets
- @Injectable()
- export class OrdersService {
- async findAll(page = 1, limit = 20): Promise<PaginatedResult<Order>> {
- const [items, total] = await this.repo.findAndCount({
- skip: (page - 1) * limit,
- take: limit,
- order: { createdAt: "DESC" },
- });
-
- return {
- items,
- meta: {
- page,
- limit,
- total,
- totalPages: Math.ceil(total / limit),
- },
- };
- }
- }
- ```
-
- Reference: [TypeORM Query Builder](https://typeorm.io/select-query-builder)
-
- ---
-
- ### 5.4 Use Caching Strategically
-
- **Impact: HIGH** — Dramatically reduces database load and response times
-
- Implement 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.
-
- **Incorrect (no caching or caching everything):**
-
- ```typescript
- // No caching for expensive, repeated queries
- @Injectable()
- export class ProductsService {
- async getPopular(): Promise<Product[]> {
- // Runs complex aggregation query EVERY request
- return this.productsRepo
- .createQueryBuilder("p")
- .leftJoin("p.orders", "o")
- .select("p.*, COUNT(o.id) as orderCount")
- .groupBy("p.id")
- .orderBy("orderCount", "DESC")
- .limit(20)
- .getMany();
- }
- }
-
- // Cache everything without thought
- @Injectable()
- export class UsersService {
- @CacheKey("users")
- @CacheTTL(3600)
- @UseInterceptors(CacheInterceptor)
- async findAll(): Promise<User[]> {
- // Caching user list for 1 hour is wrong if data changes frequently
- return this.usersRepo.find();
- }
- }
- ```
-
- **Correct (strategic caching with proper invalidation):**
-
- ```typescript
- // Setup caching module
- @Module({
- imports: [
- CacheModule.registerAsync({
- imports: [ConfigModule],
- inject: [ConfigService],
- useFactory: (config: ConfigService) => ({
- stores: [new KeyvRedis(config.get("REDIS_URL"))],
- ttl: 60 * 1000, // Default 60s
- }),
- }),
- ],
- })
- export class AppModule {}
-
- // Manual caching for granular control
- @Injectable()
- export class ProductsService {
- constructor(
- @Inject(CACHE_MANAGER) private cache: Cache,
- private productsRepo: ProductRepository,
- ) {}
-
- async getPopular(): Promise<Product[]> {
- const cacheKey = "products:popular";
-
- // Try cache first
- const cached = await this.cache.get<Product[]>(cacheKey);
- if (cached) return cached;
-
- // Cache miss - fetch and cache
- const products = await this.fetchPopularProducts();
- await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL
- return products;
- }
-
- // Invalidate cache on changes
- async updateProduct(id: string, dto: UpdateProductDto): Promise<Product> {
- const product = await this.productsRepo.save({ id, ...dto });
- await this.cache.del("products:popular"); // Invalidate
- return product;
- }
- }
-
- // Decorator-based caching with auto-interceptor
- @Controller("categories")
- @UseInterceptors(CacheInterceptor)
- export class CategoriesController {
- @Get()
- @CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change
- findAll(): Promise<Category[]> {
- return this.categoriesService.findAll();
- }
-
- @Get(":id")
- @CacheTTL(60 * 1000) // 1 minute
- @CacheKey("category")
- findOne(@Param("id") id: string): Promise<Category> {
- return this.categoriesService.findOne(id);
- }
- }
-
- // Event-based cache invalidation
- @Injectable()
- export class CacheInvalidationService {
- constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
-
- @OnEvent("product.created")
- @OnEvent("product.updated")
- @OnEvent("product.deleted")
- async invalidateProductCaches(event: ProductEvent) {
- await Promise.all([
- this.cache.del("products:popular"),
- this.cache.del(`product:${event.productId}`),
- ]);
- }
- }
- ```
-
- Reference: [NestJS Caching](https://docs.nestjs.com/techniques/caching)
-
- ---
-
- ## 6. Testing
-
- **Section Impact: MEDIUM-HIGH**
-
- ### 6.1 Use Supertest for E2E Testing
-