Backend cheatsheetNestJS request lifecycleKnow where your code runs
NestJS
Lifecycle
Middleware, guards, interceptors, pipes and filters all touch the same request. This cheatsheet shows the order they run in, what each one can see, and which one a job belongs to.
Key terms in plain words
New to how servers handle a request? Start here. Every word below shows up later on this page, and each one comes with an everyday comparison. Underlined words in the modules link back to these cards.
22 terms in 4 groups. Hover an underlined word anywhere on the page for a quick reminder, or click it to jump here.
The trip itself
A request moves through a NestJS app the way a passenger moves through an airport: in through the doors, past a few checkpoints, onto the plane, and back out.
- Request and response
- Think of it as a passenger arriving with a question and leaving with an answer
- A request is a message sent to your server, such as "show me game 42". The response is what the server sends back: the data, or an error with a number that says what went wrong.
- Request lifecycle
- Think of it as the fixed route through the airport, from the front door to the plane and back
- The order every request moves through: middleware, guards, interceptors, pipes, the handler, then interceptors again on the way out. Filters step in only if something throws.
- Route
- Think of it as a numbered gate, such as Gate 12 for flights to Delhi
- A pairing of an action (GET to read, DELETE to remove) with an address. Nest sends each request to the one route that matches it.
- For example
DELETE /games/42is one route;GET /games/42is another. - Route handler
- Think of it as the flight itself, the reason for the whole trip
- The function that does the real work for one route, such as finding a game. Every checkpoint on this page runs before or after it.
- Controller
- Think of it as the desk at a gate that takes each passenger's request and passes it to the right staff
- A class whose methods each answer one route. Checkpoints can be attached to a whole controller, so they apply to every route it holds.
- Throw an error
- Think of it as pulling the emergency cord: normal travel stops and the problem goes to the service desk
- When code hits a problem it can throw an error. Everything after that point is skipped, and Nest hands the error to an exception filter to answer the client.
The five checkpoints
Each one has a single job, and each runs at its own fixed moment.
- Middleware
- Think of it as the doors and check-in hall every passenger walks through before anyone looks at their flight
- Code that runs first, on every request, before Nest has worked out which handler will answer. Good for jobs that don't care about the route, like stamping each request with an id.
- Guard
- Think of it as passport control, which only ever says yes or no
- Code that decides whether a request may reach its handler at all. It returns true to let it through, or false or an error to stop it. Login checks and roles live here.
- Interceptor
- Think of it as a host who meets you before the gate and sees you off after you land
- Code that wraps the handler: part of it runs before, part after. It can time a request, reshape every answer the same way, or give up on one that takes too long.
- Pipe
- Think of it as the boarding pass scanner at the gate, the last check before you board
- Code that takes one input just before the handler gets it, and either converts it (the text "42" into the number 42) or rejects it with an error.
- Exception filter
- Think of it as the customer service desk that turns any problem into a clear notice for the passenger
- Code that catches errors and decides what the client sees. One filter can give every error the same shape and hide internal details that should stay private.
What each checkpoint knows
Why a guard can read a route's rules and middleware cannot, and how checkpoints get the helpers they need.
- Execution context
- Think of it as the passenger's full file, showing which flight and gate they're headed for
- An object Nest hands to guards, interceptors and filters. Through it they can reach the request and find out which controller and handler will answer it.
- Decorator
- Think of it as a label stuck on a desk or a bag that tells the airport how to treat it
- A word starting with
@placed just above a class, method or parameter. It doesn't run your code; it attaches instructions Nest reads later. - For example
@Roles(['admin'])marks a route as admins only. - Metadata
- Think of it as notes written on a passenger's file that staff further along can read
- Extra information attached to a handler or controller by a decorator, such as "admins only". A guard reads it through the Reflector to decide whether to let a request through.
- Dependency injection
- Think of it as the airport assigning staff to a desk, so no desk hires its own
- A class lists what it needs, and Nest creates those things and passes them in. Global middleware sits outside this system, so it cannot ask Nest for helpers.
- Module
- Think of it as one terminal of the airport, with its own desks and staff
- A box that groups the pieces for one feature. Middleware can be bound to a module, so it only runs for routes that module owns.
- Global, controller and route scope
- Think of it as a rule for the whole airport, for one terminal, or for one gate
- Where a checkpoint is attached. Global ones apply to every request, controller ones to every route in that controller, route ones to a single handler. Inside each step, global runs first.
Inputs, outputs and errors
What gets checked on the way in, how the answer is reshaped on the way out, and the numbers that say what happened.
- DTO
- Think of it as the arrival form listing exactly which details a passenger must fill in
- Data transfer object: a class that describes the shape of the data a request should carry, field by field, with rules for each one.
- For example
CreateGameDtosaysplayersmust be a whole number from 2 to 8. - Validation
- Think of it as checking each arrival form is filled in properly before the passenger moves on
- Checking incoming data against the rules before your code uses it. Anything missing, wrongly typed or unexpected is turned away with a 400 error.
- Observable (RxJS)
- Think of it as a baggage belt that will carry the result out once it's ready
- RxJS is a library for values that arrive later. An Observable is its container for such a value. Interceptors add steps to it, such as "wrap the answer" or "stop after 5 seconds".
- HTTP status code
- Think of it as the short code on the departure board: delayed, cancelled, gate changed
- Every response carries a number. 200 means fine, 400 means the input was wrong, 401 means not logged in, 403 means not allowed, 404 means not found, 500 means the server broke.
- For example A guard that returns false makes Nest answer 403.
- Request id
- Think of it as the baggage tag number that follows one bag through every belt
- A short unique code given to each request as it arrives. Every later step and every log line can carry it, so you can find all traces of one request later.
The whole trip
Every request passes the same checkpoints in the same order. Learn the order once and you always know where a piece of code belongs.
A request enters through middleware, is allowed or refused by guards, is wrapped by interceptors, has its input checked by pipes, and only then reaches your route handler. On the way out it passes back through the same interceptors in reverse. If anything throws along the way, an exception filter turns the error into a response.
Picture it: one request, start to finish
Solid arrows are the happy path. The coral path is taken the moment anything throws, from a guard, a pipe or the handler itself.
| Step | Runs | Knows the handler? | Typical job |
|---|---|---|---|
| Middleware | global, then module bound | No | Request id, CORS, body limits, raw logging |
| Guards | global, controller, route | Yes | Auth, roles, feature flags |
| Interceptors | in: global, controller, route. Out: reversed | Yes | Timing, caching, response mapping |
| Pipes | global, controller, route, param | Yes | Validate and transform input |
| Exception filters | route, controller, global | Yes | Shape error responses |
Middleware
Plain Express style functions that see the raw request before Nest has picked a route. Use them for work that does not care which handler runs.
Middleware runs before routing is resolved, so it gets req, res and next, but no ExecutionContext and no idea which controller method is coming. That makes it the right home for request ids, raw body limits and access logs, and the wrong home for anything that needs route metadata like roles.
Chooses which middleware to bind, in the order listed.
See the exampleLimits it to paths, methods or whole controllers.
See the exampleCarves out routes such as health checks.
See the example@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const id = req.header('x-request-id') ?? randomUUID();
req.headers['x-request-id'] = id;
res.setHeader('x-request-id', id);
next(); // forget this and the request hangs forever
}
}
@Module({ controllers: [GamesController] })
export class GamesModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestIdMiddleware)
.exclude({ path: 'health', method: RequestMethod.GET })
.forRoutes(GamesController);
}
}
Why it matters: every later step, and every log line, can rely on one request id without knowing where it came from.
Guards
One question, one answer: may this request reach the handler? Return true to let it through, false or throw to stop it.
Guards run after all middleware and before any interceptor or pipe. Unlike middleware they receive an ExecutionContext, so they can read the metadata you put on the handler with decorators. Returning false makes Nest throw a ForbiddenException (403); throw your own exception for anything else, like a 401.
export const Roles = Reflector.createDecorator<string[]>();
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride(Roles, [ctx.getHandler(), ctx.getClass()]);
if (!roles) return true; // no roles on the route: open
const user = ctx.switchToHttp().getRequest().user;
if (!user) throw new UnauthorizedException();
return roles.some((r) => user.roles.includes(r));
}
}
@Roles(['admin'])
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {}
Why it matters: the rule lives on the route as data, and one guard enforces it everywhere.
| Return or throw | Client sees |
|---|---|
true | The request carries on to interceptors |
false | 403 Forbidden resource |
throw new UnauthorizedException() | 401, with your message |
| A Promise or Observable | Awaited, then treated as above |
Interceptors
Code that wraps the handler: it runs before, sees the result after, and can change either. The only step that works on both sides.
An interceptor gets the context and a CallHandler. Anything before next.handle() runs on the way in; operators piped onto it run on the way out. Several interceptors nest like an onion: the global one is outermost, so it starts first and finishes last.
Picture it: interceptors wrap the handler
@Injectable()
export class TimingInterceptor implements NestInterceptor {
private readonly log = new Logger('HTTP');
intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
const start = performance.now();
const { method, url } = ctx.switchToHttp().getRequest();
return next.handle().pipe(
map((data) => ({ data })), // wrap every body in one envelope
tap(() => this.log.log(`${method} ${url} ${Math.round(performance.now() - start)}ms`)),
);
}
}
Why it matters: timing, caching and response envelopes live in one place instead of in every controller.
Changes the response body on the way out.
See the exampleLooks at the result without changing it, for logs and metrics.
See the exampleTurns an error into another error or a fallback value.
See the exampleFails slow handlers with a TimeoutError you can map to 408.
See the examplePipes
The last checkpoint before your handler: validate the input, or turn it into the type you want.
Pipes run after interceptors have started and right before the handler is called, once per decorated argument. A pipe either returns the value, possibly transformed, or throws, usually a BadRequestException. Parameter pipes are processed from the last parameter to the first.
Picture it: a route parameter through ParseIntPipe
"42"ParseIntPipe→42·"abc"ParseIntPipe→400Strings from the URL become numbers, or the request stops with a 400 before your code runs.export class CreateGameDto {
@IsIn(['classic', 'ranked']) mode: string;
@IsInt() @Min(2) @Max(8) players: number;
@IsOptional() @IsString() @MaxLength(40) name?: string;
}
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip fields the DTO does not declare
forbidNonWhitelisted: true, // or reject them with a 400
transform: true, // plain JSON becomes a CreateGameDto instance
}));
Why it matters: handlers receive typed, checked data, and unknown fields never reach your database.
| Built in pipe | Turns | Into |
|---|---|---|
| ParseIntPipe | "42" | 42 or 400 |
| ParseUUIDPipe | "9b1d..." | the same string, checked |
| ParseBoolPipe | "true" | true |
| ParseEnumPipe | "ranked" | an enum member |
| DefaultValuePipe | undefined | your default |
| ValidationPipe | a JSON body | a validated DTO |
Exception filters
Where errors become responses. Filters run only when something throws, and the most specific one wins.
Filters are the one step that resolves from the inside out: route filters are tried first, then controller, then global. The first filter whose @Catch() matches the error handles it, and no other filter sees it. With no filter of your own, Nest’s built in one turns an HttpException into its status and message, and anything else into a 500.
@Catch()
export class ProblemFilter implements ExceptionFilter {
catch(err: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const req = ctx.getRequest<Request>();
const status = err instanceof HttpException ? err.getStatus() : 500;
ctx.getResponse<Response>().status(status).json({
status,
error: err instanceof HttpException ? err.message : 'Internal error',
path: req.url,
requestId: req.headers['x-request-id'],
});
}
}
Why it matters: every error leaves in one shape with a request id, and internal messages never reach the client.
- Route filter@UseFilters on the method
- Controller filter@UseFilters on the class
- Global filterAPP_FILTER or useGlobalFilters
- Built in filterHttpException or 500
Order in action
A tiny model of the pipeline in plain JavaScript. Run it to watch three requests take three different paths.
This is not Nest itself, just the same rules in forty lines: middleware first, guards next, interceptors wrapping the pipe and the handler, and filters tried from the most specific out. Press Run and compare the three traces.
const trail = [];
const say = (s) => trail.push(s);
const fail = (msg, status) => Object.assign(new Error(msg), { status });
const middleware = [() => say('middleware requestId'), () => say('middleware parse token')];
const guards = [() => (say('guard throttle'), true), (req) => (say('guard roles'), req.role === 'admin')];
const interceptors = ['global timing', 'route transform'];
const pipes = [(v) => (say('pipe ParseIntPipe'), Number(v))];
const filters = [
(e) => (e.status === 404 ? 'route filter 404 ' + e.message : null),
(e) => 'global filter ' + (e.status || 500) + ' ' + e.message,
];
function handle(req, handler) {
trail.length = 0;
try {
middleware.forEach((m) => m(req));
for (const g of guards) if (!g(req)) throw fail('Forbidden resource', 403);
const call = interceptors.reduceRight((next, name) => () => {
say('before ' + name);
const out = next();
say('after ' + name);
return out;
}, () => handler(pipes.reduce((v, p) => p(v), req.id)));
call();
say('response 200');
} catch (e) {
for (const f of filters) { const r = f(e); if (r) { say(r); break; } }
}
return trail.join('\n');
}
const findGame = (id) => {
say('handler findGame(' + id + ')');
if (id !== 42) throw fail('Game not found', 404);
};
console.log('# admin asks for game 42');
console.log(handle({ role: 'admin', id: '42' }, findGame));
console.log('# player asks for game 42');
console.log(handle({ role: 'player', id: '42' }, findGame));
console.log('# admin asks for game 7');
console.log(handle({ role: 'admin', id: '7' }, findGame));
$ node lifecycle-model.js # admin asks for game 42 middleware requestId middleware parse token guard throttle guard roles before global timing before route transform pipe ParseIntPipe handler findGame(42) after route transform after global timing response 200 # player asks for game 42 middleware requestId middleware parse token guard throttle guard roles global filter 403 Forbidden resource # admin asks for game 7 middleware requestId middleware parse token guard throttle guard roles before global timing before route transform pipe ParseIntPipe handler findGame(7) route filter 404 Game not found
Why it matters: a refused request never reaches an interceptor or pipe, and a thrown error skips every after step.
| Scope | Middleware | Guards, interceptors, pipes, filters |
|---|---|---|
| Global | app.use() or a module bound for all routes | APP_GUARD, APP_INTERCEPTOR, APP_PIPE, APP_FILTER |
| Controller | forRoutes(Controller) | @UseGuards() and friends on the class |
| Route | forRoutes({ path, method }) | The same decorators on the method |
| Parameter | n/a | @Body(ValidationPipe), @Param("id", ParseIntPipe) |
Which one do I need?
Match the job to the step. When two could work, pick the one that runs latest while still having what it needs.
| I want to | Use | Why there |
|---|---|---|
| Tag every request with an id | Middleware | Runs first, needs no route metadata |
| Refuse a caller without the right role | Guard | Has the handler metadata and runs before any work |
| Time requests or wrap every body | Interceptor | The only step that sees both the way in and the way out |
| Cache a GET response | Interceptor | Can return early without calling the handler |
| Turn "42" into 42 or reject a body | Pipe | Runs per argument right before the handler |
| Give every error the same JSON shape | Exception filter | Catches anything thrown anywhere after middleware |
| Stop slow handlers | Interceptor with timeout() | Wraps the call and can fail it |