Foundations Updated 2026-09 View as Markdown

Middleware and Lifecycle

The layer beneath Nest's abstractions, and the module hooks that decide whether your redeploys leak database connections.

Middleware runs before everything Nest-specific — before guards, before interceptors, before the router has matched a handler. It is the raw Express or Fastify layer.

src/common/request-id.middleware.ts
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const id = (req.headers['x-request-id'] as string) ?? randomUUID();
    (req as any).id = id;
    res.setHeader('x-request-id', id);
    next();                     // forget this and the request hangs forever
  }
}
src/app.module.ts
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(RequestIdMiddleware)
      .forRoutes('*');

    consumer
      .apply(RawBodyMiddleware)
      .forRoutes({ path: 'webhooks/*', method: RequestMethod.POST });

    consumer
      .apply(AuditMiddleware)
      .exclude('health', 'metrics')
      .forRoutes(OrdersController);
  }
}

Functional middleware works too and is simpler when you need no dependencies:

typescript
export function requestId(req: Request, res: Response, next: NextFunction) {
  (req as any).id = randomUUID();
  next();
}

The full pipeline#

Worth memorising, because putting logic in the wrong layer is a common source of confusion:

incoming request
  → middleware          (raw req/res; no route metadata yet)
  → guards              (may this proceed? sees decorators)
  → interceptors        (before)
  → pipes               (validate and transform arguments)
  → HANDLER
  → interceptors        (after)
  → exception filters   (on throw, at any stage)

The practical consequences:

  • Middleware cannot read route decorators. It runs before routing, so @Roles() and @Public() are invisible to it. Anything decorator-driven must be a guard.
  • Guards cannot see the validated body. Pipes run after guards, so a guard sees the raw payload.
  • Only interceptors and filters see the outcome.

Where middleware is genuinely right#

Three cases, and they are all "this needs the raw request":

Request context, set once and read everywhere without threading it through:

typescript
@Injectable()
export class ContextMiddleware implements NestMiddleware {
  use(req: Request, _res: Response, next: NextFunction) {
    requestContext.run(
      { requestId: (req as any).id, tenantId: (req as any).user?.tenantId },
      () => next(),
    );
  }
}

AsyncLocalStorage here rather than a request-scoped provider — scope is contagious and expensive.

Raw body for signed webhooks:

typescript
consumer.apply(express.raw({ type: 'application/json' }))
        .forRoutes({ path: 'webhooks/stripe', method: RequestMethod.POST });

Stripe and most webhook providers sign the raw bytes. If a JSON parser runs first, the body is re-serialised and the signature no longer verifies.

Third-party Express middlewarehelmet, compression, cookie-parser. These are written for Express and slot in unchanged.

src/main.ts
app.use(helmet());
app.use(compression());

Module lifecycle hooks#

Nest calls these as modules are initialised and destroyed:

typescript
@Injectable()
export class PrismaService extends PrismaClient
  implements OnModuleInit, OnModuleDestroy {

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
HookWhen
onModuleInitonce the host module's dependencies are resolved
onApplicationBootstraponce every module has initialised
onModuleDestroywhen a termination signal is received
beforeApplicationShutdownafter all onModuleDestroy complete
onApplicationShutdownlast, with the signal

Nest awaits async hooks, which is why they are the right place for setup that a constructor cannot do:

typescript
@Injectable()
export class ReportService {
  constructor(private readonly prisma: PrismaService) {
    this.warmCache();          // WRONG — constructors cannot be async;
  }                            // this floats an unhandled promise
}

@Injectable()
export class ReportService implements OnModuleInit {
  async onModuleInit() {
    await this.warmCache();    // right — Nest waits for this
  }
}

The one line people forget#

src/main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();          // <- this one
  await app.listen(3000);
}

Without it, onModuleDestroy never runs. Database connections are not closed, in-flight requests are cut off mid-response, and every redeploy leaks connections until the pool is exhausted.

The symptom is nasty because it is gradual: the application works fine, then degrades over days as deploys accumulate, and nothing in the logs points at the cause. Two words in main.ts prevent it.

Graceful shutdown, properly#

typescript
@Injectable()
export class QueueConsumer implements OnApplicationShutdown {
  private draining = false;

  async onApplicationShutdown(signal?: string) {
    this.draining = true;                    // stop accepting new work
    await this.waitForInFlight(30_000);      // let current jobs finish
  }
}

Combined with a readiness probe that fails as soon as draining is true, this is what lets a rolling deploy happen with no dropped requests.

Exercise#

typescript
// 1. Write a middleware that starts an AsyncLocalStorage context holding
//    a requestId and the start time, applied to all routes except /health.
// 2. Write a service with OnModuleInit that connects to a cache, and
//    OnModuleDestroy that disconnects.
// 3. Say which line in main.ts makes step 2's cleanup actually run.

Common questions#

Middleware or interceptor?#

Middleware when you only need the raw request and do not care which route was matched — context setup, raw body, third-party Express plugins. Interceptor when you need route metadata or the response. If you find yourself wanting a decorator's value in middleware, it should be a guard or an interceptor.

Why does my middleware not see the user from my auth guard?#

Because middleware runs before guards. If you need authenticated context in middleware, that ordering cannot work — move the logic into a guard or an interceptor, or authenticate in the middleware itself.

Do I need enableShutdownHooks in development?#

You need it everywhere the process is ever terminated cleanly, which includes local development with hot reload. Leaving it out is how you end up with dozens of orphaned database connections while working.

Get the NestJS agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for NestJS. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.