Building Production-Ready APIs with NestJS: Advanced Patterns and Best Practices
Introduction
As a Full Stack Developer at Code N Code IT Solutions, I've worked extensively with various backend frameworks, and NestJS consistently stands out for building enterprise-grade applications. While getting started with NestJS is straightforward, mastering its advanced patterns can significantly improve your API's maintainability, scalability, and developer experience.
In this comprehensive guide, we'll explore advanced NestJS patterns that I use in production applications, including custom decorators, interceptors, guards, and proper error handling strategies.
Custom Decorators for Cleaner Code
Custom decorators are one of NestJS's most powerful features for reducing code duplication and improving readability. Let's create a practical example:
// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// Usage in controller
@Get('profile')
async getProfile(@CurrentUser() user: User) {
return this.userService.getProfile(user.id);
}This eliminates the need to extract user data from the request object manually in every controller method.
Advanced Interceptors for Cross-Cutting Concerns
Interceptors handle cross-cutting concerns like logging, caching, and response transformation. Here's a robust logging interceptor:
// interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const { method, url, body } = request;
const startTime = Date.now();
this.logger.log(`Incoming Request: ${method} ${url}`);
return next.handle().pipe(
tap({
next: (data) => {
const duration = Date.now() - startTime;
this.logger.log(
`Request completed: ${method} ${url} - ${duration}ms`,
);
},
error: (error) => {
const duration = Date.now() - startTime;
this.logger.error(
`Request failed: ${method} ${url} - ${duration}ms - ${error.message}`,
);
},
}),
);
}
} Sophisticated Guard Implementation
Guards determine whether a request should be handled by the route handler. Here's an advanced role-based guard:
// guards/roles.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '../enums/user-role.enum';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride(
'roles',
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new ForbiddenException('User not authenticated');
}
const hasRole = requiredRoles.some((role) => user.roles?.includes(role));
if (!hasRole) {
throw new ForbiddenException(
`Access denied. Required roles: ${requiredRoles.join(', ')}`,
);
}
return true;
}
}
// Usage with custom decorator
export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles);
// In controller
@Post('admin-only')
@Roles(UserRole.ADMIN)
@UseGuards(RolesGuard)
async adminOnlyAction() {
return { message: 'Admin access granted' };
} Global Error Handling Strategy
Implementing a global exception filter ensures consistent error responses across your API:
// filters/global-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
message = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || message;
}
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: Array.isArray(message) ? message : [message],
};
this.logger.error(
`${request.method} ${request.url}`,
JSON.stringify(errorResponse),
'GlobalExceptionFilter',
);
response.status(status).json(errorResponse);
}
} Advanced Configuration Management
Proper configuration management is crucial for production applications:
// config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
username: process.env.DATABASE_USERNAME || 'postgres',
password: process.env.DATABASE_PASSWORD || '',
database: process.env.DATABASE_NAME || 'nestjs_app',
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV === 'development',
}));
// In app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig],
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().default(5432),
}),
}),
],
})
export class AppModule {}Testing Advanced Features
Testing these advanced patterns requires specific approaches:
// tests/roles.guard.spec.ts
describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
RolesGuard,
{
provide: Reflector,
useValue: { getAllAndOverride: jest.fn() },
},
],
}).compile();
guard = module.get(RolesGuard);
reflector = module.get(Reflector);
});
it('should allow access when user has required role', () => {
const mockContext = createMockExecutionContext({
user: { roles: [UserRole.ADMIN] },
});
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.ADMIN]);
expect(guard.canActivate(mockContext)).toBe(true);
});
}); Production Deployment Considerations
When deploying NestJS applications to production, consider these best practices:
- Environment Variables: Use proper validation and default values
- Health Checks: Implement `/health` endpoints for monitoring
- Rate Limiting: Use `@nestjs/throttler` for API protection
- Documentation: Integrate Swagger for API documentation
- Performance: Enable compression and implement caching strategies
Conclusion
These advanced NestJS patterns form the foundation of robust, production-ready APIs. By implementing custom decorators, sophisticated guards, comprehensive error handling, and proper configuration management, you'll build applications that are not only functional but maintainable and scalable.
The key to mastering NestJS lies in understanding these patterns and knowing when to apply them. Start with one pattern at a time, test thoroughly, and gradually build your expertise. Your future self (and your team) will thank you for the clean, well-structured code.
Related Posts
Implementing GraphQL with NestJS: A Complete Guide for Modern API Development
Learn how to build scalable GraphQL APIs with NestJS using decorators, resolvers, and type-safe schemas for modern backend development.
Implementing JWT Authentication in Laravel 11: A Complete Guide
Master JWT authentication in Laravel 11 with practical examples, security best practices, and real-world implementation strategies.
Building Scalable Node.js APIs with NestJS: A Production-Ready Guide
Learn how to build enterprise-grade APIs with NestJS, featuring modular architecture, dependency injection, and advanced patterns.