NestJS Middleware

Middleware is a function that runs between the incoming HTTP request and the route handler. It receives the request and response objects, performs some work — logging, validating a header, modifying the request — and then either passes control to the next function in the chain or terminates the request by sending a response.

The Middleware Pipeline

HTTP Request
     |
     v
Middleware 1 (Logger)
     |
     v
Middleware 2 (Auth token check)
     |
     v
Route Handler (Controller method)
     |
     v
HTTP Response

Each middleware calls next() to pass control forward. If a middleware does not call next(), the request stops there and no further processing happens.

Creating Middleware

Middleware is a class decorated with @Injectable() that implements the NestMiddleware interface:

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) {
    const { method, originalUrl } = req;
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      console.log(`[${method}] ${originalUrl} - ${res.statusCode} (${duration}ms)`);
    });

    next();  // must call this to continue the request
  }
}

Registering Middleware

Unlike guards and interceptors, middleware registers in the module class using the configure() method — not through a decorator:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('users');        // applies to all /users routes
  }
}

Targeting Specific Routes

// Apply to all routes in UsersController
consumer.apply(LoggerMiddleware).forRoutes(UsersController);

// Apply only to GET /users
consumer
  .apply(LoggerMiddleware)
  .forRoutes({ path: 'users', method: RequestMethod.GET });

// Apply to everything except a specific route
consumer
  .apply(AuthMiddleware)
  .exclude({ path: 'users/register', method: RequestMethod.POST })
  .forRoutes(UsersController);

Functional Middleware

For simple middleware without dependencies, a plain function works and avoids creating a class:

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request... ${req.method} ${req.url}`);
  next();
}

// Register it the same way
consumer.apply(logger).forRoutes('*');

Global Middleware

Apply middleware globally across the entire application in main.ts:

const app = await NestFactory.create(AppModule);
app.use(logger);            // function middleware
await app.listen(3000);

Global middleware applies to every route. Use it for application-wide concerns like request logging, CORS headers, or body size limits.

Middleware vs Guards

Middleware                        Guard
─────────────────────────────     ─────────────────────────────
Runs before route matching        Runs after route matching
No access to route metadata       Has access to route metadata
Can call next() or end request    Returns true (allow) or false (block)
Good for: logging, parsing        Good for: auth, permissions
Registered via configure()        Applied via @UseGuards() decorator

A logging middleware that records every request regardless of outcome is a classic middleware use case. A guard that checks whether the user is logged in before allowing access to a protected route is a classic guard use case. Both exist in the pipeline, but they operate at different points and with different capabilities.

Request Modification Example

Middleware can attach data to the request object for downstream handlers to use:

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    req['requestId'] = crypto.randomUUID();  // attach a unique ID
    next();
  }
}

// In a controller, access the attached value
@Get()
findAll(@Req() req: Request) {
  const requestId = req['requestId'];
  this.logger.log(`Handling request ${requestId}`);
  return this.usersService.findAll();
}

Middleware sits at the outermost edge of your request pipeline. It runs before routing, before guards, before interceptors, and before pipes. Use it for cross-cutting concerns that apply broadly — logging every request, setting security headers, reading and attaching session data, or enforcing rate limits at the network level before any application logic runs.

Leave a Comment

Your email address will not be published. Required fields are marked *