Fullstack CourseLearn by building
Back to week 3

Topic

Middleware vs guards vs pipes

Definition

Nest processes an incoming request through ordered extension points — middleware, guards, interceptors, and pipes — where each stage has full access to different information and a distinct, narrow responsibility.

In simpler words

Middleware runs first and knows nothing about routes; guards decide who may proceed; pipes clean up and validate the data the handler will receive.

Correlation ids, JWT/role checks, and DTO validation look similar — “code that runs before my controller” — but belong in different stages for a reason.

After this you can

  • Order middleware, guards, and pipes in the request pipeline
  • Explain why middleware cannot read @Roles or @Public
  • Choose the right stage for a new cross-cutting check

Where each stage sits in the pipeline

Definition

Middleware executes before Nest resolves a route handler and its metadata, guards execute afterward with full execution-context access, and pipes execute per parameter to transform or validate arguments just before the handler runs.

In simpler words

Middleware cannot see @Roles or @Public; guards can. Pipes see one argument at a time, not the whole request.

Rough order: Middleware → Guards → Interceptors (before) → Pipes → Controller handler → Interceptors (after) → Exception filters.

CorrelationIdMiddleware runs for every matched path via consumer.apply(...).forRoutes("*") — it cannot make an authorization decision because guards, and the route metadata they read, do not exist yet at that point.

Correlation id middleware

const correlationId = req.header('x-correlation-id') ?? randomUUID();
req.correlationId = correlationId;
res.setHeader('x-correlation-id', correlationId);
next();

Cross-cutting and stateless — a good middleware job. It never reads @Roles or user identity.

Choosing the right tool for a new check

Definition

A new cross-cutting request concern belongs in middleware only when it needs no route metadata and no per-argument transformation, in a guard when it depends on route metadata or the authenticated subject, and in a pipe when it validates or reshapes one specific argument.

In simpler words

Ask: does this need the route’s decorators (guard), just one incoming value (pipe), or neither (middleware)?

ValidationPipe is registered globally in main.ts and runs for every DTO-typed parameter — it is what turns CreateTicketDto’s decorators into an actual 400 response.

ParseUUIDPipe on :id params is a pipe too: it rejects a malformed id before TicketsService ever runs a query.

Three stages, one request

GET /tickets/not-a-uuid
// Middleware: attaches correlation id, calls next()
// Guards: JwtAuthGuard confirms the session, RolesGuard allows (no @Roles here)
// Pipe: ParseUUIDPipe rejects 'not-a-uuid' -> 400 before the handler runs

Each stage rejects or passes the request for a different reason — none of them duplicate the others’ job.

Keep in mind

  • Middleware: cross-cutting, stateless, no route metadata.
  • Guards: authentication and authorization, route-metadata aware.
  • Pipes: per-argument validation and transformation, closest to the handler.

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥80% (enforced by the API) to save progress.

Checking your session…

15 questions · concept 5 · syntax 3 · practical 4 · logic 3

1. What runs first among middleware, guards, and pipes?
Concept
2. Can middleware read @Roles like a guard?
Syntax
3. Are guards and pipes interchangeable for DTO validation?
Practical
4. What is CorrelationIdMiddleware for?
Logic
5. When do pipes validate @Body DTOs?
Concept
6. Why put authz in guards not earliest middleware?
Practical
7. What does ValidationPipe whitelist do?
Syntax
8. What does forbidNonWhitelisted do?
Logic
9. Can middleware end the response?
Concept
10. Which layer transforms query limit strings to numbers?
Practical
11. For an unauthenticated request to a guarded route, does ValidationPipe run?
Conceptadvanced
app.useGlobalPipes(new ValidationPipe());
// a guard rejects unauthenticated requests
12. With whitelist:true, what does the handler receive as the DTO?
Syntaxintermediate
new ValidationPipe({ whitelist: true });
// body: { title: 'Bug', isAdmin: true }
13. Now with forbidNonWhitelisted:true, what happens to that same request?
Practicalintermediate
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true });
// body: { title: 'Bug', isAdmin: true }
14. With transform:true, what is limit inside the handler?
Logicadvanced
class ListDto { @IsInt() @Type(() => Number) limit: number; }
// GET /tickets?limit=20
15. Why implement this as middleware rather than a guard?
Conceptintermediate
export class CorrelationIdMiddleware implements NestMiddleware {
  use(req, res, next) {
    req.correlationId = randomUUID();
    next();
  }
}

Checking your session…