NestJS Interceptors vs. Middleware vs. Exception Filters: What’s the Difference and How They Work Together
If you’re building a NestJS application, you’ve likely encountered Interceptors, Middleware, and Exception Filters. These powerful components shape how requests and responses are handled, but their roles can be confusing. Are they interchangeable? When should you use each? This post clarifies the differences and shows how they can work together in a real-world example.
Why Understanding These Components Matters
NestJS is built on modularity and extensibility, and Interceptors, Middleware, and Exception Filters are key tools for customizing the request-response lifecycle. Whether you’re logging requests, transforming responses, or handling errors gracefully, choosing the right component ensures clean, maintainable code. Let’s break down each one.
What Are Middleware, Interceptors, and Exception Filters?
Middleware
Middleware runs early in the request-response cycle, before the route handler is invoked. It’s ideal for tasks like authentication, logging, or modifying the request/response objects. Middleware can be applied globally, to specific routes, or to entire modules.
Example Use Case: Logging incoming requests or validating API keys.
Example (from NestJS Middleware Docs):
1import { Injectable, NestMiddleware } from '@nestjs/common'; 2import { Request, Response, NextFunction } from 'express'; 3 4@Injectable() 5export class LoggerMiddleware implements NestMiddleware { 6 use(req: Request, res: Response, next: NextFunction) { 7 console.log(`Request: ${req.method} ${req.url}`); 8 next(); 9 } 10}
Apply it in a module:
1import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; 2import { LoggerMiddleware } from './logger.middleware'; 3 4@Module({}) 5export class AppModule implements NestModule { 6 configure(consumer: MiddlewareConsumer) { 7 consumer.apply(LoggerMiddleware).forRoutes('*'); 8 } 9}
Interceptors
Interceptors operate closer to the route handler, running before and/or after the handler. They can transform the handler’s input or output, add metadata, or log execution details. Interceptors are bound to specific controllers or globally via the useGlobalInterceptors
method.
Example Use Case: Transforming a response to a standardized format or measuring route execution time.
Example (from NestJS Interceptors Docs):
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2import { Observable } from 'rxjs'; 3import { map } from 'rxjs/operators'; 4 5@Injectable() 6export class TransformInterceptor implements NestInterceptor { 7 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 8 return next.handle().pipe( 9 map(data => ({ data, meta: { transformed: true } })), 10 ); 11 } 12}
Apply it to a controller:
1import { Controller, Get, UseInterceptors } from '@nestjs/common'; 2import { TransformInterceptor } from './transform.interceptor'; 3 4@Controller('users') 5@UseInterceptors(TransformInterceptor) 6export class UsersController { 7 @Get() 8 findAll() { 9 return [{ id: 1, name: 'John' }]; 10 } 11}
Exception Filters
Exception Filters catch errors thrown during request processing, allowing you to customize error responses or log errors. They’re typically used to handle HTTP exceptions (e.g., HttpException
) and return consistent error formats.
Example Use Case: Returning a standardized error response for a 404 Not Found error.
Example (from NestJS Exception Filters Docs):
1import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; 2import { Response } from 'express'; 3 4@Catch(HttpException) 5export class HttpExceptionFilter implements ExceptionFilter { 6 catch(exception: HttpException, host: ArgumentsHost) { 7 const ctx = host.switchToHttp(); 8 const response = ctx.getResponse<Response>(); 9 const status = exception.getStatus(); 10 const message = exception.message; 11 12 response.status(status).json({ 13 statusCode: status, 14 message, 15 error: 'Custom Error', 16 }); 17 } 18}
Apply it to a controller:
1import { Controller, Get, UseFilters, NotFoundException } from '@nestjs/common'; 2import { HttpExceptionFilter } from './http-exception.filter'; 3 4@Controller('users') 5@UseFilters(HttpExceptionFilter) 6export class UsersController { 7 @Get(':id') 8 findOne() { 9 throw new NotFoundException('User not found'); 10 } 11}
Comparing the Three Components
To clarify when to use each, here’s a comparison:
Feature | Middleware | Interceptor | Exception Filter |
---|---|---|---|
Execution Timing | Before route handler | Before and/or after route handler | Only when an error is thrown |
Scope | Global, module, or route-specific | Global or controller/method-specific | Global or controller/method-specific |
Primary Use | Request preprocessing (e.g., auth) | Transform input/output, add metadata | Handle errors, format error responses |
Access to Response | Full access via Express Response | Via RxJS observable (handler output) | Full access via Express Response |
Dependency Injection | Limited (no direct access to services) | Full support | Full support |
How They Work Together: A Practical Example
Let’s build a small NestJS application where Middleware logs requests, an Interceptor transforms responses, and an Exception Filter handles errors. We’ll create a UsersController
to fetch user data.
1. Setup
Create a new NestJS project:
1nest new nestjs-example 2cd nestjs-example 3npm install
2. Middleware for Logging
In src/logger.middleware.ts
:
1import { Injectable, NestMiddleware } from '@nestjs/common'; 2import { Request, Response, NextFunction } from 'express'; 3 4@Injectable() 5export class LoggerMiddleware implements NestMiddleware { 6 use(req: Request, res: Response, next: NextFunction) { 7 console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); 8 next(); 9 } 10}
Apply it globally in src/main.ts
:
1import { NestFactory } from '@nestjs/core'; 2import { AppModule } from './app.module'; 3import { LoggerMiddleware } from './logger.middleware'; 4 5async function bootstrap() { 6 const app = await NestFactory.create(AppModule); 7 app.use(LoggerMiddleware); 8 await app.listen(3000); 9} 10bootstrap();
3. Interceptor for Response Transformation
In src/transform.interceptor.ts
:
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2import { Observable } from 'rxjs'; 3import { map } from 'rxjs/operators'; 4 5@Injectable() 6export class TransformInterceptor implements NestInterceptor { 7 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 8 return next.handle().pipe( 9 map(data => ({ 10 data, 11 meta: { timestamp: new Date().toISOString(), transformed: true }, 12 })), 13 ); 14 } 15}
4. Exception Filter for Error Handling
In src/http-exception.filter.ts
:
1import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; 2import { Response } from 'express'; 3 4@Catch(HttpException) 5export class HttpExceptionFilter implements ExceptionFilter { 6 catch(exception: HttpException, host: ArgumentsHost) { 7 const ctx = host.switchToHttp(); 8 const response = ctx.getResponse<Response>(); 9 const status = exception.getStatus(); 10 const message = exception.message; 11 12 response.status(status).json({ 13 statusCode: status, 14 message, 15 error: 'API Error', 16 timestamp: new Date().toISOString(), 17 }); 18 } 19}
5. Users Controller and Service
In src/users/users.service.ts
:
1import { Injectable } from '@nestjs/common'; 2 3@Injectable() 4export class UsersService { 5 private users = [{ id: 1, name: 'John Doe' }]; 6 7 findOne(id: number) { 8 const user = this.users.find(u => u.id === id); 9 if (!user) throw new HttpException('User not found', 404); 10 return user; 11 } 12}
In src/users/users.controller.ts
:
1import { Controller, Get, Param, UseInterceptors, UseFilters } from '@nestjs/common'; 2import { UsersService } from './users.service'; 3import { TransformInterceptor } from '../transform.interceptor'; 4import { HttpExceptionFilter } from '../http-exception.filter'; 5 6@Controller('users') 7@UseInterceptors(TransformInterceptor) 8@UseFilters(HttpExceptionFilter) 9export class UsersController { 10 constructor(private readonly usersService: UsersService) {} 11 12 @Get(':id') 13 findOne(@Param('id') id: string) { 14 return this.usersService.findOne(Number(id)); 15 } 16}
In src/users/users.module.ts
:
1import { Module } from '@nestjs/common'; 2import { UsersController } from './users.controller'; 3import { UsersService } from './users.service'; 4 5@Module({ 6 controllers: [UsersController], 7 providers: [UsersService], 8}) 9export class UsersModule {}
Update src/app.module.ts
:
1import { Module } from '@nestjs/common'; 2import { UsersModule } from './users/users.module'; 3 4@Module({ 5 imports: [UsersModule], 6}) 7export class AppModule {}
6. Test the Application
Run the app:
1npm run start:dev
- Request:
GET http://localhost:3000/users/1
- Middleware: Logs
[2025-05-24T10:41:00.000Z] GET /users/1
. - Interceptor: Wraps response as
{ data: { id: 1, name: 'John Doe' }, meta: { timestamp: '2025-05-24T10:41:00.000Z', transformed: true } }
.
- Middleware: Logs
- Request:
GET http://localhost:3000/users/999
- Middleware: Logs
[2025-05-24T10:41:00.000Z] GET /users/999
. - Exception Filter: Returns
{ statusCode: 404, message: 'User not found', error: 'API Error', timestamp: '2025-05-24T10:41:00.000Z' }
.
- Middleware: Logs
Best Practices
- Use Middleware for Cross-Cutting Concerns: Apply middleware for tasks like logging or authentication that don’t depend on route logic.
- Leverage Interceptors for Response Consistency: Use interceptors to standardize API responses across endpoints.
- Centralize Error Handling with Filters: Create a global exception filter for consistent error responses.
- Avoid Overlapping Logic: Don’t duplicate functionality (e.g., logging in both middleware and interceptors).
- Test Thoroughly: Test edge cases, especially for exception filters, to ensure robust error handling.
Conclusion
Middleware, Interceptors, and Exception Filters each play distinct roles in the NestJS request-response lifecycle. Middleware preprocesses requests, Interceptors transform handler behavior, and Exception Filters handle errors gracefully. By combining them, as shown in the example, you can build robust, maintainable APIs.
Have you used these components in your NestJS projects? Share your tips or questions in the comments below!
Happy coding, and keep mastering NestJS!