NestJS Middleware & Interceptors
Processing Requests Before and After
Open interactive version (quiz + challenge)Real-world analogy
Middleware is like a toll booth on a highway — EVERY car passes through it before reaching the city. Interceptors are like a car wash with a before-and-after photo — they snap a picture when you drive in (before handler) and another when you drive out (after handler). Both process your car, but at different stages of the trip!
What is it?
Middleware processes requests before they reach route handlers, while interceptors wrap around handlers to process both the request and response. Together they form a powerful pipeline for logging, transformation, caching, and cross-cutting concerns without cluttering your controllers.
Real-world relevance
Every production API uses middleware and interceptors. Morgan/Winston for logging, helmet for security headers, compression for response gzip, rate-limiter for throttling — all are middleware. Response wrappers, timing metrics, and cache layers are interceptors.
Key points
- What is Middleware? — Middleware functions run BEFORE the route handler on every matching request. They have access to the request and response objects and a next() function. Use them for logging, CORS, cookie parsing, rate limiting, or authentication. They're the first checkpoint in the request pipeline.
- Creating Middleware — Implement the NestMiddleware interface with a use(req, res, next) method. Call next() to pass control to the next middleware or handler. If you don't call next(), the request hangs forever! Middleware can be class-based (injectable) or function-based (simple).
- Applying Middleware — In your module, implement NestModule and configure middleware in the configure() method. Use .forRoutes() to target specific routes and .exclude() to skip certain paths. You can apply middleware globally, per-module, or per-route.
- What are Interceptors? — Interceptors wrap AROUND the route handler using RxJS observables. They execute logic BEFORE the handler runs AND AFTER the handler returns. This makes them perfect for transforming responses, adding caching, logging timing, and more.
- Response Transformation — The most common use: wrap all responses in a standard format. `{ success: true, data: ..., timestamp: ... }` Instead of doing this in every controller, one interceptor handles all routes. Consistent API responses with zero repetition.
- Logging Interceptor — Measure how long every request takes. Log the method, URL, and response time. In production, send these metrics to monitoring services like DataDog or New Relic. One interceptor replaces hundreds of manual console.log calls.
- Caching Interceptor — NestJS has a built-in CacheInterceptor that caches GET responses. Apply it globally or per-route with @UseInterceptors(CacheInterceptor). It checks the cache before hitting the handler — if cached, it returns immediately without running your code.
- Middleware vs Interceptors — Middleware: runs BEFORE only, has access to raw req/res, good for cross-cutting concerns. Interceptors: run BEFORE and AFTER, use RxJS, good for transforming responses and timing. Use middleware for low-level stuff, interceptors for application-level logic.
Code example
// 1. Logging Middleware — runs on every request
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl } = req;
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const { statusCode } = res;
this.logger.log(
`${method} ${originalUrl} ${statusCode} — ${duration}ms`,
);
});
next(); // Don't forget this!
}
}
// Apply middleware in module
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
@Module({ controllers: [UsersController], providers: [UsersService] })
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); // Apply to all routes
// Or target specific routes:
// .forRoutes({ path: 'users', method: RequestMethod.GET })
// .exclude({ path: 'health', method: RequestMethod.GET })
}
}
// 2. Response Transform Interceptor — wraps all responses
import {
Injectable, NestInterceptor,
ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
// 3. Timing Interceptor — measure response time
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
const req = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => {
console.log(
`${req.method} ${req.url} — ${Date.now() - now}ms`,
);
}),
);
}
}
// Apply globally in main.ts
// app.useGlobalInterceptors(new TransformInterceptor());
// Or per-controller with decorator
// @UseInterceptors(TransformInterceptor)Line-by-line walkthrough
- 1. 1. Logging Middleware — runs on every request
- 2. Importing NestJS classes and Logger
- 3. Importing Express types
- 4.
- 5. Injectable class implementing NestMiddleware
- 6. Create a logger with 'HTTP' context label
- 7.
- 8. The middleware function — req, res, next
- 9. Destructure method and URL from request
- 10. Record the start time
- 11.
- 12. When the response finishes sending...
- 13. Calculate how long it took
- 14. Get the status code
- 15. Log the formatted message
- 16. Format: GET /users 200 — 15ms
- 17. Close the log call
- 18. Close the finish listener
- 19.
- 20. IMPORTANT: call next() to continue!
- 21. Close the middleware
- 22. Close the class
- 23.
- 24. Apply middleware in module
- 25. Import Module and middleware types
- 26.
- 27. Module decorator with providers
- 28. Implement NestModule interface
- 29. Configure method receives MiddlewareConsumer
- 30. Start building the middleware chain
- 31. Apply LoggerMiddleware
- 32. To all routes
- 33. Alternative: target specific routes
- 34. Or exclude certain routes
- 35. Close configure
- 36. Close class
- 37.
- 38. 2. Response Transform Interceptor
- 39. Import interceptor types
- 40.
- 41.
- 42.
- 43. Import RxJS Observable
- 44. Import the map operator
- 45.
- 46. Injectable interceptor class
- 47. Intercept method with context and next handler
- 48. Call the handler and pipe the result
- 49. Map transforms the response data
- 50. Wrap in standard format
- 51. success flag
- 52. Original data
- 53. Add timestamp
- 54. Close map
- 55. Close pipe
- 56. Close intercept
- 57. Close class
- 58.
- 59. 3. Timing Interceptor
- 60. Another injectable interceptor
- 61. Intercept method
- 62. Record start time
- 63. Get the request object
- 64.
- 65. Call handler and tap the result
- 66. tap runs side effects without changing data
- 67. Log the method, URL, and duration
- 68. Format the timing output
- 69. Close tap
- 70. Close pipe
- 71. Close intercept
- 72. Close class
- 73.
- 74. Apply globally in main.ts
- 75.
- 76. Or per-controller with decorator
Spot the bug
@Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const token = req.headers['authorization'];
if (!token) {
res.status(401).json({ message: 'Unauthorized' });
}
req['user'] = verifyToken(token);
next();
}
}Need a hint?
What happens when there's no token but the code keeps executing?
Show answer
When token is missing, res.status(401).json() sends a response BUT next() still gets called, causing the handler to run AND potentially sending a second response (crash!). Fix: add 'return' before res.status(401) to stop execution: `return res.status(401).json(...);
Explain like I'm 5
Imagine you're going to a friend's birthday party. Middleware is like the person at the door checking invitations — they see everyone who comes in. Interceptors are like gift wrapping — they wrap your present (data) nicely before you give it, and they unwrap the thank-you card (response) after. Both help, but at different times!
Fun fact
NestJS interceptors use RxJS (Reactive Extensions for JavaScript). RxJS was created by Microsoft and is the same library used by Angular. The pipe/map/tap pattern you see in interceptors is the same pattern used in Angular HTTP calls. Learning it once pays off in both frameworks!
Hands-on challenge
Create a LoggerMiddleware that logs method, URL, status code, and response time for every request. Then create a TransformInterceptor that wraps all responses in `{ success: true, data: ..., requestId: uuid() }`. Apply both globally and test them!
More resources
- NestJS Middleware (NestJS Official)
- NestJS Interceptors (NestJS Official)
- RxJS Operators Guide (RxJS Official)