Express-style functions called beforeguards, interceptors, pipes, and the route handler. They receive raw req/res objects and either call next() or end the response.
Signature
import { Injectable, NestMiddleware } from "@nestjs/common"import { NextFunction, Request, Response } from "express"@Injectable()export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction): void { next() }}
The use() method can be sync, return a Promise, or end the response. Never silently return without calling next() or ending res: the request hangs until the client times out.
Generate with the CLI
nest generate middleware logger # full formnest g mi logger # short alias → src/logger/logger.middleware.tsnest g mi logger --flat # no wrapping folder → src/logger.middleware.tsnest g mi http/request-id # nested path → src/http/request-id/request-id.middleware.tsnest g mi http/request-id --flat # nested + flat → src/http/request-id.middleware.tsnest g mi logger --no-spec # skip the *.spec.ts test filenest g mi logger --dry-run # preview the file plan, write nothing
Same shape as the other lifecycle generators (gu, pi, in). The CLI defaults to wrapping the file in a folder named after the element; --flat drops it directly in the target. Source: Nest CLI usages.
Functional middleware
Class middleware can inject providers from the same module. If the middleware needs no dependencies, a plain function is shorter and the official docs prefer it for that case:
import { NextFunction, Request, Response } from "express"export function logger(req: Request, res: Response, next: NextFunction): void { next()}
Functional middleware is bound the same way as class middleware: pass the function reference to consumer.apply(...) or app.use(...).
Middleware runs first in the request lifecycle and sees only raw HTTP. It has no ExecutionContext: it cannot read decorator metadata, the controller class, or the handler reference. That makes it the right tool for cross-cutting HTTP concerns (helmet, compression, request IDs) and the wrong tool for anything that depends on which handler will run.
Need
Use
Mutate raw req/res for every (matching) route
Middleware
Decide “should this handler run?” based on roles/permissions
Functional middleware, no DI needed. Read the inbound x-request-id if the caller sent one, otherwise mint a new one. Echoed back on the response so callers can correlate.
// request-id.middleware.tsimport { randomUUID } from "node:crypto"import { NextFunction, Request, Response } from "express"export function requestId(req: Request, res: Response, next: NextFunction): void { const id = (req.headers["x-request-id"] as string) ?? randomUUID() req.headers["x-request-id"] = id res.setHeader("x-request-id", id) next()}
// main.tsimport { NestFactory } from "@nestjs/core"import { AppModule } from "./app.module"import { requestId } from "./request-id.middleware"async function bootstrap() { const app = await NestFactory.create(AppModule) app.use(requestId) await app.listen(3000)}bootstrap()
For a request-scoped logger that picks this up via AsyncLocalStorage, see the trace-id recipe.
Access log middleware (status, URL, duration)
Apache/nginx-style access logs belong in middleware: the line GET /cats 200 4ms describes what the HTTP layer did. Middleware sees every request, including 404s, requests rejected by guards, and requests that blew up in pipes — an interceptor can’t log those because the handler never ran.
Rule of thumb:
Question
Where it belongs
”What did the HTTP layer do?” (method, URL, status, bytes)
Middleware (access log)
“What did my code do?” (which handler ran, what it returned, business events)
Production apps usually have both. The class form is shown here so DI is available when you need it later (e.g. constructor(private config: ConfigService) {}); the snippet itself doesn’t inject anything yet.
// app.module.tsimport { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"import { HttpLoggerMiddleware } from "./logger.middleware"@Module({})export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer): void { consumer.apply(HttpLoggerMiddleware).forRoutes("*") }}
Listening on res.on('finish', ...) is the canonical way to time the full request: it fires after interceptors, pipes, the handler, and the outgoing response have all completed.
Body parsers: raw vs json
A body parser is a piece of middleware that reads the request stream once and stores the result on req.body. The choice of parser decides what shape your handler sees. Nest’s Express adapter auto-registers express.json() and express.urlencoded(); the others you bind yourself.
Parser
Matches Content-Type
req.body becomes
When to use
express.json()
application/json
Parsed JS object ({ amount: 1 })
99% of REST endpoints. Auto-on under Nest.
express.urlencoded()
application/x-www-form-urlencoded
Object from form fields
HTML form posts. Auto-on under Nest.
express.raw()
Any (configurable, e.g. application/json)
Buffer of the original bytes
Webhooks where a third party signs the byte-for-byte payload (Stripe, GitHub), or binary uploads
express.text()
text/plain (configurable)
UTF-8 string
Plain-text payloads, XML you’ll parse yourself
The request body is a one-shot stream: once a parser has consumed it, no other parser can. That’s why mixing them on overlapping paths breaks: whichever runs first wins, and downstream code sees req.body already in that shape (or an empty {} if the type didn’t match).
Why raw matters for signed webhooks: signature verification recomputes an HMAC over the exact bytes the sender hashed. JSON.stringify(req.body) is not guaranteed to reproduce those bytes (key order, whitespace, unicode escapes can all differ), so a re-serialized body will fail the check. express.raw() keeps the original Buffer so you can verify, then JSON.parse(req.body.toString()) yourself.
Capture the raw body for a Stripe webhook
Stripe signs the raw request body. The default express.json() parser replaces it with a parsed object before your handler runs. Disable Nest’s built-in body parser, then route raw bodies to the webhook path only:
// main.tsimport { json, raw } from "express"import { NestFactory } from "@nestjs/core"import { AppModule } from "./app.module"async function bootstrap() { const app = await NestFactory.create(AppModule, { bodyParser: false }) app.use("/webhooks/stripe", raw({ type: "application/json" })) app.use(json()) // re-enable JSON parsing for everything else await app.listen(3000)}bootstrap()
An alternative is NestFactory.create(AppModule, { rawBody: true }) plus @Req() req: RawBodyRequest<Request> and req.rawBody. See Raw body.
Mount third-party Express middleware globally
Helmet and compression illustrate the standard pattern: install, import, app.use() in main.ts. They have no Nest-specific wrapper.
// main.tsimport compression from "compression"import helmet from "helmet"import { NestFactory } from "@nestjs/core"import { AppModule } from "./app.module"async function bootstrap() { const app = await NestFactory.create(AppModule) app.use(helmet()) app.use(compression()) app.enableCors({ origin: "https://example.com" }) await app.listen(3000)}bootstrap()
CORS uses app.enableCors(...) instead of app.use(cors()) so the adapter can install it before any other middleware runs.
When to reach for it
Mutate the raw request before Nest reaches guards, pipes, or controllers.
Attach correlation IDs, request IDs, or low-level logging context.
Plug in a third-party Express middleware (helmet, compression, cookie-parser, …).
Apply cross-cutting HTTP behavior that is not tied to handler metadata.
When not to
Authorization: use a guard. Guards read route metadata and decide whether the handler should run.
DTO checks or param coercion: use a pipe. Pipes run with argument metadata.
Response mapping, caching, or timing around the handler: use an interceptor.
Catching exceptions and shaping the error response: use an exception filter.
Gotchas
app.use() loses DI
Global middleware bound through app.use() cannot resolve providers from the Nest container. Bind class middleware via MiddlewareConsumer.apply(...).forRoutes(...) when the middleware needs injected services. Unlike pipes/guards/interceptors, there is no APP_MIDDLEWARE token, so the DI-aware-globals shortcut doesn’t apply here.
Default body parsers run before custom middleware
With the Express adapter, Nest registers express.json() and express.urlencoded() automatically. To customize parsing (raw bodies for webhooks, multipart, custom limits), pass { bodyParser: false } to NestFactory.create() first, then bind your parser. See Raw body.
Middleware does not run on microservices or WebSocket gateways
app.use() and MiddlewareConsumer are HTTP-only. Microservice transports and WebSocket gateways have no middleware concept; use guards, interceptors, or pipes there. In a hybrid app the HTTP middleware chain still applies to the HTTP side only.
No ExecutionContext in middleware
Middleware sees raw HTTP objects, not ExecutionContext. If the logic needs handler metadata, the controller class, or the handler reference, it belongs in a guard or interceptor.
Fastify adapter changes the req/res types
Under @nestjs/platform-fastify, the parameters are FastifyRequest and FastifyReply (and done instead of next for Fastify hooks). Class middleware via MiddlewareConsumer still works, but the request/response shape and any third-party middleware you plug in must be Fastify-compatible.
Common errors
Symptom
Likely cause
Request hangs
Middleware did not call next() and did not end the response
Injected provider is undefined
Bound through app.use() instead of MiddlewareConsumer
Custom body parser ignored
Forgot { bodyParser: false } on NestFactory.create()
Middleware runs on excluded route
exclude() placed after forRoutes() (the chain closes on forRoutes)
Cannot read handler metadata
Wrong layer: use a guard or interceptor, middleware has no ExecutionContext
Middleware never fires on a WS gateway
Middleware is HTTP-only. Move the logic to a guard or interceptor
Stripe webhook signature fails
express.json() consumed the body before your handler. Use raw() on the webhook path