Applying Middleware
Class-based middleware in NestJS isn’t wired up with a decorator the way controllers and providers are. Instead, you register it imperatively inside a module by implementing the NestModule interface and its configure() method. This gives you a fluent MiddlewareConsumer API to declare which middleware runs for which routes, methods, and even the exact order in which a chain of middleware executes. Getting this binding right is what lets you scope cross-cutting concerns precisely instead of leaking them across your whole app.
Implementing NestModule.configure
Any module can opt into middleware by implementing NestModule. The framework calls configure() once during bootstrap, passing a MiddlewareConsumer you use to build bindings.
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(LoggerMiddleware).forRoutes(UsersController);
}
}
A matching class-based middleware looks like this:
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): void {
console.log(`[${req.method}] ${req.originalUrl}`);
next();
}
}
Because the middleware is @Injectable(), it participates in dependency injection — you can inject services into its constructor just like any provider.
Middleware registered through
configure()is module-scoped binding, but the middleware instance is resolved from the DI container. You don’t list it in the module’sprovidersarray;apply()handles instantiation.
Targeting controllers, paths, and methods
forRoutes() is overloaded. You can pass a controller class, a path string, or a RouteInfo object that pins both a path and an HTTP method.
import {
Module,
NestModule,
MiddlewareConsumer,
RequestMethod,
} from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { CatsController } from './cats.controller';
@Module({ controllers: [CatsController] })
export class CatsModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(LoggerMiddleware)
// string path with a wildcard
.forRoutes({ path: 'cats/*splat', method: RequestMethod.ALL });
}
}
The three targeting styles compare like this:
Argument to forRoutes() | Matches | Use when |
|---|---|---|
CatsController | Every route declared in that controller | You want all of a controller’s endpoints covered |
'cats' (string) | The cats path for all methods | You want a path scope independent of a controller |
{ path: 'cats', method: RequestMethod.POST } | Only POST /cats | You need method-level precision |
You can also pass multiple targets in a single call, and exclude specific routes with exclude():
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
'cats/health',
)
.forRoutes(CatsController);
}
On Express 5 (NestJS 11), route wildcards use named splat syntax like
*splatrather than the bare*used in older versions. Update legacy patterns when you upgrade.
Ordering multiple middleware
Pass several middleware to a single apply() call and they execute left to right, each calling next() to hand off to the one after it.
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(CorrelationIdMiddleware, AuthMiddleware, LoggerMiddleware)
.forRoutes(UsersController);
}
Here CorrelationIdMiddleware runs first (so a request ID exists before anything logs), then AuthMiddleware, then LoggerMiddleware. If you need distinct ordering for different route sets, chain multiple apply() calls — they register in declaration order:
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(CorrelationIdMiddleware)
.forRoutes('*splat');
consumer
.apply(AuthMiddleware)
.forRoutes({ path: 'users', method: RequestMethod.ALL });
}
Given a request to GET /users, the chain produces predictable, ordered output:
Output:
[correlation] assigned id 9f3c-21ab
[auth] verifying bearer token for /users
[GET] /users
Best Practices
- Implement
NestModuleonly on the module that owns the routes a middleware should affect; this keeps scope explicit and avoids accidental global reach. - Prefer targeting a controller class over a raw path string when the binding maps cleanly to a controller — it survives path refactors.
- Use
RouteInfowithRequestMethodwhen a middleware should run for some verbs but not others (e.g. mutate-only audit logging). - Order middleware deliberately within
apply(): put context-establishing middleware (correlation IDs, request timing) before auth and logging. - Reach for
exclude()to carve out health checks, webhooks, or public endpoints instead of writing inverse path patterns. - Keep middleware
@Injectable()so you can unit test it and inject configuration or services rather than reading globals.