Interceptors vs Middleware
⭐ Interview Importance: LOW
⏱️ Revision Time: 7 min
Middleware and Interceptors both sit in the request pipeline and can manipulate requests and responses, but they operate at very different layers of abstraction.
Overview
The easiest way to understand the difference is their position in the request lifecycle:
- Middleware is the very first thing to run when a request hits the server, and the very last thing to run before the response leaves.
- Interceptors wrap the controller method closely. They run after Middleware and Guards, but before Pipes and the actual Controller logic.
Key Differences
1. Context and Knowledge
- Middleware is “Dumb”: It only knows about the raw HTTP
RequestandResponseobjects. It does not know which NestJS Controller or Method will eventually handle the request. - Interceptors are “Smart”: They receive the
ExecutionContext, which means they know exactly which Controller (context.getClass()) and Method (context.getHandler()) are being executed. They can read custom metadata (decorators) attached to those classes.
2. Execution Paradigm
- Middleware uses Callbacks: Middleware uses the standard Express/Connect callback pattern (
next()). If you forget to callnext(), the request hangs. - Interceptors use RxJS: Interceptors use RxJS Observables (
next.handle().pipe(...)). This makes interceptors incredibly powerful for mutating the response because they can cleanly hook into the data stream emitted by the controller.
3. Response Manipulation
- Middleware is clumsy with responses: To change the response body in middleware, you have to monkey-patch the
res.sendmethod. It’s ugly and error-prone. - Interceptors are designed for responses: Interceptors receive the controller’s exact return value inside an Observable stream. You can simply use
map()to cleanly format the JSON payload before it is sent to the client.
Code Examples
When to use Middleware
Middleware is perfect for low-level, globally applicable network tasks.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Perfect for:
// 1. Parsing raw body buffers
// 2. Setting security headers (Helmet)
// 3. CORS handling
// 4. Rate limiting based on IP
console.log(`Request arrived at ${req.originalUrl}`);
next();
}
}
When to use Interceptors
Interceptors are perfect for application-level logic that depends on the route’s behavior.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Perfect for:
// 1. Standardizing JSON response shapes
// 2. Caching responses
// 3. Serializing/excluding properties from DTOs
// 4. Measuring execution time of specific methods
return next.handle().pipe(map(data => ({ data })));
}
}
Best Practices
- Use Middleware for Network Boundaries: Anything that needs to happen regardless of whether a controller route actually exists for the URL (like rejecting IPs, or parsing large file uploads) belongs in Middleware.
- Use Interceptors for Business Logic Boundaries: Anything that manipulates the data flowing in or out of your specific application features belongs in an Interceptor.