Skip to content
NestJS ns middleware 4 min read

Middleware Basics

Middleware is the first thing a request touches in a NestJS application — it runs before guards, interceptors, pipes, and your route handler. Because Nest sits on top of Express (or Fastify), middleware uses the same battle-tested (req, res, next) contract you already know, which makes it the natural place for low-level concerns like logging, request enrichment, CORS, and body parsing. The catch is that middleware runs before Nest’s enhancer pipeline is fully in scope, so it sees the raw platform request rather than the rich ExecutionContext that guards and interceptors enjoy.

What middleware can do

A middleware function has access to the request and response objects and a next() callback. From that position it can read or mutate the request, set response headers, end the response early, or hand control to the next middleware in the chain. The golden rule is simple: if you call next(), the request continues down the pipeline; if you don’t, the request stops there and you are responsible for sending a response.

ActionHow
Continue the pipelineCall next()
Enrich the requestMutate req (e.g. attach req.user) before calling next()
Short-circuitSend a response with res and do not call next()
Pass an errorCall next(error) to invoke the platform error handler

Functional middleware

The simplest form of middleware is a plain function with the (req, res, next) signature. Use it for stateless logic that needs no dependency injection. Typing it with Request, Response, and NextFunction from express keeps everything strongly typed.

// logger.middleware.ts
import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  const start = Date.now();
  res.on('finish', () => {
    const ms = Date.now() - start;
    console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${ms}ms`);
  });
  next();
}

Hitting any route this middleware is applied to prints a line once the response is flushed:

Output:

GET /cats 200 - 4ms
POST /cats 201 - 11ms

Functional middleware cannot inject providers. If your logic needs a service — a ConfigService, a logger instance, a repository — reach for class-based middleware instead.

Class middleware with NestMiddleware

For anything that benefits from dependency injection, implement the NestMiddleware interface on an @Injectable() class. Nest instantiates the class through its DI container, so you can inject any provider exported by the surrounding module. The interface requires a single use(req, res, next) method.

// auth-context.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthContextMiddleware implements NestMiddleware {
  constructor(private readonly users: UsersService) {}

  async use(req: Request, _res: Response, next: NextFunction) {
    const token = req.headers.authorization?.replace('Bearer ', '');
    if (token) {
      // Attach the resolved user so downstream handlers can read it.
      (req as Request & { user?: unknown }).user =
        await this.users.findByToken(token);
    }
    next();
  }
}

Because the middleware is a real provider, UsersService is resolved and injected automatically — no manual wiring required. The async use method works fine: Nest awaits nothing, but next() is called once your asynchronous work completes.

Functional vs class middleware

Both forms run at the same point in the lifecycle; the difference is what they can reach and how you register them.

AspectFunctionalClass (NestMiddleware)
Dependency injectionNoYes
Signature(req, res, next) functionuse(req, res, next) method
Best forStateless, generic concernsLogic needing services or config
ReusabilityImport the function anywhereResolved per-module via DI
TestabilityCall directlyInstantiate with mocked deps

A practical rule: start with a function, and promote it to a class the moment you need a provider.

The request and response objects

Middleware receives the underlying platform objects, not Nest abstractions. With the default Express adapter you get Express’s Request/Response; under Fastify you get Fastify’s equivalents. This is why middleware is ideal for things that are inherently platform-level — cors, helmet, cookie-parser, raw-body capture — but a poor fit for anything that needs route metadata or the ExecutionContext.

// request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const id = (req.headers['x-request-id'] as string) ?? randomUUID();
    res.setHeader('x-request-id', id);
    next();
  }
}

Errors thrown synchronously inside middleware are handled by the underlying Express/Fastify error handler, not by a Nest @Catch() exception filter. Keep middleware defensive and prefer guards for logic that should produce a proper Nest HTTP exception.

Best practices

  • Reach for functional middleware by default; only use a NestMiddleware class when you genuinely need dependency injection.
  • Always either call next() or send a response — forgetting both leaves the request hanging until it times out.
  • Keep middleware focused on low-level, platform-level concerns (logging, headers, parsing); push authorization into guards and validation into pipes.
  • Type req, res, and next with the Express (or Fastify) types so mutations stay safe and discoverable.
  • Use res.on('finish', ...) rather than wrapping next() when you need timing or response-status data, since the status is only known after the handler runs.
  • Remember middleware sees the raw request only — if you need route metadata or the ExecutionContext, an interceptor or guard is the right tool.
Last updated June 14, 2026
Was this helpful?