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.
| Enhancer | Runs | Primary job | Has access to |
|---|---|---|---|
| Middleware | First, before routing is resolved | Cross-cutting request prep (logging, CORS, body parsing) | Raw req/res, next |
| Guard | After middleware, before interceptors | Authorization — return true/false | ExecutionContext, no route metadata about params |
| Interceptor (pre) | After guards | Bind extra logic, transform, start timers | ExecutionContext, CallHandler |
| Pipe | After interceptors, just before handler | Validate & transform arguments | The specific argument + its metadata |
| Filter | On any thrown exception | Map errors to responses | The 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:
| Concern | Right enhancer |
|---|---|
| Authentication / authorization | Guard |
| Input validation & coercion | Pipe |
| Response shaping / timing / caching | Interceptor |
| Mapping exceptions to HTTP responses | Exception 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
ValidationPipewithwhitelist: truefor 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
ReflectorwithSetMetadatato 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.