Middleware: Intercepting Requests
Creating custom middleware to intercept requests and modify them before they reach the controller.
NestJS Middleware: Intercepting Requests
Middleware in NestJS provides a powerful mechanism to intercept incoming requests and modify them or perform specific actions before they reach the controller logic. This is incredibly useful for tasks like authentication, logging, request validation, and more. It operates as a layer between the incoming request and the route handler.
What is Middleware?
Think of middleware as a filter or interceptor that sits in front of your route handlers (controllers). When a request comes in, it first passes through the middleware pipeline. Middleware can:
- Modify the request object: Add headers, transform data, etc.
- Modify the response object: Add headers, set cookies, etc.
- Terminate the request cycle: Return an error response, redirect the user, etc.
- Perform side effects: Log the request, update a counter, etc.
Middleware Lifecycle
The middleware lifecycle is straightforward:
- Request Received: A request arrives at the NestJS application.
- Middleware Execution: The configured middleware(s) are executed in the order they are registered.
- Controller Handling: If no middleware terminates the request, it's passed to the appropriate controller.
- Response Sent: The controller processes the request and sends a response back to the client. Middleware can also intercept the outgoing response.
Creating Custom Middleware in NestJS
Here's how to create custom middleware in NestJS:
- Create a Middleware Class: Implement the
NestMiddleware
interface from@nestjs/common
. This interface requires you to implement theuse
method. - Implement the
use
Method: This method is where your middleware logic resides. It receives therequest
,response
, andnext
function as arguments. Thenext()
function is crucial; you must call it to pass the request to the next middleware in the chain or, ultimately, to the controller. Failing to callnext()
will effectively hang the request. - Apply the Middleware: Middleware can be applied globally, to specific modules, or to specific routes. This is done within the module using the
configure
method of the module class (which implements theNestModule
interface).
Example: A Simple Logging Middleware
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) {
console.log('Request...');
console.log('URL:', req.url);
console.log('Method:', req.method);
// Important: Call next() to pass the request to the next handler
next();
}
}
This example logs the URL and HTTP method of each incoming request. The @Injectable()
decorator allows NestJS to manage the middleware's dependencies (if any).
Applying Middleware in a Module
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LoggerMiddleware } from './logger.middleware';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); // Apply to all routes
// or
// .forRoutes({ path: 'cats', method: RequestMethod.GET }); // Only to GET requests to /cats
//You can also exclude routes
// .exclude(
// { path: 'cats', method: RequestMethod.GET },
// 'cats/(.*)',
// )
// .forRoutes(CatsController);
}
}
In this example, we import the LoggerMiddleware
and apply it to all routes ('*'
) using the forRoutes()
method. You can also specify specific routes or route prefixes. MiddlewareConsumer
allows fine-grained control over which routes receive which middleware.
Accessing Request and Response Objects
Within the use
method, you have direct access to the request
(req
) and response
(res
) objects. These are the standard Express.js request and response objects, giving you full control over the incoming request and the outgoing response.
Example: Modifying Request Headers
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class AddHeaderMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
req.headers['x-custom-header'] = 'This is a custom header added by middleware';
next();
}
}
Example: Setting a Response Header
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class SetContentTypeMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
res.setHeader('Content-Type', 'application/json');
next();
}
}
Middleware vs. Interceptors vs. Guards
It's important to understand the difference between middleware, interceptors, and guards in NestJS:
- Middleware: Operates at the HTTP request level. It can modify the request/response objects and perform actions *before* the request reaches the route handler.
- Guards: Primarily used for authorization. They determine whether a user has permission to access a specific route *before* the request reaches the route handler. They return a boolean value (true for access granted, false for access denied).
- Interceptors: Can transform the request/response, augment the functionality, or override behavior, either *before* or *after* the route handler is called. They have access to both the request and the response stream and can be used for caching, logging, error handling, etc.
Choosing the right tool depends on the specific task. For pre-processing requests (like logging, parsing headers), middleware is often a good choice. For authorization, guards are the primary mechanism. For transforming data or handling exceptions across multiple routes, interceptors are generally the preferred approach.