NestJS course Β· Module 2: Routing and Request Lifecycle

Request Lifecycle in NestJS - the journey of a dispatch through the empire

5 min read
In this lesson5

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!

The full journey of a dispatch through the empire

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 Response

Each stage in detail

1. Middleware - the gate guard

Middleware 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.originalUrl);
7    next(); // Pass along
8  }
9}

2. Guards - permission check

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}

3. Interceptors (before) - chronicler start

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}

4. Pipes - data quality control

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}

5. Route Handler - Caesar's decision

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}

6. Interceptors (after) - chronicler end

After the handler completes, control returns to the interceptor - to the code inside pipe(tap(...)). Here you can transform the response, log time, etc.

7. Exception Filters - the tribunal

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}

Practical implications of the order

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 - tap() and map() will NOT run (no response), but the error flows through its stream (catchError sees it)
17// 7. Exception Filter - WILL EXECUTE (handles BadRequestException)

Debugging the pipeline

A practical debugging technique - add logs at each stage:

1// middleware
2console.log('[MW] ' + req.method + ' ' + req.originalUrl);
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.

Lifecycle summary

Remember this order like a marching legion column:

  1. Middleware - first checkpoint (raw request)
  2. Guards - permission check (let through/deny)
  3. Interceptors before - start of observation
  4. Pipes - validation and data transformation
  5. Handler - business logic
  6. Interceptors after - end of observation, response transformation
  7. Exception Filters - error handling (when something went wrong)

Knowing this lifecycle is the foundation of every advanced NestJS developer - like knowing battle strategy for a centurion!

Code for this lesson: src/request-lifecycle.ts
1// Request Lifecycle - Path of a Dispatch through the Empire
2console.log("=== FULL REQUEST LIFECYCLE in NestJS ===\n");
3
4// Order of HTTP request processing:
5const lifecycle = [
6  '1. MIDDLEWARE      - Gate guard (raw request)',
7  '2. GUARDS          - Permission check (allow/deny)',
8  '3. INTERCEPTORS    - Chronicler start (before)',
9  '4. PIPES           - Data validation and transformation',
10  '5. ROUTE HANDLER   - Caesar makes the decision',
11  '6. INTERCEPTORS    - Chronicler end (after)',
12  '7. EXCEPTION FILTERS - Tribunal (in case of error)',
13];
14
15lifecycle.forEach(step => console.log(step));
16
17console.log("\n=== SCENARIOS ===\n");
18
19console.log("Scenario A: Guard rejects request");
20console.log("  1. Middleware    -> EXECUTED");
21console.log("  2. Guard         -> REJECTS (ForbiddenException)");
22console.log("  3-6.             -> SKIPPED");
23console.log("  7. Exception Filter -> HANDLES error\n");
24
25console.log("Scenario B: Pipe rejects data");
26console.log("  1. Middleware    -> EXECUTED");
27console.log("  2. Guard         -> ALLOWS");
28console.log("  3. Interceptor   -> EXECUTED (before)");
29console.log("  4. Pipe          -> REJECTS (BadRequestException)");
30console.log("  5-6.             -> SKIPPED");
31console.log("  7. Exception Filter -> HANDLES error\n");
32
33console.log("Scenario C: Everything OK");
34console.log("  1. Middleware    -> EXECUTED");
35console.log("  2. Guard         -> ALLOWS");
36console.log("  3. Interceptor   -> EXECUTED (before)");
37console.log("  4. Pipe          -> VALIDATES data");
38console.log("  5. Handler       -> PROCESSES logic");
39console.log("  6. Interceptor   -> EXECUTED (after)");
40console.log("  7. Exception Filter -> not needed\n");
41
42console.log("=== KEY PRINCIPLES ===");
43console.log("- Middleware does not know the target controller method");
44console.log("- Guard decides on access (true/false)");
45console.log("- Interceptor sees request BEFORE and AFTER handler");
46console.log("- Pipe validates/transforms DIRECTLY before handler");
47console.log("- Exception Filter catches errors from EVERY stage");
48

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which NestJS element executes FIRST in the HTTP request lifecycle?

  2. 2. What distinguishes Interceptors from other elements of the NestJS pipeline?

  3. 3. What happens when a Guard rejects a request (returns false)? Which pipeline elements will NOT execute?

  4. 4. An interceptor logs the time in tap() after the response, and a pipe throws BadRequestException. What still runs?

  5. 5. At which stage of the Request Lifecycle can Exception Filters take action?

Hands-on tasks in the game

  • Vertical ordering

    Order the stages of an HTTP request lifecycle in NestJS from first to last:

  • Code editor

    Write a LifecycleLoggerMiddleware that logs the request method and path, then calls next()

  • Click in order

    Arrange the FULL order of HTTP request processing in NestJS:

  • Code editor

    Write a LifecycleAuthGuard that checks the authorization header - return true if it exists, false if not

  • Horizontal ordering

    Arrange the elements of retrieving the request object in a guard's canActivate method:

  • Code editor

    Write a LifecycleInterceptor that stores Date.now() before next.handle() and, in tap(...) after the response, logs how many milliseconds handling the request took

  • Vertical ordering

    Order what happens to a request when a guard rejects it, from the first stage to the last:

  • Code editor

    Write a LifecycleValidationPipe that parses a value to a number and throws BadRequestException if it's not a number

  • Click in order

    Arrange the debug logs in the order they will appear in the console for a successful request:

Useful articles