Middleware

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against NestJS 12.1 (npm @nestjs/core 12.1.2), Node.js 20.19+ or 22.12+, TypeScript 6, Express 5.2 and Fastify 5.12, all checked on 2026-10-02. Middleware is the first stage of the per-request pipeline: a function that runs before a route handler is selected, with access to the raw request and response and to a next() callback. The snippets use an ESM project and the Express adapter unless the Fastify adapter is named, and the outputs shown are what they produce. Where middleware sits among guards, interceptors, pipes and filters is shown in Request Lifecycle; modules are covered in Modules.

What middleware is, and where it runs

By default a Nest middleware is an Express middleware: a function (req, res, next) that can run code, change the request and response objects, end the request-response cycle, or call next() to pass control on. A middleware that neither responds nor calls next() leaves the request hanging.

flowchart LR A["Incoming request"] --> B["Global middleware
app.use()"] B --> C["Module middleware
consumer.apply()"] C --> D["Guards, interceptors,
pipes, handler"] C -. "next(err) or throw" .-> E["Exceptions layer
global filters only"] D --> F["Response"] E --> F

Because middleware runs before the router has chosen a handler, it knows nothing about the controller, the method or the ExecutionContext. That is the key difference from guards and interceptors, and the reason to prefer those when the decision depends on which handler is about to run. Typical middleware jobs are the ones that are indifferent to the handler: request identifiers, logging, CORS, helmet, cookie and body parsing, compression.

Class middleware

A class middleware is an @Injectable() class implementing the NestMiddleware interface (from @nestjs/common), with a use(req, res, next) method. Because it is a normal provider-style class, it takes part in dependency injection: its constructor can receive any provider that is available in the module that binds it.

logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import type { NextFunction, Request, Response } from 'express';
import { TraceService } from './trace.service.js';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  // Class middleware takes part in dependency injection like any provider.
  constructor(private readonly trace: TraceService) {}

  use(req: Request, res: Response, next: NextFunction): void {
    this.trace.record(`logger ${req.method} ${req.originalUrl}`);
    next();
  }
}

The Request, Response and NextFunction types come from express (a transitive dependency of @nestjs/platform-express; add @types/express if you do not have it). req.originalUrl is used because it keeps the full requested path, whereas req.url can be relative to the mount point of the middleware.

Applying middleware: NestModule.configure()

Middleware is not listed in the @Module() decorator. A module that uses middleware implements the NestModule interface and its configure(consumer: MiddlewareConsumer) method. The method may be async; Nest awaits it, which is handy when the setup depends on an asynchronous lookup.

MiddlewareConsumer method Purpose

apply(…​middleware)

Takes one or several class or functional middleware (executed in the order given) and returns a configuration object. Further apply() calls on the consumer start another binding.

exclude(…​routes)

Routes the middleware must not run for: strings or RouteInfo objects ({ path, method }), wildcards allowed.

forRoutes(…​routes)

Where it runs: a string path, several strings, a RouteInfo object, a controller class or several controller classes. This call finishes the binding, so exclude() must come before it.

app.module.ts: binding by controller, with exclusions
import {
  MiddlewareConsumer,
  Module,
  NestModule,
  RequestMethod,
} from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware.js';
import { requestId } from './functional.middleware.js';
import {
  BooksController,
  FilesController,
  HealthController,
} from './books.controller.js';
import { TraceService } from './trace.service.js';

@Module({
  controllers: [BooksController, FilesController, HealthController],
  providers: [TraceService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'books', method: RequestMethod.POST },
        'books/{*splat}',
      )
      .forRoutes(BooksController);

    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'files/abcd/{*splat}', method: RequestMethod.GET });

    consumer.apply(requestId).forRoutes('{*splat}');
  }
}

Requesting GET /books, GET /books/7, POST /books, GET /health, GET /files/abcd/xyz and GET /files/abcd in that order produces this trace from LoggerMiddleware: only GET /books (the controller’s routes minus the excluded ones) and GET /files/abcd/xyz (the wildcard binding) are logged.

Output
["logger GET /books", "logger GET /files/abcd/xyz"]

forRoutes(BooksController) binds the middleware to every route the controller declares, including its global prefix and any version segment. Binding to a controller class is the form to prefer, because it survives route renames.

Restricting by HTTP method: RouteInfo and RequestMethod

To limit a binding to one verb, pass a RouteInfo object to forRoutes() or exclude(): { path, method }, where method is a member of the RequestMethod enum (GET, POST, PUT, DELETE, PATCH, ALL, OPTIONS, HEAD, and so on). A bare string path is the same as RequestMethod.ALL.

consumer
  .apply(LoggerMiddleware)
  .forRoutes({ path: 'books', method: RequestMethod.GET });

In the first configure() above, { path: 'books', method: RequestMethod.POST } inside exclude() is what kept POST /books out of the log while GET /books stayed in it.

Named wildcards

Paths in forRoutes() and exclude() are patterns. Express 5 (and Nest 11 onward with it) uses path-to-regexp v8, which has no bare *: a wildcard must be named, written *name. The name (splat in the docs) has no special meaning; *wildcard works the same. Two forms matter:

Pattern Matches Does not match

abcd/*splat

abcd/1, abcd/123, abcd/abc: anything after abcd/

abcd/ and abcd: at least one character must follow

abcd/{*splat}

everything above, and abcd/ too, because the braces make the wildcard optional

no exceptions: everything under abcd matches

String paths treat the hyphen and the dot literally. The binding files/abcd/{*splat} in the first example shows where the boundary lies: GET /files/abcd/xyz is logged, and GET /files/abcd is not (it matches the controller route but not the middleware pattern, because the pattern requires a / after abcd).

consumer.apply(requestId).forRoutes({ path: 'abcd/*splat', method: RequestMethod.ALL });
consumer.apply(requestId).forRoutes({ path: 'abcd/{*splat}', method: RequestMethod.ALL });

For "every route" in a module on Express, use forRoutes('{*splat}'), as the first example does. The same rule as in the table applies at the root: *splat needs at least one path segment, so forRoutes('*splat') runs for GET /books and GET /books/7 but not for GET /, whereas {*splat} (the outer braces make the wildcard optional) also matches the root path. The migration guide’s Fastify example is written .forRoutes('*splat'); on Nest 12 with the Fastify adapter that form likewise skips GET /, and {*splat} matches the root there too, so {*splat} is the portable spelling for "everything". The legacy forRoutes('*') string still matches every path, including /, and the official docs still use it in one hint, but writing the named form avoids depending on that compatibility.

forRoutes(…​) (Express or Fastify adapter) GET / GET /books GET /books/7

'*splat'

not run

runs

runs

'{*splat}'

runs

runs

runs

Functional middleware

A class with no state and no dependencies is boilerplate. A plain function with the signature (req, res, next) is accepted anywhere a class is, and has no @Injectable() and no interface to implement. A function that returns a middleware function is the usual way to configure one.

functional.middleware.ts
import type { NextFunction, Request, Response } from 'express';

// Functional middleware: no class, no DI, just (req, res, next).
export function requestId(req: Request, res: Response, next: NextFunction): void {
  const id = req.header('x-request-id') ?? crypto.randomUUID();
  res.setHeader('x-request-id', id);
  next();
}

// A factory returns a configured middleware function.
export function stamp(label: string, sink: string[]) {
  return (_req: Request, _res: Response, next: NextFunction): void => {
    sink.push(label);
    next();
  };
}

Used from configure() with consumer.apply(requestId).forRoutes('{*splat}'), every response carries an x-request-id header (the incoming value, or a generated UUID). Functional middleware cannot inject providers; when you need one, write a class.

Multiple middleware

apply() takes a comma-separated list; the middleware run in that order, then the next binding runs. This is also how third-party Express middleware such as helmet or cors is attached to a subset of routes:

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

Within one module, bindings run in the order of the configure() calls. Across modules the order is described in Ordering across modules.

Global middleware and the DI caveat

To bind a middleware to every route of the application at once, call app.use() on the INestApplication in main.ts:

main.ts
const app = await NestFactory.create(AppModule);
app.use(requestId);
await app.listen(process.env.PORT ?? 3000);

app.use() hands the function straight to the underlying platform (Express or Fastify); Nest’s module system, and therefore the dependency injection container, is not involved. This is the caveat: a middleware registered with app.use() cannot receive providers in a constructor, and there is no module to resolve them from. There are two workarounds:

  • Use functional middleware with app.use(), and read configuration from the environment or the closure.

  • Keep the class middleware and bind it in a module with forRoutes('{*splat}') (or the controller classes), where DI works.

A global app.use(stamp('app.use', order)) runs before every module-level middleware:

Order with app.use() plus module middleware
["app.use", "global-module", "root-a", "root-b", "leaf"]

Two more consequences of app.use() are worth knowing. Errors raised there are handled by the platform, not by Nest’s exceptions layer, so prefer throw or next(err) from middleware bound with the consumer. And with the Express adapter Nest registers the json and urlencoded body parsers by default; to customize them through MiddlewareConsumer, create the application with bodyParser: false. With that option a POST /books with a JSON body reaches the handler with an empty body ({} comes back), because nothing parses it:

const app = await NestFactory.create<NestExpressApplication>(AppModule, {
  bodyParser: false,
});

Ordering across modules

Middleware runs sequentially in the order it is bound, exactly as in Express. For middleware bound in several modules the official lifecycle page gives the order:

  1. Middleware from app.use() (global).

  2. Middleware from global modules (@Global()).

  3. Middleware bound in the root module.

  4. Middleware from the other modules, ordered by their distance from the root module in the import graph.

Nest 11 made this order deterministic, with middleware from global modules first (module resolution switched to object-reference keys in the same release). The example registers a stamp from a @Global() module, from the root module (two middleware in one apply()) and from a leaf module that the root imports:

order.module.ts
export const order: string[] = [];

@Global()
@Module({})
export class GlobalFeatureModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(stamp('global-module', order)).forRoutes('{*splat}');
  }
}

@Module({})
export class LeafModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(stamp('leaf', order)).forRoutes('{*splat}');
  }
}

@Module({
  imports: [LeafModule, GlobalFeatureModule],
  controllers: [BooksController],
})
export class OrderModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(stamp('root-a', order), stamp('root-b', order))
      .forRoutes('{*splat}');
  }
}

Note that the root module imports LeafModule first, yet the leaf’s middleware runs last:

Order for GET /books
["global-module", "root-a", "root-b", "leaf"]

Do not depend on the relative order of two unrelated feature modules; if one middleware must precede another, bind them in the same apply() call.

Errors thrown from middleware

When middleware bound with the consumer throws, Nest’s exceptions layer handles it as it would an error from a handler. Throw an HttpException (or a built-in subclass such as UnauthorizedException); an asynchronous middleware must return its promise (declare use() as async) so a rejection is forwarded. You can also hand the error to next(), which is the right shape when wrapping Express-style code that reports failures through the callback.

auth.middleware.ts
import {
  Injectable,
  NestMiddleware,
  UnauthorizedException,
} from '@nestjs/common';
import type { NextFunction, Request, Response } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction): void {
    if (!req.headers.authorization) {
      throw new UnauthorizedException();
    }
    next();
  }
}

@Injectable()
export class AsyncAuthMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction): Promise<void> {
    await Promise.resolve(); // e.g. verify a session in a store
    if (!req.headers.authorization) {
      return next(new UnauthorizedException('Missing credentials'));
    }
    next();
  }
}

Both variants, bound with consumer.apply(…​).forRoutes(BooksController), answered an unauthenticated request with 401:

curl -i http://localhost:3000/books
{"message":"Unauthorized","statusCode":401}

and, for AsyncAuthMiddleware, {"message":"Missing credentials","error":"Unauthorized","statusCode":401}. With an Authorization header both continued to the handler and returned 200.

Only global exception filters can see these errors (app.useGlobalFilters() or an APP_FILTER provider), because no route handler, and therefore no controller or method scope, has been selected yet; @UseFilters() on a middleware class is not supported. In the module below an APP_FILTER filter that replaces the body turns the 401 from a middleware error into {"filtered":true}, while LoggerMiddleware, the first middleware in the same apply(), still runs before AuthMiddleware throws.

@Module({
  controllers: [BooksController],
  providers: [TraceService, { provide: APP_FILTER, useClass: TaggingFilter }],
})
export class FilterModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware, AuthMiddleware).forRoutes('{*splat}');
  }
}

Filters themselves are covered in Exception Filters & Error Handling.

Express versus Fastify

The consumer API is identical on both platforms; what differs is the signature of the middleware. With the Fastify adapter Nest installs @fastify/middie (already a dependency of @nestjs/platform-fastify), which runs Connect-style middleware on Fastify’s request cycle. These functions receive the raw Node.js request and response objects, not Fastify’s wrappers, so type them with FastifyRequest['raw'] and FastifyReply['raw'], and next is a plain () ⇒ void.

raw.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import type { FastifyReply, FastifyRequest } from 'fastify';

@Injectable()
export class RawLoggerMiddleware implements NestMiddleware {
  // @fastify/middie hands over the raw Node.js objects, not Fastify's wrappers.
  use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
    res.setHeader('x-raw', `${req.method} ${req.url}`);
    next();
  }
}

Bound with consumer.apply(RawLoggerMiddleware).forRoutes(BooksController) on a FastifyAdapter application, this runs for /books and /books/7. One pitfall: the raw req.url seen by the middleware is / for both, because middie strips the mount path from req.url. Do not use req.url for routing decisions in Fastify middleware; Express-specific helpers such as req.originalUrl, req.header() and res.json() are likewise not available on the raw objects.

Express adapter Fastify adapter

Engine

Express 5 middleware stack

@fastify/middie

Arguments of use()

Express Request / Response / NextFunction

raw IncomingMessage / ServerResponse, next: () ⇒ void

Third-party ecosystem

any Express middleware (helmet, cors, cookie-parser, …​)

Fastify plugins (@fastify/helmet, @fastify/cors) are the better fit; register them with app.register()

Default body parsers

json and urlencoded registered by Nest; disable with bodyParser: false

Fastify’s own content-type parsers

Middleware versus guards, interceptors and pipes

Use When Why

Middleware

the work is independent of the handler: request ids, logging, CORS, security headers, parsers

runs first, works on the raw request, can come from the Express/Fastify ecosystem

Guard

allow or deny based on the handler (roles, metadata)

has the ExecutionContext, so it can read decorators and the target class

Interceptor

code around the handler (timing, response mapping, caching)

sees both the incoming request and the response stream

Pipe

validate or transform a handler argument

runs after guards, per parameter

Authentication that merely identifies the caller (attach req.user) can live in either place, but authorization decisions belong in guards: a middleware cannot tell whether the route it is about to forward to is public.

What changed recently, and where the book is outdated

  • Express 5 wildcards (Nest 11 onward). Bare * in route patterns is gone; use *splat and {*splat} as shown above, and {*splat} (not *splat, which skips /) for "every route". Patterns copied from older articles or the book (forRoutes('abcd/*')) need updating.

  • Global-module ordering (Nest 11). Middleware from @Global() modules runs first, then the root module, then the others by distance from the root.

  • Express 5 and Fastify 5. Nest 12 targets both; the Fastify raw-object signature above is unchanged.

  • Imports end in .js in ESM projects, the current default template. Nest 12 packages are ESM-only.

  • The book (published January 2025) shows class and functional middleware with MiddlewareConsumer.apply().exclude().forRoutes() correctly, but its wildcard paths predate Express 5’s named wildcards, and it does not warn that only global filters catch middleware errors.