Most times when we think of NestJS, we picture decorators, dependency injection, and clean architecture. But beneath the surface lies a treasure trove of advanced features that can transform how you build scalable backend applications. After years of building enterprise-grade NestJS applications, I've discovered patterns and techniques that rarely make it into tutorials but are essential for senior-level development.
Custom Metadata Reflection: Building Intelligent Decorators
Standard decorators are limited in their ability to store and retrieve complex configuration data. You need a way to attach sophisticated metadata to classes and methods that can be accessed at runtime.
// metadata.constants.ts
export const CACHE_CONFIG_METADATA = Symbol('cache-config');
export const RATE_LIMIT_METADATA = Symbol('rate-limit');
// cache-config.decorator.ts
import { SetMetadata } from '@nestjs/common';
export interface CacheConfig {
ttl: number;
key?: string;
condition?: (args: any[]) => boolean;
tags?: string[];
}
export const CacheConfig = (config: CacheConfig) =>
SetMetadata(CACHE_CONFIG_METADATA, config);
// Advanced usage with conditional caching
@CacheConfig({
ttl: 300,
key: 'user-profile-{{userId}}',
condition: (args) => args[0]?.cacheEnabled !== false,
tags: ['user', 'profile']
})
async getUserProfile(userId: string, options?: GetUserOptions) {
// method implementation
}
Smart Interceptor Using Metadata
// cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class SmartCacheInterceptor implements NestInterceptor {
constructor(
private reflector: Reflector,
private cacheService: CacheService
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const cacheConfig = this.reflector.get<CacheConfig>(
CACHE_CONFIG_METADATA,
context.getHandler()
);
if (!cacheConfig) {
return next.handle();
}
const request = context.switchToHttp().getRequest();
const args = context.getArgs();
// Check condition
if (cacheConfig.condition && !cacheConfig.condition(args)) {
return next.handle();
}
// Generate cache key with template replacement
const cacheKey = this.generateCacheKey(cacheConfig.key, request, args);
// Try to get from cache
const cachedResult = this.cacheService.get(cacheKey);
if (cachedResult) {
return of(cachedResult);
}
// Execute and cache result
return next.handle().pipe(
tap(result => {
this.cacheService.set(cacheKey, result, {
ttl: cacheConfig.ttl,
tags: cacheConfig.tags
});
})
);
}
private generateCacheKey(template: string, request: any, args: any[]): string {
let key = template;
// Replace template variables
key = key.replace(/\{\{(\w+)\}\}/g, (match, prop) => {
return request.params?.[prop] ||
request.query?.[prop] ||
args[0]?.[prop] ||
match;
});
return key;
}
}
Advanced Execution Context Manipulation
The ExecutionContext
is more powerful than you realize. Here's how to leverage it for sophisticated request handling.
// advanced-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export interface AuthContext {
user?: any;
permissions?: string[];
rateLimit?: {
remaining: number;
resetTime: number;
};
}
@Injectable()
export class AdvancedAuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private authService: AuthService,
private rateLimitService: RateLimitService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
// Get handler and class metadata
const handler = context.getHandler();
const controllerClass = context.getClass();
// Check if route is public
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
handler,
controllerClass,
]);
if (isPublic) {
return true;
}
// Extract and validate token
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('Token not found');
}
try {
// Validate token and get user
const user = await this.authService.validateToken(token);
// Check rate limiting
const rateLimitKey = `rate_limit:${user.id}:${handler.name}`;
const rateLimit = await this.rateLimitService.checkLimit(rateLimitKey);
if (!rateLimit.allowed) {
response.set('X-RateLimit-Remaining', '0');
response.set('X-RateLimit-Reset', rateLimit.resetTime.toString());
throw new UnauthorizedException('Rate limit exceeded');
}
// Create enhanced auth context
const authContext: AuthContext = {
user,
permissions: user.permissions,
rateLimit: {
remaining: rateLimit.remaining,
resetTime: rateLimit.resetTime
}
};
// Attach to request for use in downstream handlers
request.authContext = authContext;
// Set rate limit headers
response.set('X-RateLimit-Remaining', rateLimit.remaining.toString());
response.set('X-RateLimit-Reset', rateLimit.resetTime.toString());
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Dynamic Module Factory Patterns
Creating modules that adapt their behavior based on runtime conditions is a powerful pattern for building flexible applications.
// feature-flag.interface.ts
export interface FeatureFlagConfig {
provider: 'redis' | 'database' | 'memory';
refreshInterval?: number;
defaultFlags?: Record<string, boolean>;
remoteConfig?: {
url: string;
apiKey: string;
};
}
// feature-flag.module.ts
import { DynamicModule, Module, Provider } from '@nestjs/common';
import { FeatureFlagService } from './feature-flag.service';
@Module({})
export class FeatureFlagModule {
static forRoot(config: FeatureFlagConfig): DynamicModule {
const providers: Provider[] = [
{
provide: 'FEATURE_FLAG_CONFIG',
useValue: config,
},
];
// Conditionally add providers based on configuration
switch (config.provider) {
case 'redis':
providers.push({
provide: 'FEATURE_FLAG_STORAGE',
useClass: RedisFeatureFlagStorage,
});
break;
case 'database':
providers.push({
provide: 'FEATURE_FLAG_STORAGE',
useClass: DatabaseFeatureFlagStorage,
});
break;
default:
providers.push({
provide: 'FEATURE_FLAG_STORAGE',
useClass: MemoryFeatureFlagStorage,
});
}
// Add remote config provider if configured
if (config.remoteConfig) {
providers.push({
provide: 'REMOTE_CONFIG_CLIENT',
useFactory: () => new RemoteConfigClient(config.remoteConfig),
});
}
providers.push(FeatureFlagService);
return {
module: FeatureFlagModule,
providers,
exports: [FeatureFlagService],
global: true,
};
}
static forRootAsync(options: {
useFactory: (...args: any[]) => Promise<FeatureFlagConfig> | FeatureFlagConfig;
inject?: any[];
}): DynamicModule {
return {
module: FeatureFlagModule,
providers: [
{
provide: 'FEATURE_FLAG_CONFIG',
useFactory: options.useFactory,
inject: options.inject || [],
},
{
provide: 'FEATURE_FLAG_STORAGE',
useFactory: async (config: FeatureFlagConfig) => {
switch (config.provider) {
case 'redis':
return new RedisFeatureFlagStorage();
case 'database':
return new DatabaseFeatureFlagStorage();
default:
return new MemoryFeatureFlagStorage();
}
},
inject: ['FEATURE_FLAG_CONFIG'],
},
FeatureFlagService,
],
exports: [FeatureFlagService],
global: true,
};
}
}
// Usage in AppModule
@Module({
imports: [
FeatureFlagModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
provider: configService.get('FEATURE_FLAG_PROVIDER') as 'redis' | 'database',
refreshInterval: configService.get('FEATURE_FLAG_REFRESH_INTERVAL', 30000),
remoteConfig: configService.get('FEATURE_FLAG_REMOTE_URL') ? {
url: configService.get('FEATURE_FLAG_REMOTE_URL'),
apiKey: configService.get('FEATURE_FLAG_API_KEY'),
} : undefined,
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
Request Scope Memory Management
Understanding REQUEST scope deeply is crucial for preventing memory leaks in high-traffic applications.
// user-context.service.ts
import { Injectable, Scope, OnModuleDestroy } from '@nestjs/common';
import { EventEmitter } from 'events';
@Injectable({ scope: Scope.REQUEST })
export class UserContextService implements OnModuleDestroy {
private readonly eventEmitter = new EventEmitter();
private readonly subscriptions: (() => void)[] = [];
private userData: Map<string, any> = new Map();
constructor() {
// Set max listeners to prevent memory leak warnings
this.eventEmitter.setMaxListeners(100);
}
setUserData(key: string, value: any): void {
this.userData.set(key, value);
this.eventEmitter.emit('userDataChanged', { key, value });
}
getUserData(key: string): any {
return this.userData.get(key);
}
onUserDataChange(callback: (data: { key: string; value: any }) => void): void {
this.eventEmitter.on('userDataChanged', callback);
// Store cleanup function
const cleanup = () => this.eventEmitter.off('userDataChanged', callback);
this.subscriptions.push(cleanup);
}
// Critical: Clean up resources when request ends
onModuleDestroy(): void {
// Remove all event listeners
this.subscriptions.forEach(cleanup => cleanup());
this.eventEmitter.removeAllListeners();
// Clear data
this.userData.clear();
console.log('UserContextService destroyed for request');
}
}
// Usage with proper cleanup
@Injectable()
export class UserService {
constructor(private userContext: UserContextService) {}
async processUser(userId: string): Promise<void> {
// This will be automatically cleaned up when request ends
this.userContext.onUserDataChange((data) => {
console.log(`User data changed: ${data.key} = ${data.value}`);
});
this.userContext.setUserData('userId', userId);
this.userContext.setUserData('lastActivity', new Date());
}
}
Advanced Exception Filter Chaining
Create sophisticated error handling with hierarchical exception filters.
// base-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
export interface ErrorContext {
correlationId: string;
userId?: string;
endpoint: string;
userAgent?: string;
ip: string;
}
@Catch()
export abstract class BaseExceptionFilter implements ExceptionFilter {
protected readonly logger = new Logger(this.constructor.name);
abstract canHandle(exception: any): boolean;
abstract handleException(exception: any, host: ArgumentsHost): void;
catch(exception: any, host: ArgumentsHost): void {
if (this.canHandle(exception)) {
this.handleException(exception, host);
} else {
// Pass to next filter in chain
this.delegateToNext(exception, host);
}
}
protected delegateToNext(exception: any, host: ArgumentsHost): void {
// This would be handled by the next filter in the chain
// or the default NestJS exception handler
throw exception;
}
protected createErrorContext(host: ArgumentsHost): ErrorContext {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
return {
correlationId: request.headers['x-correlation-id'] as string ||
Math.random().toString(36).substring(7),
userId: (request as any).authContext?.user?.id,
endpoint: `${request.method} ${request.url}`,
userAgent: request.headers['user-agent'],
ip: request.ip,
};
}
}
// validation-exception.filter.ts
@Catch(ValidationException)
export class ValidationExceptionFilter extends BaseExceptionFilter {
canHandle(exception: any): boolean {
return exception instanceof ValidationException;
}
handleException(exception: ValidationException, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const errorContext = this.createErrorContext(host);
this.logger.warn('Validation error', {
...errorContext,
errors: exception.getErrors(),
});
response.status(400).json({
statusCode: 400,
message: 'Validation failed',
errors: exception.getErrors(),
correlationId: errorContext.correlationId,
timestamp: new Date().toISOString(),
});
}
}
// business-exception.filter.ts
@Catch(BusinessException)
export class BusinessExceptionFilter extends BaseExceptionFilter {
canHandle(exception: any): boolean {
return exception instanceof BusinessException;
}
handleException(exception: BusinessException, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const errorContext = this.createErrorContext(host);
this.logger.error('Business logic error', {
...errorContext,
errorCode: exception.getErrorCode(),
message: exception.message,
});
response.status(422).json({
statusCode: 422,
message: exception.message,
errorCode: exception.getErrorCode(),
correlationId: errorContext.correlationId,
timestamp: new Date().toISOString(),
});
}
}
// global-exception.filter.ts
@Catch()
export class GlobalExceptionFilter extends BaseExceptionFilter {
canHandle(exception: any): boolean {
return true; // Global filter handles everything
}
handleException(exception: any, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const errorContext = this.createErrorContext(host);
// Handle different types of exceptions
if (exception instanceof HttpException) {
this.handleHttpException(exception, response, errorContext);
} else {
this.handleUnknownException(exception, response, errorContext);
}
}
private handleHttpException(
exception: HttpException,
response: Response,
context: ErrorContext
): void {
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
this.logger.error('HTTP Exception', {
...context,
status,
response: exceptionResponse,
});
response.status(status).json({
statusCode: status,
message: exception.message,
correlationId: context.correlationId,
timestamp: new Date().toISOString(),
});
}
private handleUnknownException(
exception: any,
response: Response,
context: ErrorContext
): void {
this.logger.error('Unhandled Exception', {
...context,
error: exception.message,
stack: exception.stack,
});
response.status(500).json({
statusCode: 500,
message: 'Internal server error',
correlationId: context.correlationId,
timestamp: new Date().toISOString(),
});
}
}
// Register filters in correct order
// main.ts
app.useGlobalFilters(
new ValidationExceptionFilter(),
new BusinessExceptionFilter(),
new GlobalExceptionFilter(), // This should be last
);
Advanced Health Check Orchestration
Build sophisticated health monitoring that goes beyond simple HTTP checks.
// health-check.service.ts
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
@Injectable()
export class AdvancedHealthIndicator extends HealthIndicator {
constructor(
private readonly databaseService: DatabaseService,
private readonly redisService: RedisService,
private readonly externalApiService: ExternalApiService,
) {
super();
}
async checkDatabase(key: string): Promise<HealthIndicatorResult> {
try {
const startTime = Date.now();
await this.databaseService.executeQuery('SELECT 1');
const responseTime = Date.now() - startTime;
const isHealthy = responseTime < 1000; // 1 second threshold
const result = this.getStatus(key, isHealthy, {
responseTime: `${responseTime}ms`,
threshold: '1000ms',
timestamp: new Date().toISOString(),
});
if (!isHealthy) {
throw new HealthCheckError('Database response time too high', result);
}
return result;
} catch (error) {
throw new HealthCheckError('Database connection failed', {
[key]: {
status: 'down',
error: error.message,
timestamp: new Date().toISOString(),
},
});
}
}
async checkRedis(key: string): Promise<HealthIndicatorResult> {
try {
const startTime = Date.now();
await this.redisService.ping();
const responseTime = Date.now() - startTime;
const result = this.getStatus(key, true, {
responseTime: `${responseTime}ms`,
timestamp: new Date().toISOString(),
});
return result;
} catch (error) {
throw new HealthCheckError('Redis connection failed', {
[key]: {
status: 'down',
error: error.message,
timestamp: new Date().toISOString(),
},
});
}
}
async checkExternalDependencies(key: string): Promise<HealthIndicatorResult> {
const checks = await Promise.allSettled([
this.checkExternalApi('payment-gateway', 'https://api.payment.com/health'),
this.checkExternalApi('notification-service', 'https://api.notifications.com/health'),
this.checkExternalApi('analytics-service', 'https://api.analytics.com/health'),
]);
const results = checks.map((check, index) => ({
name: ['payment-gateway', 'notification-service', 'analytics-service'][index],
status: check.status === 'fulfilled' ? 'up' : 'down',
details: check.status === 'fulfilled' ? check.value : check.reason,
}));
const failedServices = results.filter(r => r.status === 'down');
const isHealthy = failedServices.length === 0;
const result = this.getStatus(key, isHealthy, {
services: results,
failedCount: failedServices.length,
totalCount: results.length,
timestamp: new Date().toISOString(),
});
if (!isHealthy) {
throw new HealthCheckError('External dependencies failing', result);
}
return result;
}
private async checkExternalApi(name: string, url: string): Promise<any> {
const startTime = Date.now();
const response = await fetch(url, {
method: 'GET',
timeout: 5000 // 5 second timeout
});
const responseTime = Date.now() - startTime;
return {
name,
status: response.ok ? 'up' : 'down',
responseTime: `${responseTime}ms`,
statusCode: response.status,
};
}
}
// health.controller.ts
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private advancedHealth: AdvancedHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.advancedHealth.checkDatabase('database'),
() => this.advancedHealth.checkRedis('redis'),
]);
}
@Get('detailed')
@HealthCheck()
detailedCheck() {
return this.health.check([
() => this.advancedHealth.checkDatabase('database'),
() => this.advancedHealth.checkRedis('redis'),
() => this.advancedHealth.checkExternalDependencies('external-services'),
]);
}
@Get('readiness')
@HealthCheck()
readinessCheck() {
// More strict checks for readiness
return this.health.check([
() => this.advancedHealth.checkDatabase('database'),
() => this.advancedHealth.checkRedis('redis'),
() => this.advancedHealth.checkExternalDependencies('external-services'),
]);
}
@Get('liveness')
@HealthCheck()
livenessCheck() {
// Basic checks for liveness (pod restart criteria)
return this.health.check([
() => this.advancedHealth.checkDatabase('database'),
]);
}
}
Provider Overriding in Tests: Surgical Test Isolation
Advanced testing patterns that provide precise control over dependencies.
// user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let app: TestingModule;
let mockUserRepository: jest.Mocked<UserRepository>;
let mockEventEmitter: jest.Mocked<EventEmitter2>;
let mockCacheService: jest.Mocked<CacheService>;
beforeEach(async () => {
// Create sophisticated mocks
mockUserRepository = {
save: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
} as any;
mockEventEmitter = {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
} as any;
mockCacheService = {
get: jest.fn(),
set: jest.fn(),
del: jest.fn(),
reset: jest.fn(),
} as any;
app = await Test.createTestingModule({
imports: [
// Import actual modules but override specific providers
DatabaseModule,
CacheModule,
EventEmitterModule.forRoot(),
],
providers: [
UserService,
NotificationService,
],
})
// Override specific providers surgically
.overrideProvider(getRepositoryToken(User))
.useValue(mockUserRepository)
.overrideProvider(EventEmitter2)
.useValue(mockEventEmitter)
.overrideProvider(CACHE_TOKENS.SESSION_CACHE)
.useValue(mockCacheService)
// Override guards for testing without authentication
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
// Override interceptors to disable caching during tests
.overrideInterceptor(CacheInterceptor)
.useValue({ intercept: (context, next) => next.handle() })
.compile();
service = app.get<UserService>(UserService);
});
describe('createUser', () => {
it('should create user and emit event', async () => {
// Arrange
const userData = { email: 'test@example.com', name: 'Test User' };
const createdUser = { id: '1', ...userData };
mockUserRepository.save.mockResolvedValue(createdUser);
// Act
const result = await service.createUser(userData);
// Assert
expect(mockUserRepository.save).toHaveBeenCalledWith(userData);
expect(mockEventEmitter.emit).toHaveBeenCalledWith('user.created', {
userId: '1',
email: 'test@example.com',
preferences: undefined
});
expect(result).toEqual(createdUser);
});
it('should handle cache failure gracefully', async () => {
// Arrange
const userData = { email: 'test@example.com', name: 'Test User' };
const createdUser = { id: '1', ...userData };
mockUserRepository.save.mockResolvedValue(createdUser);
mockCacheService.set.mockRejectedValue(new Error('Cache unavailable'));
// Act & Assert - should not throw
const result = await service.createUser(userData);
expect(result).toEqual(createdUser);
});
});
// Test with different provider overrides per test
describe('with different cache configuration', () => {
beforeEach(async () => {
// Override with different cache implementation
await app.close();
app = await Test.createTestingModule({
imports: [UserModule],
})
.overrideProvider(CACHE_TOKENS.SESSION_CACHE)
.useFactory({
factory: () => new MemoryCacheService({ maxSize: 10 }),
})
.compile();
service = app.get<UserService>(UserService);
});
it('should work with memory cache', async () => {
// Test implementation with actual memory cache
});
});
afterEach(async () => {
await app.close();
});
});
The key insight is that NestJS provides the primitives, but senior engineers know how to compose them into powerful, maintainable systems. These patterns have been battle-tested in production environments handling millions of requests.
Master these techniques, and you'll find yourself building more robust, scalable, and maintainable backend applications that can handle enterprise-level complexity with ease.
Let's Connect
Top comments (0)