This content originally appeared on DEV Community and was authored by Boyinbode Ebenezer Ayomide
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
This content originally appeared on DEV Community and was authored by Boyinbode Ebenezer Ayomide