Skip to content
NestJS interview 4 min read

Request Lifecycle Questions

The request lifecycle is one of the most discriminating NestJS interview topics because it separates developers who have only used enhancers from those who understand when and why each runs. Interviewers probe the exact ordering of middleware, guards, interceptors, pipes, the handler, and filters — and the subtle differences in scope, capability, and data sharing between them. The answers below give the precise sequence, the reasoning behind it, and the code that demonstrates it.

In what order do enhancers execute?

This is the canonical question. For an incoming request that reaches a controller handler, Nest runs the pieces in a fixed order: middleware, then guards, then interceptors (pre), then pipes, then the handler, then back out through interceptors (post), with exception filters catching anything thrown along the way. Knowing this order lets you place each concern where it can actually do its job.

Incoming request
  -> Global / module middleware
  -> Guards (global -> controller -> route)
  -> Interceptors (pre-controller logic, before next.handle())
  -> Pipes (param transformation & validation)
  -> Route handler (controller method)
  -> Interceptors (post-controller logic, after next.handle())
  -> Exception filters (only if an error is thrown)
Outgoing response

A frequent gotcha: guards run before pipes, so a guard cannot rely on a validated/transformed body — it only sees the raw request. If your authorization needs parsed data, that work belongs in a pipe or the handler, not the guard.

When does each enhancer run, and what can it access?

Each enhancer has a distinct purpose and a distinct view of the request. The table below is the mental model interviewers want you to articulate.

EnhancerRunsPrimary jobHas access to
MiddlewareFirst, before routing is resolvedCross-cutting request prep (logging, CORS, body parsing)Raw req/res, next
GuardAfter middleware, before interceptorsAuthorization — return true/falseExecutionContext, no route metadata about params
Interceptor (pre)After guardsBind extra logic, transform, start timersExecutionContext, CallHandler
PipeAfter interceptors, just before handlerValidate & transform argumentsThe specific argument + its metadata
FilterOn any thrown exceptionMap errors to responsesThe exception + ArgumentsHost

A key talking point: middleware is the only piece that does not know which route handler will be invoked, because it runs before Nest resolves the route. Guards and beyond receive an ExecutionContext, which does expose the target class and handler via getClass() and getHandler().

How do guards and interceptors differ?

Both can short-circuit a request, but for different reasons. A guard answers a single boolean question — “is this request allowed?” — and runs before any interceptor or pipe. An interceptor wraps the handler, so it can run logic both before and after, transform the response, override it entirely, or extend behavior with RxJS operators.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    return req.headers['x-role'] === 'admin';
  }
}
import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const start = Date.now();
    // pre-controller code runs here
    return next
      .handle()
      .pipe(tap(() => console.log(`Handled in ${Date.now() - start}ms`)));
  }
}

Output:

GET /cats  ->  Handled in 4ms

How do you share data across the lifecycle?

The idiomatic Nest answer is to attach data to the request object in an early enhancer and read it later. Guards, interceptors, and pipes all reach the same request via the ExecutionContext, so a guard can stash the resolved user and the handler can consume it.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    req.user = { id: 42, role: 'admin' }; // shared downstream
    return true;
  }
}
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext) =>
    ctx.switchToHttp().getRequest().user,
);

For sharing static metadata between a decorator and a guard, use SetMetadata (or Reflector.createDecorator) and read it with the injected Reflector. For per-request stateful services, use REQUEST-scoped providers instead of mutating the request directly.

Which enhancer should I choose for a given concern?

Interviewers love a “where would you put X?” question. Match the concern to the enhancer designed for it:

ConcernRight enhancer
Authentication / authorizationGuard
Input validation & coercionPipe
Response shaping / timing / cachingInterceptor
Mapping exceptions to HTTP responsesException filter
Low-level request prep (CORS, raw body)Middleware

Best Practices

  • Remember the order: middleware -> guards -> interceptors -> pipes -> handler -> interceptors -> filters; place each concern accordingly.
  • Use guards purely for authorization decisions, not for transforming or validating payloads.
  • Validate and transform with pipes, and enable a global ValidationPipe with whitelist: true for DTO safety.
  • Share request-scoped data by attaching it to the request in a guard and reading it via a custom param decorator.
  • Use the Reflector with SetMetadata to pass route-level metadata (like required roles) into guards and interceptors.
  • Centralize error-to-response mapping in exception filters rather than scattering try/catch in controllers.
  • Apply enhancers at the narrowest sensible scope (route over controller over global) to keep behavior predictable.
Last updated June 14, 2026
Was this helpful?