On this page
Interceptors, middleware, and exception filters
Three cross-cutting concerns
NestJS provides three mechanisms for logic that applies across many routes without being tied to any specific controller:
| Mechanism | When it runs | Access to response |
|---|---|---|
| Middleware | Before guards and interceptors | Partial (via Express/Fastify) |
| Interceptors | Before AND after route handler | Full (wraps the handler) |
| Exception filters | When an exception is thrown | Full (constructs the response) |
Understanding when to use each is key to keeping your controllers clean.
Middleware
Middleware in NestJS is equivalent to Express middleware — a function that receives the request, response, and next function. It runs before guards, interceptors, and route handlers.
Creating middleware
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
req['id'] = crypto.randomUUID();
res.setHeader('X-Request-Id', req['id'] as string);
next();
}
}Applying middleware
Middleware is applied in the module's configure method:
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(RequestIdMiddleware)
.forRoutes('*'); // All routes
consumer
.apply(AuthMiddleware)
.exclude({ path: 'auth/(.*)', method: RequestMethod.ALL })
.forRoutes(BooksController); // Specific controller
}
}Functional middleware
Simple middleware can be written as a plain function:
export function corsMiddleware(req: Request, res: Response, next: NextFunction): void {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}Interceptors
Interceptors are one of NestJS's most powerful features. They wrap the route handler execution and can:
- Add logic before the handler runs
- Add logic after the handler runs and modify the response
- Transform the value returned by the handler
- Transform exceptions thrown by the handler
- Override the handler entirely (caching)
The NestInterceptor interface
Every interceptor implements intercept(context, next):
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
// Before the handler
console.log('Before handler...');
return next.handle().pipe(
// After the handler (response transformation)
map((data) => ({ success: true, data })),
);
}next.handle() returns an RxJS Observable. Everything piped after .handle() transforms the response.
Caching interceptor
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private readonly cache = new Map<string, { data: unknown; expiresAt: number }>();
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<Request>();
const key = request.url;
const cached = this.cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return of(cached.data); // Return cached response immediately
}
return next.handle().pipe(
tap((data) => {
this.cache.set(key, { data, expiresAt: Date.now() + 60_000 });
}),
);
}
}Applying interceptors
// Method level
@Get()
@UseInterceptors(CacheInterceptor)
findAll() { ... }
// Controller level
@Controller('books')
@UseInterceptors(LoggingInterceptor)
export class BooksController { ... }
// Global level (in main.ts or via APP_INTERCEPTOR in AppModule)
app.useGlobalInterceptors(new LoggingInterceptor());Exception filters
When an exception is thrown anywhere in the request pipeline, NestJS looks for an exception filter that handles it. The default behavior is the built-in HttpException handling, which sends a JSON response with the status code and message.
Creating a custom exception filter
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
catch(exception: HttpException, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}Catch-all exception filter
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
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;
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}Custom exceptions
Extend HttpException for domain-specific errors:
export class BookNotAvailableException extends HttpException {
constructor(bookId: number) {
super(
{ message: `Book #${bookId} is not available for checkout`, bookId },
HttpStatus.CONFLICT,
);
}
}Rate limiting middleware
Install and configure @nestjs/throttler for rate limiting:
npm install @nestjs/throttler// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([
{ ttl: 60000, limit: 100 }, // 100 requests per minute
]),
],
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
})
export class AppModule {}
// Override per controller/method
@Throttle({ default: { ttl: 60000, limit: 10 } })
@Post('login')
login() { ... }
@SkipThrottle()
@Get('health')
healthCheck() { ... }import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Request } from 'express';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<Request>();
const { method, url } = request;
const start = Date.now();
return next.handle().pipe(
tap({
next: () => {
const duration = Date.now() - start;
this.logger.log(`${method} ${url} → 200 [${duration}ms]`);
},
error: (err: unknown) => {
const duration = Date.now() - start;
const status = (err as { status?: number }).status ?? 500;
this.logger.warn(`${method} ${url} → ${status} [${duration}ms]`);
},
}),
);
}
}
Sign in to track your progress