9 min readUpdated May 24, 2025

NestJS Interceptors vs. Middleware vs. Exception Filters: What’s the Difference and How They Work Together

Learn the key differences between Middleware, Interceptors, and Exception Filters in NestJS. This guide breaks down how each works, when to use them, and how they can be combined to build clean, maintainable APIs.

Written by

JavaScriptNext.jsBackend DevelopmentReact, Vue and Typescript

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:

FeatureMiddlewareInterceptorException Filter
Execution TimingBefore route handlerBefore and/or after route handlerOnly when an error is thrown
ScopeGlobal, module, or route-specificGlobal or controller/method-specificGlobal or controller/method-specific
Primary UseRequest preprocessing (e.g., auth)Transform input/output, add metadataHandle errors, format error responses
Access to ResponseFull access via Express ResponseVia RxJS observable (handler output)Full access via Express Response
Dependency InjectionLimited (no direct access to services)Full supportFull 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 } }.
  • 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' }.

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!

About

Software Developer & Consultant specializing in JavaScript, TypeScript, and modern web technologies.

Share this article