[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-nestjs-weekly-request-lifecycle":44,"search-suggestions":60,"quiz-article-nestjs-weekly-request-lifecycle":106},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a03cdb-23ec-737a-bc56-ddd475bef681","nestjs-weekly-request-lifecycle","PRACTICE_QUIZ","NestJS Request Lifecycle","Test yourself on the exact order a NestJS request travels through, and what happens when a guard denies or a pipe throws.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,64,68,72,76,80,84,88,92,96,99,103],{"slug":62,"name":63,"articles":53},"webdev","Webdev",{"slug":65,"name":66,"articles":67},"frontend","Frontend",61,{"slug":69,"name":70,"articles":71},"javascript","Javascript",59,{"slug":73,"name":74,"articles":75},"css","Css",27,{"slug":77,"name":78,"articles":79},"typescript","Typescript",11,{"slug":81,"name":82,"articles":83},"tutorial","Tutorial",10,{"slug":85,"name":86,"articles":87},"performance","Performance",9,{"slug":89,"name":90,"articles":91},"grammar","Grammar",6,{"slug":93,"name":94,"articles":95},"react","React",5,{"slug":97,"name":98,"articles":95},"node","Node",{"slug":100,"name":101,"articles":102},"html","Html",4,{"slug":104,"name":105,"articles":102},"programming","Programming",{"id":107,"slug":46,"title":108,"subtitle":52,"excerpt":109,"coverUrl":110,"locale":13,"readingMinutes":111,"publishedAt":112,"viewCount":113,"likeCount":19,"commentCount":19,"author":114,"vertical":119,"topic":120,"tags":123,"_count":128,"playground":130,"body":132,"bodyMd":495,"seo":496,"translationGroupId":498,"series":52,"podcastUrl":52,"verticalId":5,"thread":499,"assessments":501,"translations":504,"quiz":506},"01a03cdb-22ef-748a-b555-8071c7a905e6","NestJS Request Lifecycle Explained (with Cheat Sheet)","A complete guide to the NestJS request lifecycle: the exact order middleware, guards, interceptors, pipes, and filters run, and why it matters.","\u002Fmedia\u002Fcovers\u002Fnestjs-weekly-request-lifecycle.png",13,"2026-08-28T17:01:41.941Z",68,{"id":115,"name":116,"username":117,"avatarUrl":52,"headline":118},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":121,"name":122},"nestjs","Nestjs",[124,125,126,127],{"slug":121,"name":122,"color":52},{"slug":97,"name":98,"color":52},{"slug":77,"name":78,"color":52},{"slug":81,"name":82,"color":52},{"assessments":129},1,{"slug":46,"title":131},"NestJS request lifecycle — interactive playground",{"blocks":133,"version":129},[134,138,141,146,149,158,161,164,167,170,186,189,192,198,202,206,209,212,215,218,223,226,229,232,235,239,242,246,249,252,255,259,262,266,269,273,276,279,282,286,289,292,295,299,302,305,308,312,315,318,321,324,327,330,339,342,351,354,357,360,363,366,369,372,375,378,381,384,387,391,394,397,400,403,406,409,453,457,460,468,471,474,477,480,483,486,489],{"id":135,"html":136,"type":137},"b1","\u003Cp>A request comes into your NestJS API, a \u003Ccode>RolesGuard\u003C\u002Fcode> checks \u003Ccode>request.body.age &gt;= 18\u003C\u002Fcode>, and it denies every single request — even the ones sending \u003Ccode>{ &quot;age&quot;: 25 }\u003C\u002Fcode>. The DTO has \u003Ccode>@Type(() =&gt; Number)\u003C\u002Fcode>. The validation pipe is registered. The code compiles. It still fails.\u003C\u002Fp>","paragraph",{"id":139,"html":140,"type":137},"b2","\u003Cp>The bug isn&#39;t in the guard&#39;s logic. It&#39;s in an assumption about \u003Cem>when\u003C\u002Fem> the guard runs relative to the pipe that was supposed to convert that string into a number.\u003C\u002Fp>",{"id":142,"html":143,"text":144,"type":145,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":147,"html":148,"type":137},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":150,"type":151,"items":152,"ordered":18},"b5","list",[153,154,155,156,157],"State the exact order a request travels through in Nest — middleware, guards, interceptors, pipes, the handler, and filters — and explain \u003Cem>why\u003C\u002Fem> it&#39;s that order","Predict what a guard, interceptor, or pipe can and cannot see at the moment it runs","Trace what happens to the rest of the pipeline when a guard denies or a pipe throws","Put authorization, validation, and cross-cutting logic in the component actually built for each job","Reason about global vs. controller vs. route-level ordering when several guards or interceptors are stacked",{"id":159,"html":160,"text":160,"type":145,"level":31},"b6","Who this is for",{"id":162,"html":163,"type":137},"b7","\u003Cp>You&#39;ve built at least one NestJS controller with a \u003Ccode>@Controller()\u003C\u002Fcode>, a \u003Ccode>@Get()\u003C\u002Fcode> handler, and maybe a \u003Ccode>ValidationPipe\u003C\u002Fcode>. You don&#39;t need to have written a custom guard or interceptor yet — we&#39;ll build both from nothing.\u003C\u002Fp>",{"id":165,"html":166,"type":137},"b8","\u003Cp>This article is written against \u003Cstrong>NestJS 11.x\u003C\u002Fstrong> (verified against the \u003Ccode>@nestjs\u002Fcore\u003C\u002Fcode> release history in August 2026, latest patch \u003Ccode>11.2.3\u003C\u002Fcode>). The lifecycle order below is core framework behavior and has been stable across the 10.x → 11.x line; nothing here is major-version-specific. Where the underlying HTTP adapter (Express vs. Fastify) changes the answer, it&#39;s called out.\u003C\u002Fp>",{"id":168,"html":169,"text":169,"type":145,"level":31},"b9","Table of contents",{"id":171,"type":151,"items":172,"ordered":18},"b10",[173,174,175,176,177,178,179,180,181,182,183,184,185],"\u003Ca href=\"#the-problem-a-guard-that-can-never-win\">The problem: a guard that can never win\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-a-one-way-pipeline-with-two-loop-backs\">The mental model: a one-way pipeline with two loop-backs\u003C\u002Fa>","\u003Ca href=\"#stage-1-middleware\">Stage 1: middleware\u003C\u002Fa>","\u003Ca href=\"#stage-2-guards\">Stage 2: guards\u003C\u002Fa>","\u003Ca href=\"#stage-3-interceptors-the-before-half\">Stage 3: interceptors, the &quot;before&quot; half\u003C\u002Fa>","\u003Ca href=\"#stage-4-pipes\">Stage 4: pipes\u003C\u002Fa>","\u003Ca href=\"#stage-5-the-handler\">Stage 5: the handler\u003C\u002Fa>","\u003Ca href=\"#stage-6-interceptors-the-after-half\">Stage 6: interceptors, the &quot;after&quot; half\u003C\u002Fa>","\u003Ca href=\"#stage-7-exception-filters\">Stage 7: exception filters\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices-which-component-for-what\">Best practices: which component, for what\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>",{"id":187,"html":188,"text":188,"type":145,"level":31},"b11","The problem: a guard that can never win",{"id":190,"html":191,"type":137},"b12","\u003Cp>Here&#39;s the controller and the guard that&#39;s supposedly protecting it:\u003C\u002Fp>",{"id":193,"code":194,"type":195,"language":196,"highlight":197},"b13","\u002F\u002F create-user.dto.ts\nimport { Type } from 'class-transformer';\nimport { IsInt, Min } from 'class-validator';\n\nexport class CreateUserDto {\n  @Type(() => Number)\n  @IsInt()\n  @Min(0)\n  age: number;\n}","code","ts",[],{"id":199,"code":200,"type":195,"language":196,"highlight":201},"b14","\u002F\u002F adult-only.guard.ts\nimport { CanActivate, ExecutionContext, Injectable } from '@nestjs\u002Fcommon';\n\n@Injectable()\nexport class AdultOnlyGuard implements CanActivate {\n  canActivate(context: ExecutionContext): boolean {\n    const req = context.switchToHttp().getRequest();\n    \u002F\u002F \"age is a number by now, the DTO says so\" — is it, though?\n    return req.body.age >= 18;\n  }\n}",[],{"id":203,"code":204,"type":195,"language":196,"highlight":205},"b15","@Controller('users')\nexport class UsersController {\n  @UseGuards(AdultOnlyGuard)\n  @Post()\n  create(@Body() dto: CreateUserDto) {\n    return { ok: true, age: dto.age };\n  }\n}",[],{"id":207,"html":208,"type":137},"b16","\u003Cp>Send \u003Ccode>{ &quot;age&quot;: 25 }\u003C\u002Fcode> as JSON. \u003Ccode>req.body.age\u003C\u002Fcode> at this point is the string \u003Ccode>&quot;25&quot;\u003C\u002Fcode>, because the body parser only produces JSON-shaped values — strings, numbers, booleans, objects, arrays — from the wire, and \u003Ccode>&quot;25&quot;\u003C\u002Fcode> was serialized as a number, so this particular field actually \u003Cem>does\u003C\u002Fem> arrive as a JS number here. Change the client to send it as a query param, or send \u003Ccode>{ &quot;age&quot;: &quot;25&quot; }\u003C\u002Fcode> from a form post, and \u003Ccode>req.body.age\u003C\u002Fcode> is a string. \u003Ccode>&quot;25&quot; &gt;= 18\u003C\u002Fcode> still happens to be \u003Ccode>true\u003C\u002Fcode> because of JS coercion — but \u003Ccode>&quot;abc&quot; &gt;= 18\u003C\u002Fcode> is \u003Ccode>false\u003C\u002Fcode> and so is \u003Ccode>&quot;17&quot; &gt;= 18\u003C\u002Fcode> reversed to \u003Ccode>&quot;-5&quot; &gt;= 18\u003C\u002Fcode>... the guard is doing string\u002Fnumber comparisons on data nobody has validated or transformed yet, and it will eventually compare the wrong thing and let an invalid request through, or block a valid one.\u003C\u002Fp>",{"id":210,"html":211,"type":137},"b17","\u003Cp>The DTO&#39;s \u003Ccode>@Type(() =&gt; Number)\u003C\u002Fcode> transform, and the \u003Ccode>@IsInt()\u003C\u002Fcode>\u002F\u003Ccode>@Min(0)\u003C\u002Fcode> checks, run \u003Cstrong>inside the \u003Ccode>ValidationPipe\u003C\u002Fcode>\u003C\u002Fstrong> — and pipes run \u003Cem>after\u003C\u002Fem> guards. The guard the developer wrote reads \u003Ccode>req.body\u003C\u002Fcode> raw, off the request object, before any pipe has touched it. It was never protected by the DTO at all; it just happened to work for the one payload shape someone tested.\u003C\u002Fp>",{"id":213,"html":214,"text":214,"type":145,"level":31},"b18","The mental model: a one-way pipeline with two loop-backs",{"id":216,"html":217,"type":137},"b19","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a request moves through Nest&#39;s components in one fixed, linear order — but two of those components, guards and pipes, are gatekeepers that can end the trip early, and one component, the interceptor, wraps the \u003Cem>rest\u003C\u002Fem> of the pipeline rather than sitting at one point in it.\u003C\u002Fp>",{"id":219,"code":220,"type":195,"language":221,"highlight":222},"b20","Middleware → Guards → Interceptors (before) → Pipes → Handler\n                                                          │\n                                              Interceptors (after) ← ┘\n                                                          │\n                                                     Response\n        (any exception, from any stage) → Exception filters → Response","plain",[],{"id":224,"html":225,"type":137},"b21","\u003Cp>Every stage after the current one is conditional on the current one succeeding. A guard returning \u003Ccode>false\u003C\u002Fcode>, or any component throwing, skips straight to exception filters — nothing downstream of that point runs. That single rule explains every &quot;why didn&#39;t my interceptor&#39;s logging line fire&quot; question you&#39;ll ever have about Nest.\u003C\u002Fp>",{"id":227,"html":228,"type":137},"b22","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> don&#39;t memorize &quot;guards before pipes&quot; as trivia. Memorize \u003Cem>why\u003C\u002Fem>: guards answer &quot;should this request happen at all,&quot; which has to be decided before Nest spends any effort parsing or transforming the payload for a request that might get rejected outright.\u003C\u002Fp>",{"id":230,"html":231,"text":231,"type":145,"level":31},"b23","Stage 1: middleware",{"id":233,"html":234,"type":137},"b24","\u003Cp>Middleware is the same Express\u002FConnect-style middleware you&#39;d write for any Node HTTP server: \u003Ccode>(req, res, next) =&gt; void\u003C\u002Fcode>. It runs first, before Nest&#39;s own routing has resolved anything, and it has no idea what controller or handler is about to run.\u003C\u002Fp>",{"id":236,"code":237,"type":195,"language":196,"highlight":238},"b25","@Injectable()\nexport class RequestIdMiddleware implements NestMiddleware {\n  use(req: Request, res: Response, next: NextFunction) {\n    req['requestId'] = req.headers['x-request-id'] ?? randomUUID();\n    next();\n  }\n}",[],{"id":240,"html":241,"type":137},"b26","\u003Cp>Wire it in a module:\u003C\u002Fp>",{"id":243,"code":244,"type":195,"language":196,"highlight":245},"b27","export class AppModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer.apply(RequestIdMiddleware).forRoutes('*');\n  }\n}",[],{"id":247,"html":248,"type":137},"b28","\u003Cp>Use middleware for things that don&#39;t need Nest&#39;s execution context at all: request IDs, raw body logging, \u003Ccode>helmet\u003C\u002Fcode>, \u003Ccode>compression\u003C\u002Fcode>, cookie parsing. \u003Cstrong>With the Fastify adapter\u003C\u002Fstrong>, plain Express-style middleware isn&#39;t a drop-in — Fastify has its own plugin\u002Fhook system, and \u003Ccode>@nestjs\u002Fplatform-fastify\u003C\u002Fcode> bridges \u003Cem>some\u003C\u002Fem> middleware but not arbitrary Express middleware written against \u003Ccode>req\u003C\u002Fcode>\u002F\u003Ccode>res\u003C\u002Fcode>\u002F\u003Ccode>next\u003C\u002Fcode>. If you&#39;re on Fastify, check a middleware package&#39;s Fastify compatibility before assuming it works unchanged.\u003C\u002Fp>",{"id":250,"html":251,"text":251,"type":145,"level":31},"b29","Stage 2: guards",{"id":253,"html":254,"type":137},"b30","\u003Cp>A guard implements \u003Ccode>canActivate()\u003C\u002Fcode> and returns (or resolves\u002Femits) a boolean. \u003Ccode>true\u003C\u002Fcode> lets the request continue; \u003Ccode>false\u003C\u002Fcode> — or a thrown exception — stops it immediately, before anything else in the pipeline runs.\u003C\u002Fp>",{"id":256,"code":257,"type":195,"language":196,"highlight":258},"b31","@Injectable()\nexport class RolesGuard implements CanActivate {\n  constructor(private reflector: Reflector) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    const required = this.reflector.get\u003Cstring[]>('roles', context.getHandler());\n    if (!required) return true;\n    const { user } = context.switchToHttp().getRequest();\n    return required.some((role) => user?.roles?.includes(role));\n  }\n}",[],{"id":260,"html":261,"type":137},"b32","\u003Cp>Guards are where authorization belongs — &quot;is this caller \u003Cem>allowed\u003C\u002Fem> to do this&quot; — because that question should be answered before Nest does any further work, and because a guard is exactly the layer that has access to the identity a prior guard (or middleware) attached to the request, via constructor-injected providers.\u003C\u002Fp>",{"id":263,"html":264,"text":265,"type":145,"level":31},"b33","Stage 3: interceptors, the &quot;before&quot; half","Stage 3: interceptors, the \"before\" half",{"id":267,"html":268,"type":137},"b34","\u003Cp>An interceptor implements \u003Ccode>intercept(context, next)\u003C\u002Fcode>, where \u003Ccode>next.handle()\u003C\u002Fcode> returns an RxJS \u003Ccode>Observable\u003C\u002Fcode> representing &quot;the rest of the pipeline&quot; — pipes, the handler, and everything after. Code written \u003Cem>before\u003C\u002Fem> the \u003Ccode>next.handle()\u003C\u002Fcode> call runs on the way in; code chained onto the returned observable (via \u003Ccode>.pipe(tap(...))\u003C\u002Fcode>, \u003Ccode>.pipe(map(...))\u003C\u002Fcode>, etc.) runs on the way out.\u003C\u002Fp>",{"id":270,"code":271,"type":195,"language":196,"highlight":272},"b35","@Injectable()\nexport class TimingInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    const start = Date.now();               \u002F\u002F before: runs now, pre-pipes\n    return next.handle().pipe(\n      tap(() => console.log(`took ${Date.now() - start}ms`)), \u002F\u002F after: post-handler\n    );\n  }\n}",[],{"id":274,"html":275,"type":137},"b36","\u003Cp>This is why interceptors are drawn twice in the pipeline diagram: they&#39;re a single component whose code straddles both sides of everything from pipes through the handler.\u003C\u002Fp>",{"id":277,"html":278,"text":278,"type":145,"level":31},"b37","Stage 4: pipes",{"id":280,"html":281,"type":137},"b38","\u003Cp>Pipes implement \u003Ccode>transform(value, metadata)\u003C\u002Fcode> and run against individual arguments — \u003Ccode>@Body()\u003C\u002Fcode>, \u003Ccode>@Param()\u003C\u002Fcode>, \u003Ccode>@Query()\u003C\u002Fcode> — immediately before Nest calls the handler with them. This is where \u003Ccode>ValidationPipe\u003C\u002Fcode> lives, and it&#39;s the \u003Cem>only\u003C\u002Fem> stage in the whole lifecycle where \u003Ccode>CreateUserDto\u003C\u002Fcode>&#39;s decorators actually execute.\u003C\u002Fp>",{"id":283,"code":284,"type":195,"language":196,"highlight":285},"b39","@Injectable()\nexport class ParseAgePipe implements PipeTransform {\n  transform(value: unknown): number {\n    const n = Number(value);\n    if (Number.isNaN(n)) throw new BadRequestException('age must be a number');\n    return n;\n  }\n}",[],{"id":287,"html":288,"type":137},"b40","\u003Cp>This is the fix for the opening bug: the age check belongs in a pipe (or, more precisely, validation belongs in a pipe; the \u003Cem>18-or-over\u003C\u002Fem> rule is a business\u002Fauthorization decision and belongs in a guard — but a guard that reads the \u003Cstrong>already-validated\u003C\u002Fstrong> DTO, which pipes running earlier in a \u003Cem>different\u003C\u002Fem> request never happened for this guard&#39;s read of raw \u003Ccode>req.body\u003C\u002Fcode>. Since pipes run after guards, an &quot;is this person old enough&quot; check can&#39;t be a guard reading the DTO shape at all — it has to either be middleware\u002Fguard logic that does its own parsing, or, more idiomatically, be enforced as validation (\u003Ccode>@Min(18)\u003C\u002Fcode> on the DTO) plus a normal \u003Ccode>403\u003C\u002Fcode> path, not a guard.\u003C\u002Fp>",{"id":290,"html":291,"text":291,"type":145,"level":31},"b41","Stage 5: the handler",{"id":293,"html":294,"type":137},"b42","\u003Cp>The controller method itself. By the time it runs, every guard passed, every &quot;before&quot; interceptor ran, and every argument has been through its pipes — \u003Ccode>dto\u003C\u002Fcode> in \u003Ccode>create(@Body() dto: CreateUserDto)\u003C\u002Fcode> is a real \u003Ccode>CreateUserDto\u003C\u002Fcode> instance with \u003Ccode>age\u003C\u002Fcode> as an actual \u003Ccode>number\u003C\u002Fcode>. This is the one stage that&#39;s always exactly what it looks like.\u003C\u002Fp>",{"id":296,"html":297,"text":298,"type":145,"level":31},"b43","Stage 6: interceptors, the &quot;after&quot; half","Stage 6: interceptors, the \"after\" half",{"id":300,"html":301,"type":137},"b44","\u003Cp>Once the handler returns (or its promise\u002Fobservable resolves), control passes back through every interceptor that wrapped the call, in reverse order, via the operators chained onto \u003Ccode>next.handle()\u003C\u002Fcode>. This is where response transformation, caching the result, and after-the-fact logging belong — a \u003Ccode>ClassSerializerInterceptor\u003C\u002Fcode> stripping \u003Ccode>@Exclude()\u003C\u002Fcode> fields, a cache interceptor storing the result, the \u003Ccode>tap()\u003C\u002Fcode> above logging duration.\u003C\u002Fp>",{"id":303,"html":304,"text":304,"type":145,"level":31},"b45","Stage 7: exception filters",{"id":306,"html":307,"type":137},"b46","\u003Cp>If \u003Cem>anything\u003C\u002Fem> in the pipeline throws — a guard, an interceptor, a pipe, or the handler — Nest stops walking forward and looks for an exception filter instead. Filters resolve \u003Cstrong>bottom-up\u003C\u002Fstrong>: a filter bound to the specific route handler runs before one bound to the controller, which runs before a global filter.\u003C\u002Fp>",{"id":309,"code":310,"type":195,"language":196,"highlight":311},"b47","@Catch(HttpException)\nexport class HttpExceptionFilter implements ExceptionFilter {\n  catch(exception: HttpException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const res = ctx.getResponse\u003CResponse>();\n    res.status(exception.getStatus()).json({\n      statusCode: exception.getStatus(),\n      message: exception.message,\n      path: ctx.getRequest().url,\n    });\n  }\n}",[],{"id":313,"html":314,"type":137},"b48","\u003C!-- playground:start -->",{"id":316,"html":317,"text":317,"type":145,"level":31},"b49","🎮 Try it yourself",{"id":319,"html":320,"type":137},"b50","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-request-lifecycle\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":322,"html":323,"type":137},"b51","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":325,"html":326,"type":137},"b52","\u003C!-- playground:end -->",{"id":328,"html":329,"text":329,"type":145,"level":31},"b53","Edge cases and gotchas",{"id":331,"type":151,"items":332,"ordered":18},"b54",[333,334,335,336,337,338],"\u003Cstrong>A denied guard skips interceptors entirely — both halves.\u003C\u002Fstrong> If \u003Ccode>canActivate()\u003C\u002Fcode> returns \u003Ccode>false\u003C\u002Fcode>, Nest throws a \u003Ccode>ForbiddenException\u003C\u002Fcode> before any interceptor&#39;s &quot;before&quot; code runs. Timing and logging interceptors that assume they always fire will silently miss every rejected request.","\u003Cstrong>A pipe that throws skips the &quot;after&quot; interceptor logic, not the &quot;before&quot; half.\u003C\u002Fstrong> The interceptor&#39;s pre-\u003Ccode>next.handle()\u003C\u002Fcode> code already ran; the exception happens \u003Cem>inside\u003C\u002Fem> the observable \u003Ccode>next.handle()\u003C\u002Fcode> returns, so a \u003Ccode>.pipe(tap(...))\u003C\u002Fcode> chained for success won&#39;t fire — you need \u003Ccode>catchError\u003C\u002Fcode> if you want interceptor code to run on the error path too.","\u003Cstrong>Multiple guards\u002Finterceptors run in registration order\u003C\u002Fstrong>, and at three possible scopes — global (\u003Ccode>app.useGlobalGuards()\u003C\u002Fcode>), controller (\u003Ccode>@UseGuards()\u003C\u002Fcode> on the class), and route (\u003Ccode>@UseGuards()\u003C\u002Fcode> on the method) — with global running first, then controller, then route. The same ordering applies to interceptors and to pipes.","\u003Cstrong>A guard cannot see a validated DTO\u003C\u002Fstrong>, ever, for the current request — pipes haven&#39;t run. If a guard needs shape-checked data, it must parse defensively itself; it can&#39;t rely on a pipe that hasn&#39;t executed yet.","\u003Cstrong>WebSocket gateways and microservice transports reuse guards, interceptors, pipes, and filters\u003C\u002Fstrong>, but there is no middleware stage — middleware is an HTTP-adapter concept (Express\u002FFastify), and gateways don&#39;t go through it.","\u003Cstrong>\u003Ccode>app.useGlobalPipes()\u003C\u002Fcode> registers \u003Cem>after\u003C\u002Fem> module-level guards conceptually apply per-request\u003C\u002Fstrong> — the stage order (guards, then pipes) holds regardless of whether a given guard\u002Fpipe is global, controller-scoped, or route-scoped.",{"id":340,"html":341,"text":341,"type":145,"level":31},"b55","Best practices: which component, for what",{"id":343,"type":151,"items":344,"ordered":18},"b56",[345,346,347,348,349,350],"\u003Cstrong>Middleware\u003C\u002Fstrong> — request-level, framework-agnostic concerns with no need for Nest&#39;s execution context: IDs, raw logging, compression, security headers.","\u003Cstrong>Guards\u003C\u002Fstrong> — authorization: &quot;can this identity do this,&quot; using data already attached by middleware or a prior guard (like \u003Ccode>request.user\u003C\u002Fcode> from a passport strategy).","\u003Cstrong>Interceptors\u003C\u002Fstrong> — cross-cutting concerns that wrap a call: timing, logging both sides, response shaping, caching, retry.","\u003Cstrong>Pipes\u003C\u002Fstrong> — per-argument validation and transformation: turning a raw \u003Ccode>body\u003C\u002Fcode>\u002F\u003Ccode>param\u003C\u002Fcode>\u002F\u003Ccode>query\u003C\u002Fcode> into a typed, checked value.","\u003Cstrong>Exception filters\u003C\u002Fstrong> — turning any thrown error, from any stage, into a consistent response shape.","Never put validation logic in a guard, and never put authorization logic in a pipe — each answers a question the other one can&#39;t see the data for yet.",{"id":352,"html":353,"type":137},"b57","\u003C!-- quiz:start -->",{"id":355,"html":356,"text":356,"type":145,"level":31},"b58","🧠 Test yourself",{"id":358,"html":359,"type":137},"b59","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-request-lifecycle\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":361,"html":362,"type":137},"b60","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":364,"html":365,"type":137},"b61","\u003C!-- quiz:end -->",{"id":367,"html":368,"text":368,"type":145,"level":31},"b62","FAQ",{"id":370,"html":371,"text":371,"type":145,"level":43},"b63","Do guards run before or after middleware in NestJS?",{"id":373,"html":374,"type":137},"b64","\u003Cp>After. The order is middleware, then guards, then interceptors, then pipes, then the handler.\u003C\u002Fp>",{"id":376,"html":377,"text":377,"type":145,"level":43},"b65","Can a guard read a validated, transformed DTO?",{"id":379,"html":380,"type":137},"b66","\u003Cp>No. Pipes — including \u003Ccode>ValidationPipe\u003C\u002Fcode>, which applies a DTO&#39;s \u003Ccode>class-validator\u003C\u002Fcode>\u002F\u003Ccode>class-transformer\u003C\u002Fcode> decorators — run \u003Cem>after\u003C\u002Fem> guards. A guard only ever sees the raw request.\u003C\u002Fp>",{"id":382,"html":383,"text":383,"type":145,"level":43},"b67","What happens to interceptors if a guard denies the request?",{"id":385,"html":386,"type":137},"b68","\u003Cp>Nothing runs. A denied guard (\u003Ccode>canActivate()\u003C\u002Fcode> returning \u003Ccode>false\u003C\u002Fcode>, or throwing) skips both the &quot;before&quot; and &quot;after&quot; halves of every interceptor and goes straight to exception filters.\u003C\u002Fp>",{"id":388,"html":389,"text":390,"type":145,"level":43},"b69","Does an interceptor&#39;s &quot;after&quot; logic run if the handler throws?","Does an interceptor's \"after\" logic run if the handler throws?",{"id":392,"html":393,"type":137},"b70","\u003Cp>Only if the interceptor explicitly handles the error, e.g. with RxJS&#39;s \u003Ccode>catchError\u003C\u002Fcode>. A plain \u003Ccode>tap()\u003C\u002Fcode> chained for the success path is skipped when the observable errors instead of emitting a value.\u003C\u002Fp>",{"id":395,"html":396,"text":396,"type":145,"level":43},"b71","Which exception filter runs when several are registered?",{"id":398,"html":399,"type":137},"b72","\u003Cp>The most specific one: a filter bound to the route handler runs before a controller-level filter, which runs before a global filter — filters resolve from the bottom up, unlike guards, interceptors, and pipes, which resolve global-first.\u003C\u002Fp>",{"id":401,"html":402,"text":402,"type":145,"level":43},"b73","Does the request lifecycle differ between Express and Fastify?",{"id":404,"html":405,"type":137},"b74","\u003Cp>The guard\u002Finterceptor\u002Fpipe\u002Ffilter order is identical — that&#39;s Nest&#39;s own execution model, independent of the HTTP adapter. Middleware is where they diverge: arbitrary Express-style middleware isn&#39;t automatically Fastify-compatible, since Fastify has its own hook and plugin system.\u003C\u002Fp>",{"id":407,"html":408,"text":408,"type":145,"level":31},"b75","Cheat sheet",{"id":410,"head":411,"rows":416,"type":452},"b76",[412,413,414,415],"Stage","Runs","Can stop the request","Typical use",[417,422,427,432,437,442,447],[418,419,420,421],"Middleware","First, before routing","\u003Ccode>next()\u003C\u002Fcode> not called","request IDs, compression, headers",[423,424,425,426],"Guards","After middleware","\u003Ccode>canActivate()\u003C\u002Fcode> returns \u003Ccode>false\u003C\u002Fcode>\u002Fthrows","authorization",[428,429,430,431],"Interceptors (before)","After guards, before pipes","only by throwing","start a timer, attach context",[433,434,435,436],"Pipes","After interceptors (before), before the handler","throws on invalid input","validation, transformation",[438,439,440,441],"Handler","After every argument is piped","throws","your actual business logic",[443,444,445,446],"Interceptors (after)","After the handler resolves","only affects success path unless using \u003Ccode>catchError\u003C\u002Fcode>","logging, response shaping, caching",[448,449,450,451],"Exception filters","On any thrown error, from any stage","—","consistent error responses","table",{"id":454,"code":455,"type":195,"language":196,"highlight":456},"b77","\u002F\u002F The order, as code you'd actually register:\napp.use(RequestIdMiddleware);                 \u002F\u002F 1. middleware\napp.useGlobalGuards(new RolesGuard(reflector)); \u002F\u002F 2. guards\napp.useGlobalInterceptors(new TimingInterceptor()); \u002F\u002F 3 & 6. before + after\napp.useGlobalPipes(new ValidationPipe());     \u002F\u002F 4. pipes\n\u002F\u002F 5. your handler runs here\napp.useGlobalFilters(new HttpExceptionFilter()); \u002F\u002F 7. on any throw, from any stage",[],{"id":458,"html":459,"text":459,"type":145,"level":31},"b78","Key takeaways",{"id":461,"type":151,"items":462,"ordered":18},"b79",[463,464,465,466,467],"The order is fixed: \u003Cstrong>middleware → guards → interceptors (before) → pipes → handler → interceptors (after) → exception filters.\u003C\u002Fstrong>","Guards decide \u003Cem>if\u003C\u002Fem> a request proceeds; they never see data a pipe would have validated or transformed, because pipes run later.","Interceptors wrap the rest of the pipeline — their &quot;before&quot; and &quot;after&quot; halves are one component, not two, and the &quot;after&quot; half only fires on success unless you handle errors explicitly.","Any exception, from any stage, exits straight to exception filters, which resolve most-specific first.","Put each concern in the component built for it — authorization in guards, validation in pipes — and lifecycle bugs like the opening one stop happening.",{"id":469,"html":470,"text":470,"type":145,"level":31},"b80","Back to that guard",{"id":472,"html":473,"type":137},"b81","\u003Cp>The \u003Ccode>AdultOnlyGuard\u003C\u002Fcode> wasn&#39;t wrong about the rule — eighteen-or-over is a real requirement. It was wrong about \u003Cem>when\u003C\u002Fem> it was running: before anything had touched, checked, or converted the value it was reading. Move that rule into the DTO (\u003Ccode>@Min(18)\u003C\u002Fcode>) or into logic that reads the same raw shape a guard actually sees, and it stops being a guard that occasionally agrees with the data by accident.\u003C\u002Fp>",{"id":475,"html":476,"type":137},"b82","\u003Cp>Next Friday&#39;s episode goes one level deeper: what actually happens inside the DI container when two of your providers depend on each other.\u003C\u002Fp>",{"id":478,"html":479,"type":137},"b83","\u003Cp>Where has the lifecycle order bitten you — a guard, an interceptor, or a filter that fired (or didn&#39;t) when you least expected?\u003C\u002Fp>",{"id":481,"type":482},"b84","divider",{"id":484,"html":485,"type":137},"b85","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":487,"html":488,"type":137},"b86","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":490,"type":151,"items":491,"ordered":18},"b87",[492,493,494],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","A request comes into your NestJS API, a `RolesGuard` checks `request.body.age >= 18`, and it denies every single request — even the ones sending `{ \"age\": 25 }`. The DTO has `@Type(() => Number)`. The validation pipe is registered. The code compiles. It still fails.\n\nThe bug isn't in the guard's logic. It's in an assumption about *when* the guard runs relative to the pipe that was supposed to convert that string into a number.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- State the exact order a request travels through in Nest — middleware, guards, interceptors, pipes, the handler, and filters — and explain *why* it's that order\n- Predict what a guard, interceptor, or pipe can and cannot see at the moment it runs\n- Trace what happens to the rest of the pipeline when a guard denies or a pipe throws\n- Put authorization, validation, and cross-cutting logic in the component actually built for each job\n- Reason about global vs. controller vs. route-level ordering when several guards or interceptors are stacked\n\n## Who this is for\n\nYou've built at least one NestJS controller with a `@Controller()`, a `@Get()` handler, and maybe a `ValidationPipe`. You don't need to have written a custom guard or interceptor yet — we'll build both from nothing.\n\nThis article is written against **NestJS 11.x** (verified against the `@nestjs\u002Fcore` release history in August 2026, latest patch `11.2.3`). The lifecycle order below is core framework behavior and has been stable across the 10.x → 11.x line; nothing here is major-version-specific. Where the underlying HTTP adapter (Express vs. Fastify) changes the answer, it's called out.\n\n## Table of contents\n\n- [The problem: a guard that can never win](#the-problem-a-guard-that-can-never-win)\n- [The mental model: a one-way pipeline with two loop-backs](#the-mental-model-a-one-way-pipeline-with-two-loop-backs)\n- [Stage 1: middleware](#stage-1-middleware)\n- [Stage 2: guards](#stage-2-guards)\n- [Stage 3: interceptors, the \"before\" half](#stage-3-interceptors-the-before-half)\n- [Stage 4: pipes](#stage-4-pipes)\n- [Stage 5: the handler](#stage-5-the-handler)\n- [Stage 6: interceptors, the \"after\" half](#stage-6-interceptors-the-after-half)\n- [Stage 7: exception filters](#stage-7-exception-filters)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices: which component, for what](#best-practices-which-component-for-what)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n\n## The problem: a guard that can never win\n\nHere's the controller and the guard that's supposedly protecting it:\n\n```ts\n\u002F\u002F create-user.dto.ts\nimport { Type } from 'class-transformer';\nimport { IsInt, Min } from 'class-validator';\n\nexport class CreateUserDto {\n  @Type(() => Number)\n  @IsInt()\n  @Min(0)\n  age: number;\n}\n```\n\n```ts\n\u002F\u002F adult-only.guard.ts\nimport { CanActivate, ExecutionContext, Injectable } from '@nestjs\u002Fcommon';\n\n@Injectable()\nexport class AdultOnlyGuard implements CanActivate {\n  canActivate(context: ExecutionContext): boolean {\n    const req = context.switchToHttp().getRequest();\n    \u002F\u002F \"age is a number by now, the DTO says so\" — is it, though?\n    return req.body.age >= 18;\n  }\n}\n```\n\n```ts\n@Controller('users')\nexport class UsersController {\n  @UseGuards(AdultOnlyGuard)\n  @Post()\n  create(@Body() dto: CreateUserDto) {\n    return { ok: true, age: dto.age };\n  }\n}\n```\n\nSend `{ \"age\": 25 }` as JSON. `req.body.age` at this point is the string `\"25\"`, because the body parser only produces JSON-shaped values — strings, numbers, booleans, objects, arrays — from the wire, and `\"25\"` was serialized as a number, so this particular field actually *does* arrive as a JS number here. Change the client to send it as a query param, or send `{ \"age\": \"25\" }` from a form post, and `req.body.age` is a string. `\"25\" >= 18` still happens to be `true` because of JS coercion — but `\"abc\" >= 18` is `false` and so is `\"17\" >= 18` reversed to `\"-5\" >= 18`... the guard is doing string\u002Fnumber comparisons on data nobody has validated or transformed yet, and it will eventually compare the wrong thing and let an invalid request through, or block a valid one.\n\nThe DTO's `@Type(() => Number)` transform, and the `@IsInt()`\u002F`@Min(0)` checks, run **inside the `ValidationPipe`** — and pipes run *after* guards. The guard the developer wrote reads `req.body` raw, off the request object, before any pipe has touched it. It was never protected by the DTO at all; it just happened to work for the one payload shape someone tested.\n\n## The mental model: a one-way pipeline with two loop-backs\n\n**The mental model:** a request moves through Nest's components in one fixed, linear order — but two of those components, guards and pipes, are gatekeepers that can end the trip early, and one component, the interceptor, wraps the *rest* of the pipeline rather than sitting at one point in it.\n\n```\nMiddleware → Guards → Interceptors (before) → Pipes → Handler\n                                                          │\n                                              Interceptors (after) ← ┘\n                                                          │\n                                                     Response\n        (any exception, from any stage) → Exception filters → Response\n```\n\nEvery stage after the current one is conditional on the current one succeeding. A guard returning `false`, or any component throwing, skips straight to exception filters — nothing downstream of that point runs. That single rule explains every \"why didn't my interceptor's logging line fire\" question you'll ever have about Nest.\n\n**Key concept:** don't memorize \"guards before pipes\" as trivia. Memorize *why*: guards answer \"should this request happen at all,\" which has to be decided before Nest spends any effort parsing or transforming the payload for a request that might get rejected outright.\n\n## Stage 1: middleware\n\nMiddleware is the same Express\u002FConnect-style middleware you'd write for any Node HTTP server: `(req, res, next) => void`. It runs first, before Nest's own routing has resolved anything, and it has no idea what controller or handler is about to run.\n\n```ts\n@Injectable()\nexport class RequestIdMiddleware implements NestMiddleware {\n  use(req: Request, res: Response, next: NextFunction) {\n    req['requestId'] = req.headers['x-request-id'] ?? randomUUID();\n    next();\n  }\n}\n```\n\nWire it in a module:\n\n```ts\nexport class AppModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer.apply(RequestIdMiddleware).forRoutes('*');\n  }\n}\n```\n\nUse middleware for things that don't need Nest's execution context at all: request IDs, raw body logging, `helmet`, `compression`, cookie parsing. **With the Fastify adapter**, plain Express-style middleware isn't a drop-in — Fastify has its own plugin\u002Fhook system, and `@nestjs\u002Fplatform-fastify` bridges *some* middleware but not arbitrary Express middleware written against `req`\u002F`res`\u002F`next`. If you're on Fastify, check a middleware package's Fastify compatibility before assuming it works unchanged.\n\n## Stage 2: guards\n\nA guard implements `canActivate()` and returns (or resolves\u002Femits) a boolean. `true` lets the request continue; `false` — or a thrown exception — stops it immediately, before anything else in the pipeline runs.\n\n```ts\n@Injectable()\nexport class RolesGuard implements CanActivate {\n  constructor(private reflector: Reflector) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    const required = this.reflector.get\u003Cstring[]>('roles', context.getHandler());\n    if (!required) return true;\n    const { user } = context.switchToHttp().getRequest();\n    return required.some((role) => user?.roles?.includes(role));\n  }\n}\n```\n\nGuards are where authorization belongs — \"is this caller *allowed* to do this\" — because that question should be answered before Nest does any further work, and because a guard is exactly the layer that has access to the identity a prior guard (or middleware) attached to the request, via constructor-injected providers.\n\n## Stage 3: interceptors, the \"before\" half\n\nAn interceptor implements `intercept(context, next)`, where `next.handle()` returns an RxJS `Observable` representing \"the rest of the pipeline\" — pipes, the handler, and everything after. Code written *before* the `next.handle()` call runs on the way in; code chained onto the returned observable (via `.pipe(tap(...))`, `.pipe(map(...))`, etc.) runs on the way out.\n\n```ts\n@Injectable()\nexport class TimingInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    const start = Date.now();               \u002F\u002F before: runs now, pre-pipes\n    return next.handle().pipe(\n      tap(() => console.log(`took ${Date.now() - start}ms`)), \u002F\u002F after: post-handler\n    );\n  }\n}\n```\n\nThis is why interceptors are drawn twice in the pipeline diagram: they're a single component whose code straddles both sides of everything from pipes through the handler.\n\n## Stage 4: pipes\n\nPipes implement `transform(value, metadata)` and run against individual arguments — `@Body()`, `@Param()`, `@Query()` — immediately before Nest calls the handler with them. This is where `ValidationPipe` lives, and it's the *only* stage in the whole lifecycle where `CreateUserDto`'s decorators actually execute.\n\n```ts\n@Injectable()\nexport class ParseAgePipe implements PipeTransform {\n  transform(value: unknown): number {\n    const n = Number(value);\n    if (Number.isNaN(n)) throw new BadRequestException('age must be a number');\n    return n;\n  }\n}\n```\n\nThis is the fix for the opening bug: the age check belongs in a pipe (or, more precisely, validation belongs in a pipe; the *18-or-over* rule is a business\u002Fauthorization decision and belongs in a guard — but a guard that reads the **already-validated** DTO, which pipes running earlier in a *different* request never happened for this guard's read of raw `req.body`. Since pipes run after guards, an \"is this person old enough\" check can't be a guard reading the DTO shape at all — it has to either be middleware\u002Fguard logic that does its own parsing, or, more idiomatically, be enforced as validation (`@Min(18)` on the DTO) plus a normal `403` path, not a guard.\n\n## Stage 5: the handler\n\nThe controller method itself. By the time it runs, every guard passed, every \"before\" interceptor ran, and every argument has been through its pipes — `dto` in `create(@Body() dto: CreateUserDto)` is a real `CreateUserDto` instance with `age` as an actual `number`. This is the one stage that's always exactly what it looks like.\n\n## Stage 6: interceptors, the \"after\" half\n\nOnce the handler returns (or its promise\u002Fobservable resolves), control passes back through every interceptor that wrapped the call, in reverse order, via the operators chained onto `next.handle()`. This is where response transformation, caching the result, and after-the-fact logging belong — a `ClassSerializerInterceptor` stripping `@Exclude()` fields, a cache interceptor storing the result, the `tap()` above logging duration.\n\n## Stage 7: exception filters\n\nIf *anything* in the pipeline throws — a guard, an interceptor, a pipe, or the handler — Nest stops walking forward and looks for an exception filter instead. Filters resolve **bottom-up**: a filter bound to the specific route handler runs before one bound to the controller, which runs before a global filter.\n\n```ts\n@Catch(HttpException)\nexport class HttpExceptionFilter implements ExceptionFilter {\n  catch(exception: HttpException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const res = ctx.getResponse\u003CResponse>();\n    res.status(exception.getStatus()).json({\n      statusCode: exception.getStatus(),\n      message: exception.message,\n      path: ctx.getRequest().url,\n    });\n  }\n}\n```\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-request-lifecycle\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Edge cases and gotchas\n\n- **A denied guard skips interceptors entirely — both halves.** If `canActivate()` returns `false`, Nest throws a `ForbiddenException` before any interceptor's \"before\" code runs. Timing and logging interceptors that assume they always fire will silently miss every rejected request.\n- **A pipe that throws skips the \"after\" interceptor logic, not the \"before\" half.** The interceptor's pre-`next.handle()` code already ran; the exception happens *inside* the observable `next.handle()` returns, so a `.pipe(tap(...))` chained for success won't fire — you need `catchError` if you want interceptor code to run on the error path too.\n- **Multiple guards\u002Finterceptors run in registration order**, and at three possible scopes — global (`app.useGlobalGuards()`), controller (`@UseGuards()` on the class), and route (`@UseGuards()` on the method) — with global running first, then controller, then route. The same ordering applies to interceptors and to pipes.\n- **A guard cannot see a validated DTO**, ever, for the current request — pipes haven't run. If a guard needs shape-checked data, it must parse defensively itself; it can't rely on a pipe that hasn't executed yet.\n- **WebSocket gateways and microservice transports reuse guards, interceptors, pipes, and filters**, but there is no middleware stage — middleware is an HTTP-adapter concept (Express\u002FFastify), and gateways don't go through it.\n- **`app.useGlobalPipes()` registers *after* module-level guards conceptually apply per-request** — the stage order (guards, then pipes) holds regardless of whether a given guard\u002Fpipe is global, controller-scoped, or route-scoped.\n\n## Best practices: which component, for what\n\n- **Middleware** — request-level, framework-agnostic concerns with no need for Nest's execution context: IDs, raw logging, compression, security headers.\n- **Guards** — authorization: \"can this identity do this,\" using data already attached by middleware or a prior guard (like `request.user` from a passport strategy).\n- **Interceptors** — cross-cutting concerns that wrap a call: timing, logging both sides, response shaping, caching, retry.\n- **Pipes** — per-argument validation and transformation: turning a raw `body`\u002F`param`\u002F`query` into a typed, checked value.\n- **Exception filters** — turning any thrown error, from any stage, into a consistent response shape.\n- Never put validation logic in a guard, and never put authorization logic in a pipe — each answers a question the other one can't see the data for yet.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-request-lifecycle\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## FAQ\n\n### Do guards run before or after middleware in NestJS?\n\nAfter. The order is middleware, then guards, then interceptors, then pipes, then the handler.\n\n### Can a guard read a validated, transformed DTO?\n\nNo. Pipes — including `ValidationPipe`, which applies a DTO's `class-validator`\u002F`class-transformer` decorators — run *after* guards. A guard only ever sees the raw request.\n\n### What happens to interceptors if a guard denies the request?\n\nNothing runs. A denied guard (`canActivate()` returning `false`, or throwing) skips both the \"before\" and \"after\" halves of every interceptor and goes straight to exception filters.\n\n### Does an interceptor's \"after\" logic run if the handler throws?\n\nOnly if the interceptor explicitly handles the error, e.g. with RxJS's `catchError`. A plain `tap()` chained for the success path is skipped when the observable errors instead of emitting a value.\n\n### Which exception filter runs when several are registered?\n\nThe most specific one: a filter bound to the route handler runs before a controller-level filter, which runs before a global filter — filters resolve from the bottom up, unlike guards, interceptors, and pipes, which resolve global-first.\n\n### Does the request lifecycle differ between Express and Fastify?\n\nThe guard\u002Finterceptor\u002Fpipe\u002Ffilter order is identical — that's Nest's own execution model, independent of the HTTP adapter. Middleware is where they diverge: arbitrary Express-style middleware isn't automatically Fastify-compatible, since Fastify has its own hook and plugin system.\n\n## Cheat sheet\n\n| Stage | Runs | Can stop the request | Typical use |\n| --- | --- | --- | --- |\n| Middleware | First, before routing | `next()` not called | request IDs, compression, headers |\n| Guards | After middleware | `canActivate()` returns `false`\u002Fthrows | authorization |\n| Interceptors (before) | After guards, before pipes | only by throwing | start a timer, attach context |\n| Pipes | After interceptors (before), before the handler | throws on invalid input | validation, transformation |\n| Handler | After every argument is piped | throws | your actual business logic |\n| Interceptors (after) | After the handler resolves | only affects success path unless using `catchError` | logging, response shaping, caching |\n| Exception filters | On any thrown error, from any stage | — | consistent error responses |\n\n```ts\n\u002F\u002F The order, as code you'd actually register:\napp.use(RequestIdMiddleware);                 \u002F\u002F 1. middleware\napp.useGlobalGuards(new RolesGuard(reflector)); \u002F\u002F 2. guards\napp.useGlobalInterceptors(new TimingInterceptor()); \u002F\u002F 3 & 6. before + after\napp.useGlobalPipes(new ValidationPipe());     \u002F\u002F 4. pipes\n\u002F\u002F 5. your handler runs here\napp.useGlobalFilters(new HttpExceptionFilter()); \u002F\u002F 7. on any throw, from any stage\n```\n\n## Key takeaways\n\n- The order is fixed: **middleware → guards → interceptors (before) → pipes → handler → interceptors (after) → exception filters.**\n- Guards decide *if* a request proceeds; they never see data a pipe would have validated or transformed, because pipes run later.\n- Interceptors wrap the rest of the pipeline — their \"before\" and \"after\" halves are one component, not two, and the \"after\" half only fires on success unless you handle errors explicitly.\n- Any exception, from any stage, exits straight to exception filters, which resolve most-specific first.\n- Put each concern in the component built for it — authorization in guards, validation in pipes — and lifecycle bugs like the opening one stop happening.\n\n## Back to that guard\n\nThe `AdultOnlyGuard` wasn't wrong about the rule — eighteen-or-over is a real requirement. It was wrong about *when* it was running: before anything had touched, checked, or converted the value it was reading. Move that rule into the DTO (`@Min(18)`) or into logic that reads the same raw shape a guard actually sees, and it stops being a guard that occasionally agrees with the data by accident.\n\nNext Friday's episode goes one level deeper: what actually happens inside the DI container when two of your providers depend on each other.\n\nWhere has the lifecycle order bitten you — a guard, an interceptor, or a filter that fired (or didn't) when you least expected?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":108,"canonical":497,"description":109},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-request-lifecycle","01a03cdb-22f0-753d-b6a4-f61a61d44c1a",{"id":500,"locked":18},"01a03cdb-23ca-71d6-bf54-e5b88a9fb0bd",[502],{"id":45,"slug":46,"title":48,"_count":503},{"questions":51},[505],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":507,"questionCount":51},{"questions":51}]