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.
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.
Source: NestJS: Middleware
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.
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 |
|---|---|
|
Takes one or several class or functional middleware (executed in the order given) and returns a configuration object. Further |
|
Routes the middleware must not run for: strings or |
|
Where it runs: a string path, several strings, a |
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.
["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 |
|---|---|---|
|
|
|
|
everything above, and |
no exceptions: everything under |
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 |
|---|---|---|---|
|
not run |
runs |
runs |
|
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.
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:
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:
["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:
-
Middleware from
app.use()(global). -
Middleware from global modules (
@Global()). -
Middleware bound in the root module.
-
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:
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:
["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.
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.
Source: NestJS: Middleware - Error handling and Exception filters
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.
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 |
|
Arguments of |
Express |
raw |
Third-party ecosystem |
any Express middleware ( |
Fastify plugins ( |
Default body parsers |
|
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 |
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*splatand{*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
.jsin 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.
Source: Migration guide and Request lifecycle