Interceptor Concept
Interceptors are classes decorated with @Injectable() that implement the NestInterceptor interface. Interceptors have a range of useful capabilities:
- Bind extra logic before/after method execution
- Transform the result returned from a function
- Transform the exception thrown from a function
- Extend basic function behavior
- Completely override a function depending on specific conditions (e.g., for caching purposes)
Basic Interceptor Structure
typescriptimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class TransformInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( map(data => ({ data, timestamp: new Date().toISOString() })) ); } }
Using Interceptors
Use Interceptors on Controllers
typescript@Controller('cats') @UseInterceptors(TransformInterceptor) export class CatsController {}
Use Interceptors on Methods
typescript@Controller('cats') export class CatsController { @Get() @UseInterceptors(TransformInterceptor) findAll() { return this.catsService.findAll(); } }
Global Interceptors
typescriptimport { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { TransformInterceptor } from './common/interceptors/transform.interceptor'; @Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: TransformInterceptor, }, ], }) export class AppModule {}
Common Interceptor Use Cases
1. Logging Interceptor
typescriptimport { 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<any> { const request = context.switchToHttp().getRequest(); const { method, url, body } = request; const now = Date.now(); this.logger.log(`Incoming request: ${method} ${url}`); return next.handle().pipe( tap(() => { const response = context.switchToHttp().getResponse(); const { statusCode } = response; const delay = Date.now() - now; this.logger.log(`Outgoing response: ${statusCode} - ${delay}ms`); }), ); } }
2. Cache Interceptor
typescriptimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, of } from 'rxjs'; @Injectable() export class CacheInterceptor implements NestInterceptor { private cache = new Map(); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const cacheKey = request.url; if (this.cache.has(cacheKey)) { return of(this.cache.get(cacheKey)); } return next.handle().pipe( tap(response => { this.cache.set(cacheKey, response); }), ); } }
3. Timeout Interceptor
typescriptimport { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common'; import { Observable, throwError, TimeoutError } from 'rxjs'; import { catchError, timeout } from 'rxjs/operators'; @Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( timeout(5000), catchError(err => { if (err instanceof TimeoutError) { return throwError(() => new RequestTimeoutException()); } return throwError(() => err); }), ); } }
4. Response Transform Interceptor
typescriptimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; export interface Response<T> { success: boolean; data: T; message: string; timestamp: string; } @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> { return next.handle().pipe( map(data => ({ success: true, data, message: 'Success', timestamp: new Date().toISOString(), })), ); } }
Exception Filter Concept
Exception filters are classes decorated with @Catch() that are used to catch and handle exceptions thrown in the application. They allow you to have complete control over the exception handling flow, including response format, status codes, etc.
Basic Exception Filter Structure
typescriptimport { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: exception.message, }); } }
Using Exception Filters
Use Exception Filters on Methods
typescript@Post() @UseFilters(HttpExceptionFilter) create(@Body() createCatDto: CreateCatDto) { return this.catsService.create(createCatDto); }
Use Exception Filters on Controllers
typescript@Controller('cats') @UseFilters(HttpExceptionFilter) export class CatsController {}
Global Exception Filters
typescriptimport { Module } from '@nestjs/common'; import { APP_FILTER } from '@nestjs/core'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; @Module({ providers: [ { provide: APP_FILTER, useClass: HttpExceptionFilter, }, ], }) export class AppModule {}
Common Exception Filters
1. Global Exception Filter
typescriptimport { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; import { Request, Response } from 'express'; @Catch() export class AllExceptionsFilter implements ExceptionFilter { 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'; response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message, }); } }
2. Custom Exceptions
typescriptimport { HttpException, HttpStatus } from '@nestjs/common'; export class BusinessException extends HttpException { constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) { super( { statusCode: status, message, error: 'Business Error', }, status, ); } } // Use custom exception @Get() findAll() { if (someCondition) { throw new BusinessException('Invalid operation'); } return this.catsService.findAll(); }
3. Validation Exception Filter
typescriptimport { ExceptionFilter, Catch, ArgumentsHost, BadRequestException } from '@nestjs/common'; import { ValidationError } from 'class-validator'; @Catch(BadRequestException) export class ValidationExceptionFilter implements ExceptionFilter { catch(exception: BadRequestException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); const exceptionResponse = exception.getResponse(); let errors = []; if (typeof exceptionResponse === 'object' && 'message' in exceptionResponse) { errors = (exceptionResponse as any).message; } response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: 'Validation failed', errors, }); } }
Execution Order
The execution order of various processors in NestJS is as follows:
- Middleware
- Guards
- Interceptors before
- Pipes
- Controller method
- Interceptors after
- Exception filters
Best Practices
Interceptor Best Practices
- Single Responsibility: Each interceptor should only handle one functionality
- Performance Considerations: Avoid time-consuming operations in interceptors
- Error Handling: Handle errors appropriately in interceptors
- Test Coverage: Write tests for interceptors
- Documentation: Add clear documentation to interceptors
Exception Filter Best Practices
- Unified Response Format: Use a unified exception response format
- Logging: Log errors in exception filters
- Sensitive Information: Avoid exposing sensitive information in responses
- Custom Exceptions: Create custom exception classes to represent business errors
- Global Filters: Use global exception filters to handle uncaught exceptions
Summary
The NestJS interceptor and exception filter system provides:
- Powerful request/response handling capabilities
- Flexible exception handling mechanisms
- Clear separation of concerns
- Easy to extend and customize
- Complete request lifecycle control
Mastering interceptors and exception filters is key to building robust, maintainable NestJS applications. Interceptors handle cross-cutting concerns of requests/responses, and exception filters handle errors uniformly. Together they form the error handling and response transformation foundation of the application.