Architect of the empire! Architect Vitruvius, master of buildings and systems, has prepared the most important lesson of this module for you. You've already learned about middleware, guards, interceptors, pipes, custom decorators, and exception filters. Now it's time to see how ALL of this comes together as one whole - the full lifecycle of an HTTP request in NestJS!
Imagine that a dispatch (HTTP request) sets out from a distant province to Caesar. Along the way, it passes through many checkpoints. Here is the exact order:
1Incoming HTTP request
2 |
3 v
4 1. MIDDLEWARE (Gate guard)
5 |
6 v
7 2. GUARDS (Permission check)
8 |
9 v
10 3. INTERCEPTORS - before (Chronicler - start)
11 |
12 v
13 4. PIPES (Data quality control)
14 |
15 v
16 5. ROUTE HANDLER (Caesar makes the decision)
17 |
18 v
19 6. INTERCEPTORS - after (Chronicler - end)
20 |
21 v
22 7. EXCEPTION FILTERS (Tribunal - in case of error)
23 |
24 v
25 HTTP ResponseMiddleware executes FIRST, before NestJS even knows which controller method will be called. It has access to the raw Request and Response objects:
1// Middleware executes FIRST
2@Injectable()
3export class LoggerMiddleware implements NestMiddleware {
4 use(req: Request, res: Response, next: NextFunction) {
5 console.log('[1. MIDDLEWARE] Dispatch reached the gate');
6 console.log('Method: ' + req.method + ', Path: ' + req.path);
7 next(); // Pass along
8 }
9}Guards execute AFTER middleware but BEFORE interceptors and pipes. Their only job is to answer the question: "Does this request have the right to continue?":
1// Guard executes SECOND
2@Injectable()
3export class AuthGuard implements CanActivate {
4 canActivate(context: ExecutionContext): boolean {
5 console.log('[2. GUARD] Checking legionary permissions');
6 const request = context.switchToHttp().getRequest();
7 return !!request.headers.authorization; // true = let through, false = block
8 }
9}Interceptors have a unique property - they execute twice: once BEFORE the handler and once AFTER it. The "before" phase is the code before
next.handle():1// Interceptor - BEFORE phase (third step)
2@Injectable()
3export class TimingInterceptor implements NestInterceptor {
4 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
5 const start = Date.now();
6 console.log('[3. INTERCEPTOR before] Chronicler starts recording');
7
8 return next.handle().pipe(
9 // This part is the AFTER phase (sixth step)
10 tap(() => {
11 const duration = Date.now() - start;
12 console.log('[6. INTERCEPTOR after] Time: ' + duration + 'ms');
13 }),
14 );
15 }
16}Pipes execute DIRECTLY before the handler. They transform and validate input data:
1// Pipe executes FOURTH
2@Injectable()
3export class ParseLegionIdPipe implements PipeTransform {
4 transform(value: any) {
5 console.log('[4. PIPE] Validating input data: ' + value);
6 const id = parseInt(value, 10);
7 if (isNaN(id)) {
8 throw new BadRequestException('ID must be a number!');
9 }
10 return id;
11 }
12}The handler is the target controller method - this is where business logic executes:
1@Controller('legiones')
2export class LegionController {
3 @Get(':id')
4 @UseGuards(AuthGuard)
5 @UseInterceptors(TimingInterceptor)
6 findLegion(@Param('id', ParseLegionIdPipe) id: number) {
7 console.log('[5. HANDLER] Caesar processes the query about legion ' + id);
8 return { id, name: 'Legio X Gemina', commander: 'Marcus' };
9 }
10}After the handler completes, control returns to the interceptor - to the code inside
pipe(tap(...)). Here you can transform the response, log time, etc.Exception Filters execute ONLY when an exception is thrown - at any stage. If a guard throws ForbiddenException, the filter catches it. If a pipe throws BadRequestException - it catches that too:
1// Exception Filter - catches errors from EVERY stage
2@Catch(HttpException)
3export class ImperiumExceptionFilter implements ExceptionFilter {
4 catch(exception: HttpException, host: ArgumentsHost) {
5 console.log('[7. EXCEPTION FILTER] Tribunal handles the error');
6 const ctx = host.switchToHttp();
7 const response = ctx.getResponse();
8 response.status(exception.getStatus()).json({
9 error: exception.message,
10 timestamp: new Date().toISOString(),
11 });
12 }
13}Understanding the order is key for debugging:
1// Example: What happens when a guard rejects the request?
2// 1. Middleware - WILL EXECUTE (logging)
3// 2. Guard - WILL REJECT (throws ForbiddenException)
4// 3. Interceptor before - will NOT execute
5// 4. Pipe - will NOT execute
6// 5. Handler - will NOT execute
7// 6. Interceptor after - will NOT execute
8// 7. Exception Filter - WILL EXECUTE (handles ForbiddenException)
9
10// Example: What happens when a pipe rejects the data?
11// 1. Middleware - WILL EXECUTE
12// 2. Guard - WILL EXECUTE (passes through)
13// 3. Interceptor before - WILL EXECUTE
14// 4. Pipe - WILL REJECT (throws BadRequestException)
15// 5. Handler - will NOT execute
16// 6. Interceptor after - will NOT execute (no response)
17// 7. Exception Filter - WILL EXECUTE (handles BadRequestException)A practical debugging technique - add logs at each stage:
1// middleware
2console.log('[MW] ' + req.method + ' ' + req.path);
3
4// guard
5console.log('[GUARD] User: ' + request.user?.name);
6
7// interceptor before
8console.log('[INT-B] Handler: ' + context.getHandler().name);
9
10// pipe
11console.log('[PIPE] Value: ' + value + ' -> ' + transformed);
12
13// handler
14console.log('[HANDLER] Processing...');
15
16// interceptor after
17console.log('[INT-A] Duration: ' + duration + 'ms');
18
19// exception filter
20console.log('[FILTER] Error: ' + exception.message);When you see the logs, you can precisely trace the request's journey through the entire pipeline and quickly find at which stage a problem occurs.
Remember this order like a marching legion column:
Knowing this lifecycle is the foundation of every advanced NestJS developer - like knowing battle strategy for a centurion!