Skip to content

Middleware in NestJS

Aman Kumar Singh7 min read
Part 10 of 40From the NestJS Production Guide series
Middleware in NestJS — article by Aman Kumar Singh

The previous article covered exception filters: the stage at the end of the request pipeline that catches whatever went wrong and shapes it into a consistent error response. This one goes back to the front of that pipeline, to middleware, the earliest point where your code touches an incoming request. It's part of the NestJS Production Guide series.

Middleware is the plainest piece of the Nest request lifecycle, and that's exactly why it gets misused. It has no idea about guards. It has no idea about the route handler's metadata, and no access to the execution context that interceptors and guards get. It just sees a raw request and response, the same as Express or Fastify middleware always has. That simplicity makes it the right tool for a narrow set of jobs and the wrong tool for almost everything else in the pipeline.

This article covers what middleware actually is in Nest, how to apply it with real control over ordering and scope, and where the line sits between "this belongs in middleware" and "this belongs in a guard or interceptor instead."

What middleware actually is

Nest middleware is functionally identical to Express middleware: a function that receives the request, the response, and a next callback, and either calls next() to continue or ends the response itself. Nest wraps this in two forms, a plain function or a class implementing NestMiddleware, but the underlying mechanics are the HTTP adapter's, not Nest's own.

// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const existing = req.headers['x-request-id'];
    const requestId = typeof existing === 'string' ? existing : randomUUID();

    req['requestId'] = requestId;
    res.setHeader('x-request-id', requestId);
    next();
  }
}

That distinction matters more than it looks like on the surface. Because middleware runs before Nest builds the execution context for the request, it has no visibility into which controller or handler is about to run, what decorators are attached to it, or what guards and interceptors will apply. It only sees the raw HTTP objects. If your logic needs to know "which route is this" or "what roles does this user need", middleware is the wrong layer; that's what guards and interceptors, running later with full context, are for.

The class-based form exists mainly for dependency injection. If your middleware needs a service, say, a request-scoped tenant resolver backed by a database lookup, the class form lets you inject it the same way you would into a controller or provider. The functional form is fine for anything stateless.

Applying middleware with real control

Nest deliberately does not let you attach middleware with a decorator the way you attach guards or interceptors. Instead, it goes through the module's configure method, which gives you explicit control over which routes it applies to and in what order.

// src/app.module.ts
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { RequestIdMiddleware } from './common/middleware/request-id.middleware';
import { RequestLoggerMiddleware } from './common/middleware/request-logger.middleware';
import { OrdersModule } from './modules/orders/orders.module';

@Module({
  imports: [OrdersModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(RequestIdMiddleware, RequestLoggerMiddleware)
      .exclude({ path: 'health', method: RequestMethod.GET })
      .forRoutes('*');
  }
}

The order you pass middleware to .apply() is the order it runs in, which matters here: RequestIdMiddleware needs to run before RequestLoggerMiddleware so the logger can include the request ID it just generated. This is one of the few places in Nest where ordering isn't implicit or inferred from decorators; you're writing it out directly, which is worth treating as documentation in its own right.

forRoutes() accepts a controller class, a specific path and method, or a wildcard, and .exclude() lets you carve out exceptions like health check endpoints that shouldn't carry the same overhead as everything else. Scoping middleware to specific modules rather than applying it globally in AppModule keeps that logic close to the feature it's protecting, which pays off the first time you need to remove or change it for just one part of the app.

You can also apply middleware globally in main.ts with app.use(), bypassing Nest's module system entirely. That's the right call for genuinely framework-agnostic middleware, like helmet or compression, that has no reason to know it's running inside Nest at all. Reach for MiddlewareConsumer when you want Nest's routing and exclusion rules; reach for app.use() when you're wiring in something that predates Nest and doesn't care about it.

Where middleware fits, and where it doesn't

The honest way to decide whether something belongs in middleware is to ask what information it needs. Middleware is the right layer for anything that operates purely on the raw HTTP request or response and doesn't care which controller is about to handle it: request ID generation, raw request/response logging, CORS headers, body size limits, and low-level security headers all fit that description.

Authentication is the case that trips people up most often. It's tempting to put a JWT check in middleware because that's the Express habit, and it does work mechanically; middleware can read a header and reject a request before it goes further. But Nest gives authentication a more specific home in guards, which run inside the execution context and have access to route metadata through Reflector. That's what lets a guard distinguish @Public() routes from protected ones, or check role requirements attached via a custom decorator, none of which middleware can see. Put authentication in middleware and you lose that flexibility, or end up reimplementing route-awareness by hand.

Response shaping and timing are the other common misfire. Middleware only sees the response object before the handler runs; it doesn't wrap the handler call itself, so it can't cleanly measure how long the handler took or transform what it returned. Interceptors, which run around the handler on both sides, are built for exactly that. If you find yourself monkey-patching res.end inside a middleware function to capture a response body or timing, that's usually the sign you actually want an interceptor.

Production pitfalls

The most common mistake is treating middleware as a catch-all for anything that "needs to run early." Every additional middleware function executes on every matching request, in order, before the request touches your business logic, and each one adds latency even if it's small. A stack of five middleware functions that each do a little bit of unrelated work is harder to reason about, and harder to profile, than three functions with clear, distinct responsibilities.

A second pitfall is forgetting that middleware runs before validation and before guards, which means anything it touches on the request object hasn't been sanitized or authenticated yet. Middleware that trusts a header blindly, say, reading a tenant ID straight from x-tenant-id without any check, is a real security gap if that header ever reaches production without a guard or gateway enforcing where it can come from. Reading a header in middleware is fine. Trusting it without a guard downstream confirming the request is legitimate is not.

The third is applying middleware globally in AppModule by default instead of scoping it to the routes that actually need it. A logging middleware that's genuinely useful on API routes might be pure overhead on a health check endpoint or a static asset route, and excluding those explicitly with .exclude() costs one line versus debugging later why health checks are slower than expected.

// src/common/middleware/request-logger.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger(RequestLoggerMiddleware.name);

  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now();

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

    next();
  }
}

Listening on the response's finish event, rather than trying to wrap res.end, is the reliable way to capture timing and status from middleware without interfering with how the response actually gets sent. It's a small detail, but it's the kind of thing that's easy to get subtly wrong the first time you write logging middleware by hand.

Key takeaways

  • Middleware is the raw HTTP layer of the Nest pipeline: it runs before Nest builds the execution context, so it has no visibility into route metadata, decorators, or which handler is about to run.
  • Apply middleware through a module's `configure` method and `MiddlewareConsumer` when you need Nest's routing, ordering, and `.exclude()` control; use `app.use()` in `main.ts` for framework-agnostic middleware that doesn't need to know it's running inside Nest.
  • The order middleware is applied in is the order it executes in. That ordering is explicit, not inferred, and it matters whenever one piece of middleware depends on another (a logger reading a request ID a prior middleware generated, for example).
  • Authentication, authorization, and anything that needs route metadata belongs in guards, not middleware; response timing and transformation belong in interceptors, which wrap the handler on both sides in a way middleware structurally cannot.
  • Scope middleware to the routes that need it instead of applying everything globally by default; unnecessary middleware on every request is a cost paid quietly on every single request, including the ones that didn't need it.

Frequently asked questions

What's the difference between NestJS middleware and Express middleware?

Functionally, almost nothing. Nest middleware runs on the same underlying HTTP adapter (Express or Fastify) and receives the same request, response, and `next` arguments. The difference is how you register it: Nest gives you `MiddlewareConsumer` inside a module's `configure` method, with route matching and exclusion built in, instead of calling `app.use()` for everything.

Can middleware access NestJS's dependency injection container?

Yes, if you write it as a class implementing `NestMiddleware` and register it through a module rather than as a bare function. That's the main reason to reach for the class form: it lets you inject services the same way a controller or provider would, which a plain middleware function cannot do on its own.

Should authentication logic live in middleware or in a guard?

In a guard. Middleware runs before Nest has built the execution context for the request, so it can't see route-level metadata like `@Roles()` or `@Public()` decorators. Guards run later, with full access to that context, which is what lets them make route-aware authorization decisions middleware structurally cannot.

How do I control the order multiple middleware functions run in?

The order you list them in `.apply(MiddlewareA, MiddlewareB)` is the order they execute in for a given request. This is explicit rather than inferred from decorators, so when one middleware function depends on state set by another, write that dependency out clearly, ideally in a code comment, since nothing else in the framework enforces it for you.

Is it fine to apply middleware globally to every route?

It's fine when the middleware genuinely applies everywhere, like a request ID generator. It's a real cost when it doesn't, since every additional middleware function runs on every matching request before the handler does. Scope middleware to specific modules or exclude routes like health checks explicitly rather than defaulting everything to global.

Why doesn't NestJS let me apply middleware with a decorator like guards and interceptors?

Middleware operates purely on the raw HTTP layer, before Nest's execution context exists, so there's no handler metadata yet for a decorator to attach to or read. Configuring it through a module's `configure` method keeps that distinction visible: it's a reminder that middleware is doing something structurally earlier and less Nest-aware than guards or interceptors.

Related articles

Security Best Practices — Aman Kumar Singh
API Documentation with Swagger — Aman Kumar Singh
Rate Limiting and Throttling — Aman Kumar Singh
Aman Kumar Singh

About the author

I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.