How to Use Middleware in NestJS Applications: Class-Based and Functional Patterns
NestJS middleware runs before route handlers and is configured through the MiddlewareConsumer interface inside a module's configure() method, with the framework orchestrating registration and resolution via MiddlewareModule in packages/core/middleware/middleware-module.ts.
Middleware in NestJS provides a type-safe, testable mechanism to process incoming requests before they reach your controllers. According to the nestjs/nest source code, the framework implements a sophisticated execution pipeline that integrates with the dependency injection container, global exception filters, and request-scoped lifecycles. Learning how to use middleware in NestJS applications enables you to handle cross-cutting concerns like logging, authentication, and request transformation through a clean, declarative API.
Understanding the NestJS Middleware Architecture
The middleware system centers on the MiddlewareModule (packages/core/middleware/middleware-module.ts). When your application bootstraps, MiddlewareModule.register() executes a four-step process:
- Collects all middleware configurations declared by modules via their
configure()methods. - Resolves concrete middleware instances using the
MiddlewareResolver. - Creates a proxy that wraps your
use()method with Nest’s exception-filter handling viaRouterProxy. - Registers the resulting handler with the underlying HTTP adapter (Express, Fastify, etc.) for specific routes and methods.
The public API you interact with is the MiddlewareConsumer interface (packages/common/interfaces/middleware/middleware-consumer.interface.ts), implemented by the MiddlewareBuilder (packages/core/middleware/builder.ts). This builder exposes a fluent API where apply() returns a ConfigProxy that stores your middleware list, exclude() optionally removes routes from scope, and forRoutes() finalizes the configuration into a MiddlewareConfiguration object consumed by MiddlewareModule.
Creating Class-Based Middleware
Class-based middleware implements the NestMiddleware interface and must be decorated with @Injectable() to participate in the dependency injection system.
// src/common/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log(`Incoming ${req.method} ${req.url}`);
next();
}
}
The use method receives the raw request and response objects from the underlying HTTP server. You must invoke next() to pass control to the next middleware in the chain or to the route handler. This pattern mirrors the Express middleware signature while providing full access to NestJS’s DI container for injecting services.
Registering Middleware with MiddlewareConsumer
To activate middleware, your module must implement the NestModule interface and define a configure() method. This is where you interact with the MiddlewareConsumer to bind middleware to specific routes.
// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
@Module({
controllers: [CatsController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats', '/dogs');
}
}
When configure() executes, the MiddlewareBuilder creates a MiddlewareConfiguration object that MiddlewareModule later consumes during the bootstrap phase. The apply() method accepts either a single middleware class or an array, while forRoutes() accepts strings, route wildcards, or controller classes. Internally, ConfigProxy deduplicates overlapping routes to prevent double registration.
Using Functional Middleware
NestJS also supports plain functional middleware for simple use cases that do not require dependency injection.
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
function requestId(req, res, next) {
req.id = Date.now();
next();
}
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(requestId).forRoutes('cats');
}
}
Both class-based and functional approaches are valid. The builder flattens nested arrays internally (apply(...middleware) calls middleware.flat()), allowing you to mix and match styles within a single apply() call.
Advanced Configuration: Exclusions and Request Scoping
Excluding Specific Routes
Use the exclude() method to prevent middleware from running on specific paths or HTTP methods.
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats/public', method: RequestMethod.GET },
'health'
)
.forRoutes('cats');
The exclude method accepts either string routes or RouteInfo objects containing both path and method. Internally, ConfigProxy.exclude expands wildcards and stores them in excludedRoutes, which the middleware pipeline filters before final registration.
Request-Scoped Middleware
For scenarios requiring a fresh instance per request (such as request-specific state or user context), declare middleware with request scope.
import { Injectable, NestMiddleware, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestScopeMiddleware implements NestMiddleware {
use(req, res, next) {
console.log('Request-scoped instance');
next();
}
}
When a request arrives, MiddlewareModule.bindHandler generates a unique context ID via ContextIdFactory and loads a fresh instance through injector.loadPerContext. This ensures that request-scoped providers injected into your middleware receive the correct request-scoped instances.
Summary
- MiddlewareModule (
packages/core/middleware/middleware-module.ts) orchestrates middleware registration during application bootstrap by collecting configurations, resolving instances, and binding them to the HTTP server. - Configure middleware within a module’s
configure()method using the MiddlewareConsumer fluent API:apply().forRoutes(). - NestJS supports both class-based middleware (implementing
NestMiddlewarewith@Injectable()) and functional middleware (plain functions). - Use
exclude()to filter routes by path or HTTP method before registration. - Request-scoped middleware creates fresh instances per request via
Scope.REQUEST, integrated with Nest’s context ID factory and dependency injection system.
Frequently Asked Questions
What is the difference between class-based and functional middleware in NestJS?
Class-based middleware implements the NestMiddleware interface and can leverage NestJS’s dependency injection container to inject services, while functional middleware consists of plain JavaScript functions that receive (req, res, next) but cannot access the DI container. Both are accepted by MiddlewareConsumer.apply(), and the builder internally flattens nested arrays to handle mixed declarations.
How does NestJS determine middleware execution order?
NestJS executes middleware in the order they are registered within each module’s configure() method, following the module import sequence. The MiddlewareModule processes configurations sequentially during bootstrap, registering handlers with the underlying HTTP adapter (Express or Fastify) in that same order, creating a deterministic pipeline where earlier registrations run first.
Can NestJS middleware access the dependency injection container?
Yes, but only class-based middleware decorated with @Injectable() can access the DI container. When MiddlewareModule resolves middleware instances via MiddlewareResolver, it instantiates class-based middleware through the NestJS injector, enabling constructor injection of providers. Functional middleware operates outside the container and cannot inject dependencies.
How do I exclude specific HTTP methods from middleware processing?
Use the exclude() method on the MiddlewareConsumer chain, passing a RouteInfo object that specifies both the path and method: consumer.apply(Middleware).exclude({ path: 'route', method: RequestMethod.GET }).forRoutes('route'). Internally, ConfigProxy.exclude stores these exclusions in excludedRoutes, which the middleware pipeline filters during the registration phase to prevent binding on matching routes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →