Every HTTP request that lands on a NestJS application follows a predictable path through several layers of logic. Understanding the NestJS request lifecycle is essential because it determines exactly when your guards run, when pipes validate input, and where exceptions get caught. Get this flow wrong, and you'll spend hours debugging mysterious validation failures or permission checks that don't trigger when you expect.
The complete execution order
When a request enters your NestJS application, it doesn't jump straight to your route handler. Instead, it passes through a series of gates, validators, and wrappers in a strict sequence. Here's what runs, and in what order:
- Middleware — Raw request preprocessing
- Guards — Access control decisions
- Interceptors (before) — Pre-handler wrapping
- Pipes — Transform and validate input
- Route handler — Your controller method
- Interceptors (after) — Post-handler wrapping
- Exception filters — Error handling and response formatting
This ordering is not arbitrary. Each layer has a specific purpose, and they build on the results of the previous layer. Knowing this sequence helps you decide which layer should handle each concern in your application.
Guards decide access
Guards are the bouncers at the door. They run early in the lifecycle—after middleware but before anything touches your request body. A guard's job is to answer one question: Is this request allowed to proceed? It returns true (proceed) or false (reject) and nothing else.
Because guards run before pipes, you can make access control decisions without worrying about whether the body has been validated yet. This is crucial when you're checking JWT tokens, role-based permissions, or API key validity.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class RoleGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
return user && user.role === 'admin';
}
}
When you apply this guard to a route with @UseGuards(RoleGuard), it executes before the request body is even parsed or validated. If it returns false, NestJS throws a 403 Forbidden error immediately, and pipes never run.
A real-world scenario: you're building a multi-tenant SaaS platform. A guard checks that the requesting user belongs to the organization they're trying to access. This must happen before any pipe transforms the request body, because you need to make the access decision on the raw request context, not on validated DTOs.
Pipes transform and validate
Pipes are transformers and validators. They take input (usually from the request body, query parameters, or URL segments) and either transform it into the shape your handler expects or reject it with a validation error.
Pipes run after guards pass, which means they're safe to use. You know the request is authorized before pipes try to do their work. Common use cases include converting a string ID to a number, validating a DTO against a schema, or parsing JSON from a query parameter.
import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: any) {
const parsed = parseInt(value, 10);
if (isNaN(parsed)) {
throw new BadRequestException('ID must be a number');
}
return parsed;
}
}
// In your controller:
@Get(':id')
getUser(@Param('id', ParseIntPipe) id: number) {
return { userId: id };
}
NestJS includes built-in pipes like ValidationPipe (via the class-validator library) that automatically validate request bodies against DTO class definitions. When validation fails, the pipe throws an exception before your route handler is ever called.
In a real-world e-commerce platform, a ValidationPipe ensures that order quantities are positive integers, email addresses are valid, and required fields are present. If validation fails, the user gets a clear 400 Bad Request response with field-level error details—all without your handler ever running.
Interceptors wrap the handler
Interceptors are middleware on steroids. They wrap your route handler and can run code before it executes and after it returns. Think of them as the ability to add cross-cutting concerns like logging, timing, caching, or response transformation.
Because interceptors run after pipes, the data your handler receives is already validated and transformed. Interceptors can also modify the response before it's sent to the client.
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 {
const request = context.switchToHttp().getRequest();
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] ${request.method} ${request.path}`);
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startTime;
console.log(`Response sent in ${duration}ms`);
})
);
}
}
Here, the interceptor logs the incoming request, calls next.handle() (which runs your handler), then logs the response time. The tap operator runs side effects without modifying the response.
You can also use interceptors to catch exceptions thrown by your handler and transform them, add headers to every response, or cache successful responses. In a ride-hailing app, an interceptor could log every request to an analytics service and measure handler execution time to detect performance regressions.
Exception filters catch what escapes
Exception filters are the safety net. Any exception thrown by a guard, pipe, interceptor, or route handler that doesn't get caught elsewhere ends up here. Filters transform exceptions into HTTP responses that make sense for your API.
NestJS has a built-in HttpExceptionFilter that handles common cases, but you can write custom filters to handle domain-specific exceptions or format error responses in a particular way.
import { Catch, ExceptionFilter, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ExecutionContext) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.getMessage();
}
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
});
}
}
When you register this filter globally (using app.useGlobalFilters(new GlobalExceptionFilter())), every unhandled exception becomes a structured JSON response with a consistent format.
A practical example: a validation pipe throws a BadRequestException because the DTO is invalid. The exception filter catches it, extracts the validation errors, and returns a 400 response with field-level error details that the frontend can use to show specific error messages next to each form field.
Common mistakes
Mistake 1: Trying to access the validated body in a guard. Guards run before pipes, so the body hasn't been transformed or validated yet. If you need to inspect the body in an access control decision, use an interceptor or move the logic into your route handler.
Mistake 2: Using pipes for business logic. Pipes are for transforming and validating input, not for fetching data or running side effects. If you need to check whether a user owns a resource, do that in your handler or use a custom guard that queries the database.
Mistake 3: Swallowing exceptions in interceptors. If you catch exceptions in an interceptor without re-throwing them, they won't reach your exception filter, and you might end up returning 200 OK with an error embedded in the response body. Catch and handle exceptions explicitly, or let them propagate.
Mistake 4: Registering global pipes/filters without understanding their scope. A global ValidationPipe applies to every route unless you explicitly exclude it. This is usually what you want, but if you have legacy routes or special cases, understand the order in which global and route-level pipes are applied.
Key takeaways
- The NestJS request lifecycle follows a strict order: middleware → guards → interceptors (before) → pipes → handler → interceptors (after) → filters.
- Guards run before pipes and are ideal for access control decisions that don't require a validated body.
- Pipes validate and transform input, and they run after guards have approved the request.
- Interceptors wrap handlers and are perfect for logging, timing, caching, and response transformation.
- Exception filters catch exceptions from any layer and format them into HTTP responses.
- Use each layer for its intended purpose. Conflating them makes your code harder to debug and test.






Comments
Loading comments…