[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-nestjs-weekly-optional-not-inherited":44,"search-suggestions":60,"quiz-article-nestjs-weekly-optional-not-inherited":108,"path-membership-nestjs-weekly-optional-not-inherited":519},[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},"01a11150-ccb0-773a-8fe4-5b1d8cf840f2","nestjs-weekly-optional-not-inherited","PRACTICE_QUIZ","NestJS 12: @Optional() and Subclass Inheritance","Test yourself on NestJS 12's @Optional() metadata rule: which subclasses are actually at risk, what changed, and how to fix one correctly.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",159,{"slug":66,"name":67,"articles":68},"javascript","Javascript",129,{"slug":70,"name":71,"articles":72},"frontend","Frontend",90,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",63,{"slug":78,"name":79,"articles":80},"css","Css",48,{"slug":82,"name":83,"articles":84},"performance","Performance",27,{"slug":86,"name":87,"articles":88},"typescript","Typescript",23,{"slug":90,"name":91,"articles":92},"react","React",19,{"slug":94,"name":95,"articles":96},"programming","Programming",15,{"slug":98,"name":99,"articles":96},"browser","Browser",{"slug":101,"name":102,"articles":103},"node","Node",14,{"slug":105,"name":106,"articles":107},"html","Html",11,{"id":109,"slug":46,"title":110,"subtitle":52,"excerpt":111,"coverUrl":112,"locale":13,"readingMinutes":96,"publishedAt":113,"viewCount":114,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":124,"_count":129,"playground":131,"body":133,"bodyMd":472,"seo":473,"translationGroupId":475,"series":476,"podcastUrl":52,"verticalId":5,"thread":510,"assessments":512,"translations":515,"quiz":517},"01a11150-cc6d-7657-8fe5-ae7578a8058b","NestJS 12: @Optional() Not Inherited by a Redeclared Constructor","NestJS 12 stops inheriting @Optional() once a subclass redeclares its own constructor — not when it has none at all. Cheat sheet included.","\u002Fmedia\u002Fcovers\u002Fnestjs-weekly-optional-not-inherited.png","2026-10-30T05:36:46.066Z",41,{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":122,"name":123},"nestjs","Nestjs",[125,126,127,128],{"slug":122,"name":123,"color":52},{"slug":86,"name":87,"color":52},{"slug":101,"name":102,"color":52},{"slug":74,"name":75,"color":52},{"assessments":130},1,{"slug":46,"title":132},"NestJS 12 @Optional() inheritance — interactive playground",{"blocks":134,"version":130},[135,139,142,147,150,159,162,165,168,182,185,188,194,197,201,204,208,211,216,219,222,225,228,233,236,239,242,245,249,252,255,258,262,265,268,271,274,278,281,284,287,290,293,296,299,302,306,309,313,316,319,322,331,334,343,346,350,353,357,360,364,367,371,374,377,400,404,413,416,419,422,425,428,431,439,442,445,448,454,457,460,463,466],{"id":136,"html":137,"type":138},"b1","\u003Cp>A NestJS 11 → 12 upgrade runs clean. \u003Ccode>npm install\u003C\u002Fcode>, \u003Ccode>nest build\u003C\u002Fcode>, no TypeScript errors, no deprecation warnings. Then the app won&#39;t boot: \u003Ccode>Error: Nest can&#39;t resolve dependencies of the TenantApiKeyGuard (?). Please make sure that the argument at index [0] is available in the AuthModule context.\u003C\u002Fcode> The guard in question has one recent change: a constructor was added to it last sprint, purely to forward an already-optional option down to its parent. Nothing about the dependency itself changed — it&#39;s the same parameter, with the same intent, that&#39;s been optional since the base class was written.\u003C\u002Fp>","paragraph",{"id":140,"html":141,"type":138},"b2","\u003Cp>This is written against \u003Cstrong>\u003Ccode>@nestjs\u002Fcore\u003C\u002Fcode> 12.1.2\u003C\u002Fstrong> (verified 2026-10-06 via \u003Ccode>npm view @nestjs\u002Fcore dist-tags\u003C\u002Fcode>; \u003Ccode>v12.0.0\u003C\u002Fcode> shipped 2026-08-27, and the \u003Ccode>11.x\u003C\u002Fcode> line is now on the \u003Ccode>legacy\u003C\u002Fcode> npm tag at \u003Ccode>11.2.7\u003C\u002Fcode>). The behavior below is documented in NestJS&#39;s own \u003Ca href=\"https:\u002F\u002Fdocs.nestjs.com\u002Fmigration-guide\">migration guide\u003C\u002Fa>, under &quot;\u003Ccode>@Optional()\u003C\u002Fcode> is no longer inherited by a redeclared constructor&quot; — a deliberate v12 change, not a bug. One detail matters enough to state up front, because it inverts the obvious guess: \u003Cstrong>a subclass with no constructor of its own is \u003Cem>not\u003C\u002Fem> affected.\u003C\u002Fstrong> Only a subclass that declares its own constructor loses the inherited marker. (Versions 12.0.0–12.0.4 briefly broke the childless case too — more on that below — but that was its own regression, fixed in 12.1.0, not the rule going forward.) It is unrelated to the v12 ESM migration or the Standard Schema validation feature covered in an \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnestjs-12-standard-schema-validation-without-class-validator-161f\">earlier episode of this series\u003C\u002Fa> — this is purely DI-container behavior.\u003C\u002Fp>",{"id":143,"html":144,"text":145,"type":146,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":148,"html":149,"type":138},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":151,"type":152,"items":153,"ordered":18},"b5","list",[154,155,156,157,158],"Explain exactly what changed in how NestJS 12 reads \u003Ccode>@Optional()\u003C\u002Fcode> metadata on constructor parameters, and why it&#39;s specifically tied to \u003Cem>which class declares the constructor\u003C\u002Fem>, not to &quot;the subclass&quot; in general","Correctly predict that a childless subclass (\u003Ccode>class X extends Y {}\u003C\u002Fcode>) is safe, and that the risk starts the moment that subclass gets a constructor of its own","Read an \u003Ccode>UnknownDependenciesException\u003C\u002Fcode> that names a subclass with its own constructor, and know that the fix is re-adding \u003Ccode>@Optional()\u003C\u002Fcode> there, not removing the constructor","Fix an affected subclass correctly — and explain why &quot;it compiled before&quot; isn&#39;t evidence it was ever safe","Decide, for a new subclass you&#39;re writing today, whether adding a constructor to it needs a \u003Ccode>@Optional()\u003C\u002Fcode> audit first",{"id":160,"html":161,"text":161,"type":146,"level":31},"b6","Who this is for",{"id":163,"html":164,"type":138},"b7","\u003Cp>You&#39;ve written at least one NestJS provider or guard with a constructor-injected dependency, and you know roughly what \u003Ccode>@Injectable()\u003C\u002Fcode> and \u003Ccode>@Optional()\u003C\u002Fcode> do. You don&#39;t need any TypeScript decorator metaprogramming experience — this article builds the one piece of \u003Ccode>reflect-metadata\u003C\u002Fcode> behavior that matters, from nothing.\u003C\u002Fp>",{"id":166,"html":167,"text":167,"type":146,"level":31},"b8","Table of contents",{"id":169,"type":152,"items":170,"ordered":18},"b9",[171,172,173,174,175,176,177,178,179,180,181],"\u003Ca href=\"#the-problem-a-guard-that-worked-in-v11-throws-on-v12\">The problem: a guard that worked in v11 throws on v12\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-the-optional-marker-belongs-to-whichever-class-declares-the-constructor\">The mental model: the optional marker belongs to whichever class declares the constructor\u003C\u002Fa>","\u003Ca href=\"#stage-1-a-base-class-with-one-optional-dependency\">Stage 1: a base class with one optional dependency\u003C\u002Fa>","\u003Ca href=\"#stage-2-the-childless-subclass-safe-in-v11-and-v12\">Stage 2: the childless subclass — safe in v11 and v12\u003C\u002Fa>","\u003Ca href=\"#stage-3-add-a-constructor-lose-the-marker\">Stage 3: add a constructor, lose the marker\u003C\u002Fa>","\u003Ca href=\"#stage-4-the-fix-and-why-its-deliberate\">Stage 4: the fix, and why it&#39;s deliberate\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":183,"html":184,"text":184,"type":146,"level":31},"b10","The problem: a guard that worked in v11 throws on v12",{"id":186,"html":187,"type":138},"b11","\u003Cp>Here&#39;s a base guard, trimmed to the part that matters. It optionally takes module options — if none were provided, it falls back to defaults:\u003C\u002Fp>",{"id":189,"code":190,"type":191,"language":192,"highlight":193},"b12","\u002F\u002F base-api-key.guard.ts\n@Injectable()\nexport class BaseApiKeyGuard implements CanActivate {\n  constructor(@Optional() protected options?: ApiKeyModuleOptions) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    const header = this.options?.headerName ?? 'x-api-key';\n    \u002F\u002F ...validate the request against `header`\n    return true;\n  }\n}","code","ts",[],{"id":195,"html":196,"type":138},"b13","\u003Cp>A per-feature guard extends it. Originally it was a one-liner, written only to get a distinct DI token:\u003C\u002Fp>",{"id":198,"code":199,"type":191,"language":192,"highlight":200},"b14","\u002F\u002F tenant-api-key.guard.ts\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {}",[],{"id":202,"html":203,"type":138},"b15","\u003Cp>That version — no constructor at all — is fine on both v11 and v12. The trouble starts when someone later adds a constructor to \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode>, for an ordinary reason: forwarding the same option explicitly, adding a log line, or preparing to inject a second collaborator next sprint.\u003C\u002Fp>",{"id":205,"code":206,"type":191,"language":192,"highlight":207},"b16","\u002F\u002F tenant-api-key.guard.ts, after a routine-looking change\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(options?: ApiKeyModuleOptions) {\n    super(options);\n  }\n}",[],{"id":209,"html":210,"type":138},"b17","\u003Cp>\u003Ccode>ApiKeyModuleOptions\u003C\u002Fcode> isn&#39;t registered as a provider anywhere (there&#39;s no module-wide config object, by design — it&#39;s optional), and on NestJS 11 this is unremarkable: the app boots fine. On NestJS 12, with no further changes, bootstrap now fails:\u003C\u002Fp>",{"id":212,"code":213,"type":191,"language":214,"highlight":215},"b18","Error: Nest can't resolve dependencies of the TenantApiKeyGuard (?).\nPlease make sure that the argument at index [0] is available in the AuthModule context.","plain",[],{"id":217,"html":218,"type":138},"b19","\u003Cp>The constructor added to \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> looks harmless — it just forwards the same parameter to \u003Ccode>super()\u003C\u002Fcode>. But forwarding it isn&#39;t the same as marking it. The \u003Ccode>@Optional()\u003C\u002Fcode> call that made this parameter optional lives only on \u003Ccode>BaseApiKeyGuard\u003C\u002Fcode>, and \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> just started declaring its own constructor without repeating it.\u003C\u002Fp>",{"id":220,"html":221,"text":221,"type":146,"level":31},"b20","The mental model: the optional marker belongs to whichever class declares the constructor",{"id":223,"html":224,"type":138},"b21","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a TypeScript decorator like \u003Ccode>@Optional()\u003C\u002Fcode> doesn&#39;t attach a property to an instance, and it doesn&#39;t attach to &quot;the class&quot; in some abstract sense — it calls \u003Ccode>Reflect.defineMetadata(key, data, target)\u003C\u002Fcode> on one specific constructor function, at the moment that constructor&#39;s parameter list is compiled. \u003Ccode>BaseApiKeyGuard\u003C\u002Fcode>&#39;s own constructor is decorated with \u003Ccode>@Optional()\u003C\u002Fcode>, so the marker for &quot;parameter 0 is optional&quot; is defined on \u003Ccode>BaseApiKeyGuard\u003C\u002Fcode> itself.\u003C\u002Fp>",{"id":226,"html":227,"type":138},"b22","\u003Cp>Whether a lookup later finds that metadata from a subclass depends on which \u003Ccode>reflect-metadata\u003C\u002Fcode> method does the reading, because the two behave differently with respect to the prototype chain — and, as of NestJS 12.1, on \u003Cem>which class actually owns the constructor being resolved\u003C\u002Fem>:\u003C\u002Fp>",{"id":229,"type":152,"items":230,"ordered":18},"b23",[231,232],"\u003Cstrong>\u003Ccode>Reflect.getMetadata(key, target)\u003C\u002Fcode>\u003C\u002Fstrong> walks up the prototype chain. NestJS still uses this for parameter \u003Cem>types\u003C\u002Fem> (\u003Ccode>design:paramtypes\u003C\u002Fcode>), so a subclass — with or without its own constructor — always sees the correct inherited type.","\u003Cstrong>\u003Ccode>Reflect.getOwnMetadata(key, target)\u003C\u002Fcode>\u003C\u002Fstrong> does not walk the chain. NestJS 12 uses this for \u003Ccode>@Optional()\u003C\u002Fcode> markers, but it&#39;s called on \u003Cstrong>the class that declares the constructor being resolved\u003C\u002Fstrong>, not blindly on the exact subclass. For a class with no constructor of its own, that owner is the nearest ancestor that does declare one — so the marker is still found. For a class that declares its own constructor, that owner is the class itself — so a marker that exists only on the parent is not found.",{"id":234,"html":235,"type":138},"b24","\u003Cp>NestJS&#39;s own \u003Ca href=\"https:\u002F\u002Fdocs.nestjs.com\u002Fmigration-guide\">migration guide\u003C\u002Fa> states it this way: \u003Cem>&quot;Nest now reads the optional markers of constructor parameters from the class that declares the constructor. A subclass that declares its own constructor no longer inherits the markers of its parent&#39;s constructor... Starting with NestJS v12.1, a subclass without its own constructor keeps the optional markers of its parent&#39;s constructor, as in v11.&quot;\u003C\u002Fem> Parameter \u003Cem>types\u003C\u002Fem> are unaffected either way — they&#39;re read through the separate, always-chain-walking \u003Ccode>design:paramtypes\u003C\u002Fcode> mechanism — so Nest still correctly sees that \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> takes one constructor argument of type \u003Ccode>ApiKeyModuleOptions\u003C\u002Fcode>; once that subclass has its own constructor, it just no longer sees that argument as optional.\u003C\u002Fp>",{"id":237,"html":238,"type":138},"b25","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the same two facts — &quot;this parameter exists&quot; and &quot;this parameter is optional&quot; — are stored as two different kinds of metadata, read with two different \u003Ccode>reflect-metadata\u003C\u002Fcode> calls. Declaring your own constructor doesn&#39;t change where parameter \u003Cem>types\u003C\u002Fem> come from. It does change where the \u003Cem>optional marker\u003C\u002Fem> is looked up: from &quot;wherever an ancestor happened to define it&quot; to &quot;the class that now owns this constructor&quot; — which, now that you&#39;ve written one, is you.\u003C\u002Fp>",{"id":240,"html":241,"text":241,"type":146,"level":31},"b26","Stage 1: a base class with one optional dependency",{"id":243,"html":244,"type":138},"b27","\u003Cp>Start from the smallest version of the base guard, with nothing inherited yet:\u003C\u002Fp>",{"id":246,"code":247,"type":191,"language":192,"highlight":248},"b28","@Injectable()\nexport class BaseApiKeyGuard implements CanActivate {\n  constructor(@Optional() protected options?: ApiKeyModuleOptions) {}\n\n  canActivate(): boolean {\n    return Boolean(this.options?.headerName ?? 'x-api-key');\n  }\n}",[],{"id":250,"html":251,"type":138},"b29","\u003Cp>Used directly (\u003Ccode>@UseGuards(BaseApiKeyGuard)\u003C\u002Fcode>), this behaves identically in v11 and v12: \u003Ccode>@Optional()\u003C\u002Fcode> metadata is defined on \u003Ccode>BaseApiKeyGuard\u003C\u002Fcode> itself, and whatever reads it — chain-walking or not — finds it right there, no prototype walk required.\u003C\u002Fp>",{"id":253,"html":254,"text":254,"type":146,"level":31},"b30","Stage 2: the childless subclass — safe in v11 and v12",{"id":256,"html":257,"type":138},"b31","\u003Cp>Add the subclass that exists purely to give the feature module its own injectable token:\u003C\u002Fp>",{"id":259,"code":260,"type":191,"language":192,"highlight":261},"b32","@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {}",[],{"id":263,"html":264,"type":138},"b33","\u003Cp>No constructor, no new behavior — this is the entire class body. Register it in a module, inject it with \u003Ccode>@UseGuards(TenantApiKeyGuard)\u003C\u002Fcode>, and this boots normally on \u003Cstrong>both\u003C\u002Fstrong> NestJS 11 and NestJS 12.1+. \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> has no constructor of its own, so Nest&#39;s lookup for the optional marker walks up to find the nearest class that \u003Cem>does\u003C\u002Fem> declare one — \u003Ccode>BaseApiKeyGuard\u003C\u002Fcode> — and reads the marker from there. This is exactly the common pattern you&#39;ll recognize from \u003Ccode>@nestjs\u002Fpassport\u003C\u002Fcode>, \u003Ccode>class JwtAuthGuard extends AuthGuard(&#39;jwt&#39;) {}\u003C\u002Fcode>, and it was never the trap here.\u003C\u002Fp>",{"id":266,"html":267,"type":138},"b34","\u003Cp>(For a few weeks, it was: NestJS \u003Ccode>12.0.0\u003C\u002Fcode> through \u003Ccode>12.0.4\u003C\u002Fcode> read the optional marker directly off the exact subclass with no such fallback, which broke this childless case too. That was a regression against a widely-used pattern, not the intended design, and it was fixed in \u003Ccode>12.1.0\u003C\u002Fcode>. If you&#39;re on an exact \u003Ccode>12.0.x\u003C\u002Fcode> pin, this stage can fail for you — upgrading to \u003Ccode>12.1+\u003C\u002Fcode> is the fix, not changing your class.)\u003C\u002Fp>",{"id":269,"html":270,"text":270,"type":146,"level":31},"b35","Stage 3: add a constructor, lose the marker",{"id":272,"html":273,"type":138},"b36","\u003Cp>Now give \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> a constructor — for a reason as ordinary as forwarding the same parameter explicitly:\u003C\u002Fp>",{"id":275,"code":276,"type":191,"language":192,"highlight":277},"b37","@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(options?: ApiKeyModuleOptions) {\n    super(options);\n  }\n}",[],{"id":279,"html":280,"type":138},"b38","\u003Cp>No behavior changed. The parameter is still forwarded to the exact same place. But bootstrap now throws on NestJS 12 (12.1+, the version this article targets):\u003C\u002Fp>",{"id":282,"code":213,"type":191,"language":214,"highlight":283},"b39",[],{"id":285,"html":286,"type":138},"b40","\u003Cp>Per the mental model above: \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> now declares its own constructor, so it is the class the optional-marker lookup reads from — and it never had \u003Ccode>@Optional()\u003C\u002Fcode> called on its own constructor&#39;s parameter. The parameter type is still inherited (Nest knows it&#39;s an \u003Ccode>ApiKeyModuleOptions\u003C\u002Fcode>), but the optional flag is not. A parameter that looks required, with no provider for \u003Ccode>ApiKeyModuleOptions\u003C\u002Fcode> anywhere in \u003Ccode>AuthModule\u003C\u002Fcode>, is an unresolvable dependency, which \u003Ccode>UnknownDependenciesException\u003C\u002Fcode> reports.\u003C\u002Fp>",{"id":288,"html":289,"type":138},"b41","\u003C!-- playground:start -->",{"id":291,"html":292,"text":292,"type":146,"level":31},"b42","🎮 Try it yourself",{"id":294,"html":295,"type":138},"b43","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-optional-not-inherited\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":297,"html":298,"type":138},"b44","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":300,"html":301,"type":138},"b45","\u003C!-- playground:end -->",{"id":303,"html":304,"text":305,"type":146,"level":31},"b46","Stage 4: the fix, and why it&#39;s deliberate","Stage 4: the fix, and why it's deliberate",{"id":307,"html":308,"type":138},"b47","\u003Cp>The fix is to re-declare \u003Ccode>@Optional()\u003C\u002Fcode> on the subclass&#39;s own constructor parameter:\u003C\u002Fp>",{"id":310,"code":311,"type":191,"language":192,"highlight":312},"b48","@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(@Optional() options?: ApiKeyModuleOptions) {\n    super(options);\n  }\n}",[],{"id":314,"html":315,"type":138},"b49","\u003Cp>This works on v12.1+ (and still works on v11) because the parameter decorator now runs \u003Cem>on\u003C\u002Fem> \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> directly — the lookup no longer needs to find an ancestor&#39;s marker, because the marker is right there on the class that owns the constructor.\u003C\u002Fp>",{"id":317,"html":318,"type":138},"b50","\u003Cp>It&#39;s worth taking the migration guide&#39;s own framing seriously here: \u003Cem>&quot;This is deliberate: a dependency that was genuinely missing used to resolve to \u003Ccode>undefined\u003C\u002Fcode> silently.&quot;\u003C\u002Fem> Before this change, once you added a constructor to a subclass and simply forgot to re-mark a forwarded parameter, the mistake was invisible — the old chain-walking behavior would still find the ancestor&#39;s marker and quietly pass \u003Ccode>undefined\u003C\u002Fcode>, with nothing in the logs to say the subclass&#39;s own constructor made no promise about that parameter at all. \u003Ccode>UnknownDependenciesException\u003C\u002Fcode> is a less convenient failure mode than silent \u003Ccode>undefined\u003C\u002Fcode>, but it&#39;s a more honest one.\u003C\u002Fp>",{"id":320,"html":321,"text":321,"type":146,"level":31},"b51","Edge cases and gotchas",{"id":323,"type":152,"items":324,"ordered":18},"b52",[325,326,327,328,329,330],"\u003Cstrong>The break is specific to a subclass that declares its own constructor.\u003C\u002Fstrong> A childless subclass — the common \u003Ccode>class X extends Y {}\u003C\u002Fcode> or \u003Ccode>class JwtAuthGuard extends AuthGuard(&#39;jwt&#39;) {}\u003C\u002Fcode> shape — is unaffected on 12.1+.","\u003Cstrong>This is narrower than &quot;subclassing is risky.&quot;\u003C\u002Fstrong> Regular method inheritance, property inheritance, and \u003Ccode>instanceof\u003C\u002Fcode> checks are untouched. Only the DI container&#39;s optional-parameter lookup changed, and only once a subclass&#39;s own constructor exists.","\u003Cstrong>Multi-level inheritance follows the same rule, not a worse one.\u003C\u002Fstrong> If \u003Ccode>GrandchildGuard extends TenantApiKeyGuard extends BaseApiKeyGuard\u003C\u002Fcode>, and only \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> declares a constructor, a childless \u003Ccode>GrandchildGuard\u003C\u002Fcode> is safe — the lookup walks past it to \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode>, the nearest class that owns a constructor, and reads the marker there (not from \u003Ccode>BaseApiKeyGuard\u003C\u002Fcode>, which is one link further).","\u003Cstrong>The 12.0.0–12.0.4 window is a real but temporary trap.\u003C\u002Fstrong> If you&#39;re reading an older blog post, Stack Overflow answer, or internal wiki page that describes the childless subclass as the broken case, it was likely written against one of those four patch releases, where that was briefly true. The documented, intended behavior since 12.1.0 is the one in this article.","\u003Cstrong>Node.js version matters for the same upgrade.\u003C\u002Fstrong> NestJS 12 requires \u003Cstrong>Node.js v20.19+ or v22.12+\u003C\u002Fstrong> (per the official migration guide&#39;s Node.js requirements) because the ESM-packaged core relies on Node&#39;s \u003Ccode>require(esm)\u003C\u002Fcode> interop — unrelated to this specific bug, but the same upgrade that triggers it will also fail outright on an older Node runtime, which is worth ruling out first if bootstrap errors look different from the one shown here.","\u003Cstrong>This isn&#39;t limited to guards.\u003C\u002Fstrong> Any \u003Ccode>@Injectable()\u003C\u002Fcode> subclass — a repository base class, a strategy base class, an interceptor base class — that adds a constructor forwarding an optional parameter from its parent without redeclaring \u003Ccode>@Optional()\u003C\u002Fcode> has the identical exposure.",{"id":332,"html":333,"text":333,"type":146,"level":31},"b53","Best practices",{"id":335,"type":152,"items":336,"ordered":18},"b54",[337,338,339,340,341,342],"\u003Cstrong>Grep for recently-added constructors on subclasses of your own injectable base classes\u003C\u002Fstrong>, especially after a v12 upgrade: any \u003Ccode>class X extends Y { constructor(...) { super(...); } }\u003C\u002Fcode> where \u003Ccode>Y\u003C\u002Fcode>&#39;s constructor has an \u003Ccode>@Optional()\u003C\u002Fcode> parameter, and \u003Ccode>X\u003C\u002Fcode>&#39;s constructor doesn&#39;t repeat it, is a candidate.","\u003Cstrong>Treat &quot;adding a constructor to a subclass&quot; as a checkpoint, not a no-op.\u003C\u002Fstrong> The moment a subclass gets its own constructor, every parameter it forwards from the parent needs its own decorators re-declared — optional markers included — because the subclass is now the class Nest reads them from.","\u003Cstrong>Prefer composition over inheritance for DI-bearing base classes\u003C\u002Fstrong> where you can. A \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> that takes a \u003Ccode>BaseApiKeyGuard\u003C\u002Fcode> as an injected collaborator, rather than extending it, never has this problem to begin with, because there&#39;s no constructor-ownership handoff for \u003Ccode>@Optional()\u003C\u002Fcode> metadata to trip over.","\u003Cstrong>Run \u003Ccode>nest upgrade\u003C\u002Fcode>\u003C\u002Fstrong> when you move to v12 — the CLI&#39;s upgrade command is built to flag this class of change, and catching it at upgrade time is cheaper than catching it at a production boot failure.","\u003Cstrong>Don&#39;t &quot;fix&quot; this by making the dependency required instead.\u003C\u002Fstrong> That trades a confusing error for a real one at the cost of forcing every consumer to provide \u003Ccode>ApiKeyModuleOptions\u003C\u002Fcode>, even ones that were correctly relying on the default. Redeclare \u003Ccode>@Optional()\u003C\u002Fcode>, don&#39;t remove it.","\u003Cstrong>Pin to \u003Ccode>12.1.0\u003C\u002Fcode> or later, not an exact \u003Ccode>12.0.x\u003C\u002Fcode> release\u003C\u002Fstrong>, if you&#39;re locking a specific NestJS 12 version. The childless-subclass regression only existed in \u003Ccode>12.0.0\u003C\u002Fcode>–\u003Ccode>12.0.4\u003C\u002Fcode>.",{"id":344,"html":345,"text":345,"type":146,"level":31},"b55","FAQ",{"id":347,"html":348,"text":349,"type":146,"level":43},"b56","Does this affect property injection (\u003Ccode>@Optional() @Inject() someProp\u003C\u002Fcode>), or only constructor parameters?","Does this affect property injection (@Optional() @Inject() someProp), or only constructor parameters?",{"id":351,"html":352,"type":138},"b57","\u003Cp>Constructor parameters are the mechanism this article covers, and it&#39;s also the one the migration guide documents. Property-based \u003Ccode>@Optional()\u003C\u002Fcode> injection is read through a different metadata key, and a subclass redeclaring its own \u003Ccode>@Optional()\u003C\u002Fcode> property does \u003Cem>not\u003C\u002Fem> lose the parent&#39;s separately-declared optional properties — that inheritance was specifically preserved in a later 12.1.x patch. Still, verify your own case against the current docs before assuming the two injection styles behave identically.\u003C\u002Fp>",{"id":354,"html":355,"text":356,"type":146,"level":43},"b58","Is \u003Ccode>@Optional()\u003C\u002Fcode> metadata handling &quot;broken,&quot; or is this intentional?","Is @Optional() metadata handling \"broken,\" or is this intentional?",{"id":358,"html":359,"type":138},"b59","\u003Cp>Intentional, per NestJS&#39;s own migration guide, for the case it targets: a subclass that declares its own constructor no longer silently inherits the parent&#39;s optional markers. The previous (v11) behavior let a parameter that genuinely had no provider resolve to \u003Ccode>undefined\u003C\u002Fcode> without any signal, as long as some ancestor class had marked it optional — regardless of whether the subclass&#39;s own constructor had any business making that same promise. The v12.1+ behavior surfaces that case as an explicit \u003Ccode>UnknownDependenciesException\u003C\u002Fcode> instead, while still correctly treating a childless subclass as making no promises of its own to override.\u003C\u002Fp>",{"id":361,"html":362,"text":363,"type":146,"level":43},"b60","Will \u003Ccode>nest upgrade\u003C\u002Fcode> fix this for me automatically?","Will nest upgrade fix this for me automatically?",{"id":365,"html":366,"type":138},"b61","\u003Cp>The \u003Ccode>nest upgrade\u003C\u002Fcode> command (new in the v12 CLI) is designed to apply migration steps and flag changes like this one for review, but a decorator-level fix — re-declaring \u003Ccode>@Optional()\u003C\u002Fcode> on a subclass&#39;s own constructor — is a code change specific to your class hierarchy. Treat the tooling as a way to \u003Cem>find\u003C\u002Fem> affected classes faster, not as something that edits them for you unsupervised.\u003C\u002Fp>",{"id":368,"html":369,"text":370,"type":146,"level":43},"b62","If I just added a constructor to log something, with no new dependencies, do I still need to redeclare \u003Ccode>@Optional()\u003C\u002Fcode>?","If I just added a constructor to log something, with no new dependencies, do I still need to redeclare @Optional()?",{"id":372,"html":373,"type":138},"b63","\u003Cp>Yes, if that constructor forwards a parameter the parent marked optional. The trigger isn&#39;t &quot;you added a new dependency&quot; — it&#39;s &quot;this subclass now has a constructor at all.&quot; Even a constructor that only forwards existing parameters and adds a \u003Ccode>console.log\u003C\u002Fcode> moves the optional-marker lookup to the subclass itself.\u003C\u002Fp>",{"id":375,"html":376,"text":376,"type":146,"level":31},"b64","Cheat sheet",{"id":378,"head":379,"rows":383,"type":399},"b65",[380,381,382],"Situation","v11","v12 (12.1+)",[384,388,392,395],[385,386,387],"Subclass has no constructor of its own","Inherited \u003Ccode>@Optional()\u003C\u002Fcode> applies","Inherited \u003Ccode>@Optional()\u003C\u002Fcode> still applies (fixed in 12.1.0; briefly broken only in 12.0.0–12.0.4)",[389,390,391],"Subclass declares its own constructor without redeclaring \u003Ccode>@Optional()\u003C\u002Fcode>","Inherited \u003Ccode>@Optional()\u003C\u002Fcode> still applies","Inherited \u003Ccode>@Optional()\u003C\u002Fcode> is ignored — parameter looks required",[393,394,394],"Subclass declares its own constructor and redeclares \u003Ccode>@Optional()\u003C\u002Fcode>","Works",[396,397,398],"Parameter \u003Cem>types\u003C\u002Fem> across the subclass boundary","Inherited","Still inherited (unchanged)","table",{"id":401,"code":402,"type":191,"language":192,"highlight":403},"b66","\u002F\u002F The pattern to use any time you give a subclass its own constructor\n\u002F\u002F that forwards a parameter the parent class marked @Optional().\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(@Optional() options?: ApiKeyModuleOptions) {\n    super(options); \u002F\u002F forward it — don't just swallow the param\n  }\n}",[],{"id":405,"type":152,"items":406,"ordered":18},"b67",[407,408,409,410,411,412],"\u003Ccode>Reflect.getOwnMetadata(key, Target)\u003C\u002Fcode> → own metadata only, no prototype walk.","\u003Ccode>Reflect.getMetadata(key, Target)\u003C\u002Fcode> → walks the prototype chain.","NestJS 12.1+ reads \u003Ccode>@Optional()\u003C\u002Fcode> constructor markers from whichever class declares the constructor being resolved (\u003Ccode>getOwnMetadata\u003C\u002Fcode> on that owner) — not blindly from the literal subclass. Parameter \u003Cem>types\u003C\u002Fem> still use the chain-walking \u003Ccode>getMetadata\u003C\u002Fcode>.","A childless subclass is safe. A subclass with its own constructor must redeclare \u003Ccode>@Optional()\u003C\u002Fcode> for every forwarded optional parameter.","Requires Node.js \u003Cstrong>v20.19+\u003C\u002Fstrong> or \u003Cstrong>v22.12+\u003C\u002Fstrong>.","Verified against \u003Ccode>@nestjs\u002Fcore\u003C\u002Fcode> \u003Cstrong>12.1.2\u003C\u002Fstrong> — re-check \u003Ccode>npm view @nestjs\u002Fcore dist-tags\u003C\u002Fcode> before relying on a patch-version detail, and avoid pinning an exact \u003Ccode>12.0.x\u003C\u002Fcode> release.",{"id":414,"html":415,"type":138},"b68","\u003C!-- quiz:start -->",{"id":417,"html":418,"text":418,"type":146,"level":31},"b69","🧠 Test yourself",{"id":420,"html":421,"type":138},"b70","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-optional-not-inherited\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":423,"html":424,"type":138},"b71","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":426,"html":427,"type":138},"b72","\u003C!-- quiz:end -->",{"id":429,"html":430,"text":430,"type":146,"level":31},"b73","Key takeaways",{"id":432,"type":152,"items":433,"ordered":18},"b74",[434,435,436,437,438],"NestJS 12.1+ reads \u003Ccode>@Optional()\u003C\u002Fcode> constructor markers from whichever class declares the constructor being resolved, using \u003Ccode>Reflect.getOwnMetadata\u003C\u002Fcode> on that owner — not blindly from the exact subclass instance.","A subclass with \u003Cstrong>no\u003C\u002Fstrong> constructor of its own still inherits the marker correctly (the lookup walks up to find the owner); a subclass that \u003Cstrong>declares its own constructor\u003C\u002Fstrong> does not, unless it redeclares \u003Ccode>@Optional()\u003C\u002Fcode> itself.","Parameter \u003Cem>types\u003C\u002Fem> are read differently and are still inherited either way, so Nest still sees the parameter — it just no longer sees it as optional once the subclass owns its own constructor, which is why the error is &quot;can&#39;t resolve,&quot; not &quot;unknown parameter.&quot;","NestJS 12.0.0–12.0.4 briefly broke the childless-subclass case too, as a regression; that was fixed in 12.1.0 and is not the intended or current rule.","The fix is always the same shape: redeclare \u003Ccode>@Optional()\u003C\u002Fcode> on the subclass&#39;s own constructor and forward to \u003Ccode>super()\u003C\u002Fcode>.",{"id":440,"html":441,"type":138},"b75","\u003Cp>A guard whose only recent change was a constructor added purely to forward an existing parameter — that&#39;s what made the \u003Ccode>TenantApiKeyGuard\u003C\u002Fcode> error so disorienting to debug the first time, because the instinct is to suspect the \u003Cem>new\u003C\u002Fem> code, not the fact that adding a constructor at all moved where Nest looks for an old marker. Once you know the optional marker now travels with whoever owns the constructor, it stops being mysterious: audit every injectable subclass the moment it gains a constructor of its own. Have you hit this one yet on a v12 upgrade — and did your first guess point at the right class?\u003C\u002Fp>",{"id":443,"html":444,"type":138},"b76","\u003C!-- related:start -->",{"id":446,"html":447,"text":447,"type":146,"level":31},"b77","📚 Read next",{"id":449,"type":152,"items":450,"ordered":18},"b78",[451,452,453],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-guards-canactivate-reflector\">NestJS Guards: CanActivate, ExecutionContext &amp; Reflector\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-request-lifecycle\">NestJS Request Lifecycle Explained (with Cheat Sheet)\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-testing-provider-overrides\">NestJS Testing Module: Provider Overrides (with Cheat Sheet)\u003C\u002Fa>",{"id":455,"html":456,"type":138},"b79","\u003C!-- related:end -->",{"id":458,"type":459},"b80","divider",{"id":461,"html":462,"type":138},"b81","\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":464,"html":465,"type":138},"b82","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":467,"type":152,"items":468,"ordered":18},"b83",[469,470,471],"⭐ \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 NestJS 11 → 12 upgrade runs clean. `npm install`, `nest build`, no TypeScript errors, no deprecation warnings. Then the app won't boot: `Error: Nest can't resolve dependencies of the TenantApiKeyGuard (?). Please make sure that the argument at index [0] is available in the AuthModule context.` The guard in question has one recent change: a constructor was added to it last sprint, purely to forward an already-optional option down to its parent. Nothing about the dependency itself changed — it's the same parameter, with the same intent, that's been optional since the base class was written.\n\nThis is written against **`@nestjs\u002Fcore` 12.1.2** (verified 2026-10-06 via `npm view @nestjs\u002Fcore dist-tags`; `v12.0.0` shipped 2026-08-27, and the `11.x` line is now on the `legacy` npm tag at `11.2.7`). The behavior below is documented in NestJS's own [migration guide](https:\u002F\u002Fdocs.nestjs.com\u002Fmigration-guide), under \"`@Optional()` is no longer inherited by a redeclared constructor\" — a deliberate v12 change, not a bug. One detail matters enough to state up front, because it inverts the obvious guess: **a subclass with no constructor of its own is *not* affected.** Only a subclass that declares its own constructor loses the inherited marker. (Versions 12.0.0–12.0.4 briefly broke the childless case too — more on that below — but that was its own regression, fixed in 12.1.0, not the rule going forward.) It is unrelated to the v12 ESM migration or the Standard Schema validation feature covered in an [earlier episode of this series](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnestjs-12-standard-schema-validation-without-class-validator-161f) — this is purely DI-container behavior.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain exactly what changed in how NestJS 12 reads `@Optional()` metadata on constructor parameters, and why it's specifically tied to *which class declares the constructor*, not to \"the subclass\" in general\n- Correctly predict that a childless subclass (`class X extends Y {}`) is safe, and that the risk starts the moment that subclass gets a constructor of its own\n- Read an `UnknownDependenciesException` that names a subclass with its own constructor, and know that the fix is re-adding `@Optional()` there, not removing the constructor\n- Fix an affected subclass correctly — and explain why \"it compiled before\" isn't evidence it was ever safe\n- Decide, for a new subclass you're writing today, whether adding a constructor to it needs a `@Optional()` audit first\n\n## Who this is for\n\nYou've written at least one NestJS provider or guard with a constructor-injected dependency, and you know roughly what `@Injectable()` and `@Optional()` do. You don't need any TypeScript decorator metaprogramming experience — this article builds the one piece of `reflect-metadata` behavior that matters, from nothing.\n\n## Table of contents\n\n- [The problem: a guard that worked in v11 throws on v12](#the-problem-a-guard-that-worked-in-v11-throws-on-v12)\n- [The mental model: the optional marker belongs to whichever class declares the constructor](#the-mental-model-the-optional-marker-belongs-to-whichever-class-declares-the-constructor)\n- [Stage 1: a base class with one optional dependency](#stage-1-a-base-class-with-one-optional-dependency)\n- [Stage 2: the childless subclass — safe in v11 and v12](#stage-2-the-childless-subclass-safe-in-v11-and-v12)\n- [Stage 3: add a constructor, lose the marker](#stage-3-add-a-constructor-lose-the-marker)\n- [Stage 4: the fix, and why it's deliberate](#stage-4-the-fix-and-why-its-deliberate)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: a guard that worked in v11 throws on v12\n\nHere's a base guard, trimmed to the part that matters. It optionally takes module options — if none were provided, it falls back to defaults:\n\n```ts\n\u002F\u002F base-api-key.guard.ts\n@Injectable()\nexport class BaseApiKeyGuard implements CanActivate {\n  constructor(@Optional() protected options?: ApiKeyModuleOptions) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    const header = this.options?.headerName ?? 'x-api-key';\n    \u002F\u002F ...validate the request against `header`\n    return true;\n  }\n}\n```\n\nA per-feature guard extends it. Originally it was a one-liner, written only to get a distinct DI token:\n\n```ts\n\u002F\u002F tenant-api-key.guard.ts\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {}\n```\n\nThat version — no constructor at all — is fine on both v11 and v12. The trouble starts when someone later adds a constructor to `TenantApiKeyGuard`, for an ordinary reason: forwarding the same option explicitly, adding a log line, or preparing to inject a second collaborator next sprint.\n\n```ts\n\u002F\u002F tenant-api-key.guard.ts, after a routine-looking change\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(options?: ApiKeyModuleOptions) {\n    super(options);\n  }\n}\n```\n\n`ApiKeyModuleOptions` isn't registered as a provider anywhere (there's no module-wide config object, by design — it's optional), and on NestJS 11 this is unremarkable: the app boots fine. On NestJS 12, with no further changes, bootstrap now fails:\n\n```\nError: Nest can't resolve dependencies of the TenantApiKeyGuard (?).\nPlease make sure that the argument at index [0] is available in the AuthModule context.\n```\n\nThe constructor added to `TenantApiKeyGuard` looks harmless — it just forwards the same parameter to `super()`. But forwarding it isn't the same as marking it. The `@Optional()` call that made this parameter optional lives only on `BaseApiKeyGuard`, and `TenantApiKeyGuard` just started declaring its own constructor without repeating it.\n\n## The mental model: the optional marker belongs to whichever class declares the constructor\n\n**The mental model:** a TypeScript decorator like `@Optional()` doesn't attach a property to an instance, and it doesn't attach to \"the class\" in some abstract sense — it calls `Reflect.defineMetadata(key, data, target)` on one specific constructor function, at the moment that constructor's parameter list is compiled. `BaseApiKeyGuard`'s own constructor is decorated with `@Optional()`, so the marker for \"parameter 0 is optional\" is defined on `BaseApiKeyGuard` itself.\n\nWhether a lookup later finds that metadata from a subclass depends on which `reflect-metadata` method does the reading, because the two behave differently with respect to the prototype chain — and, as of NestJS 12.1, on *which class actually owns the constructor being resolved*:\n\n- **`Reflect.getMetadata(key, target)`** walks up the prototype chain. NestJS still uses this for parameter *types* (`design:paramtypes`), so a subclass — with or without its own constructor — always sees the correct inherited type.\n- **`Reflect.getOwnMetadata(key, target)`** does not walk the chain. NestJS 12 uses this for `@Optional()` markers, but it's called on **the class that declares the constructor being resolved**, not blindly on the exact subclass. For a class with no constructor of its own, that owner is the nearest ancestor that does declare one — so the marker is still found. For a class that declares its own constructor, that owner is the class itself — so a marker that exists only on the parent is not found.\n\nNestJS's own [migration guide](https:\u002F\u002Fdocs.nestjs.com\u002Fmigration-guide) states it this way: *\"Nest now reads the optional markers of constructor parameters from the class that declares the constructor. A subclass that declares its own constructor no longer inherits the markers of its parent's constructor... Starting with NestJS v12.1, a subclass without its own constructor keeps the optional markers of its parent's constructor, as in v11.\"* Parameter *types* are unaffected either way — they're read through the separate, always-chain-walking `design:paramtypes` mechanism — so Nest still correctly sees that `TenantApiKeyGuard` takes one constructor argument of type `ApiKeyModuleOptions`; once that subclass has its own constructor, it just no longer sees that argument as optional.\n\n**Key concept:** the same two facts — \"this parameter exists\" and \"this parameter is optional\" — are stored as two different kinds of metadata, read with two different `reflect-metadata` calls. Declaring your own constructor doesn't change where parameter *types* come from. It does change where the *optional marker* is looked up: from \"wherever an ancestor happened to define it\" to \"the class that now owns this constructor\" — which, now that you've written one, is you.\n\n## Stage 1: a base class with one optional dependency\n\nStart from the smallest version of the base guard, with nothing inherited yet:\n\n```ts\n@Injectable()\nexport class BaseApiKeyGuard implements CanActivate {\n  constructor(@Optional() protected options?: ApiKeyModuleOptions) {}\n\n  canActivate(): boolean {\n    return Boolean(this.options?.headerName ?? 'x-api-key');\n  }\n}\n```\n\nUsed directly (`@UseGuards(BaseApiKeyGuard)`), this behaves identically in v11 and v12: `@Optional()` metadata is defined on `BaseApiKeyGuard` itself, and whatever reads it — chain-walking or not — finds it right there, no prototype walk required.\n\n## Stage 2: the childless subclass — safe in v11 and v12\n\nAdd the subclass that exists purely to give the feature module its own injectable token:\n\n```ts\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {}\n```\n\nNo constructor, no new behavior — this is the entire class body. Register it in a module, inject it with `@UseGuards(TenantApiKeyGuard)`, and this boots normally on **both** NestJS 11 and NestJS 12.1+. `TenantApiKeyGuard` has no constructor of its own, so Nest's lookup for the optional marker walks up to find the nearest class that *does* declare one — `BaseApiKeyGuard` — and reads the marker from there. This is exactly the common pattern you'll recognize from `@nestjs\u002Fpassport`, `class JwtAuthGuard extends AuthGuard('jwt') {}`, and it was never the trap here.\n\n(For a few weeks, it was: NestJS `12.0.0` through `12.0.4` read the optional marker directly off the exact subclass with no such fallback, which broke this childless case too. That was a regression against a widely-used pattern, not the intended design, and it was fixed in `12.1.0`. If you're on an exact `12.0.x` pin, this stage can fail for you — upgrading to `12.1+` is the fix, not changing your class.)\n\n## Stage 3: add a constructor, lose the marker\n\nNow give `TenantApiKeyGuard` a constructor — for a reason as ordinary as forwarding the same parameter explicitly:\n\n```ts\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(options?: ApiKeyModuleOptions) {\n    super(options);\n  }\n}\n```\n\nNo behavior changed. The parameter is still forwarded to the exact same place. But bootstrap now throws on NestJS 12 (12.1+, the version this article targets):\n\n```\nError: Nest can't resolve dependencies of the TenantApiKeyGuard (?).\nPlease make sure that the argument at index [0] is available in the AuthModule context.\n```\n\nPer the mental model above: `TenantApiKeyGuard` now declares its own constructor, so it is the class the optional-marker lookup reads from — and it never had `@Optional()` called on its own constructor's parameter. The parameter type is still inherited (Nest knows it's an `ApiKeyModuleOptions`), but the optional flag is not. A parameter that looks required, with no provider for `ApiKeyModuleOptions` anywhere in `AuthModule`, is an unresolvable dependency, which `UnknownDependenciesException` reports.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-optional-not-inherited\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Stage 4: the fix, and why it's deliberate\n\nThe fix is to re-declare `@Optional()` on the subclass's own constructor parameter:\n\n```ts\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(@Optional() options?: ApiKeyModuleOptions) {\n    super(options);\n  }\n}\n```\n\nThis works on v12.1+ (and still works on v11) because the parameter decorator now runs *on* `TenantApiKeyGuard` directly — the lookup no longer needs to find an ancestor's marker, because the marker is right there on the class that owns the constructor.\n\nIt's worth taking the migration guide's own framing seriously here: *\"This is deliberate: a dependency that was genuinely missing used to resolve to `undefined` silently.\"* Before this change, once you added a constructor to a subclass and simply forgot to re-mark a forwarded parameter, the mistake was invisible — the old chain-walking behavior would still find the ancestor's marker and quietly pass `undefined`, with nothing in the logs to say the subclass's own constructor made no promise about that parameter at all. `UnknownDependenciesException` is a less convenient failure mode than silent `undefined`, but it's a more honest one.\n\n## Edge cases and gotchas\n\n- **The break is specific to a subclass that declares its own constructor.** A childless subclass — the common `class X extends Y {}` or `class JwtAuthGuard extends AuthGuard('jwt') {}` shape — is unaffected on 12.1+.\n- **This is narrower than \"subclassing is risky.\"** Regular method inheritance, property inheritance, and `instanceof` checks are untouched. Only the DI container's optional-parameter lookup changed, and only once a subclass's own constructor exists.\n- **Multi-level inheritance follows the same rule, not a worse one.** If `GrandchildGuard extends TenantApiKeyGuard extends BaseApiKeyGuard`, and only `TenantApiKeyGuard` declares a constructor, a childless `GrandchildGuard` is safe — the lookup walks past it to `TenantApiKeyGuard`, the nearest class that owns a constructor, and reads the marker there (not from `BaseApiKeyGuard`, which is one link further).\n- **The 12.0.0–12.0.4 window is a real but temporary trap.** If you're reading an older blog post, Stack Overflow answer, or internal wiki page that describes the childless subclass as the broken case, it was likely written against one of those four patch releases, where that was briefly true. The documented, intended behavior since 12.1.0 is the one in this article.\n- **Node.js version matters for the same upgrade.** NestJS 12 requires **Node.js v20.19+ or v22.12+** (per the official migration guide's Node.js requirements) because the ESM-packaged core relies on Node's `require(esm)` interop — unrelated to this specific bug, but the same upgrade that triggers it will also fail outright on an older Node runtime, which is worth ruling out first if bootstrap errors look different from the one shown here.\n- **This isn't limited to guards.** Any `@Injectable()` subclass — a repository base class, a strategy base class, an interceptor base class — that adds a constructor forwarding an optional parameter from its parent without redeclaring `@Optional()` has the identical exposure.\n\n## Best practices\n\n- **Grep for recently-added constructors on subclasses of your own injectable base classes**, especially after a v12 upgrade: any `class X extends Y { constructor(...) { super(...); } }` where `Y`'s constructor has an `@Optional()` parameter, and `X`'s constructor doesn't repeat it, is a candidate.\n- **Treat \"adding a constructor to a subclass\" as a checkpoint, not a no-op.** The moment a subclass gets its own constructor, every parameter it forwards from the parent needs its own decorators re-declared — optional markers included — because the subclass is now the class Nest reads them from.\n- **Prefer composition over inheritance for DI-bearing base classes** where you can. A `TenantApiKeyGuard` that takes a `BaseApiKeyGuard` as an injected collaborator, rather than extending it, never has this problem to begin with, because there's no constructor-ownership handoff for `@Optional()` metadata to trip over.\n- **Run `nest upgrade`** when you move to v12 — the CLI's upgrade command is built to flag this class of change, and catching it at upgrade time is cheaper than catching it at a production boot failure.\n- **Don't \"fix\" this by making the dependency required instead.** That trades a confusing error for a real one at the cost of forcing every consumer to provide `ApiKeyModuleOptions`, even ones that were correctly relying on the default. Redeclare `@Optional()`, don't remove it.\n- **Pin to `12.1.0` or later, not an exact `12.0.x` release**, if you're locking a specific NestJS 12 version. The childless-subclass regression only existed in `12.0.0`–`12.0.4`.\n\n## FAQ\n\n### Does this affect property injection (`@Optional() @Inject() someProp`), or only constructor parameters?\n\nConstructor parameters are the mechanism this article covers, and it's also the one the migration guide documents. Property-based `@Optional()` injection is read through a different metadata key, and a subclass redeclaring its own `@Optional()` property does *not* lose the parent's separately-declared optional properties — that inheritance was specifically preserved in a later 12.1.x patch. Still, verify your own case against the current docs before assuming the two injection styles behave identically.\n\n### Is `@Optional()` metadata handling \"broken,\" or is this intentional?\n\nIntentional, per NestJS's own migration guide, for the case it targets: a subclass that declares its own constructor no longer silently inherits the parent's optional markers. The previous (v11) behavior let a parameter that genuinely had no provider resolve to `undefined` without any signal, as long as some ancestor class had marked it optional — regardless of whether the subclass's own constructor had any business making that same promise. The v12.1+ behavior surfaces that case as an explicit `UnknownDependenciesException` instead, while still correctly treating a childless subclass as making no promises of its own to override.\n\n### Will `nest upgrade` fix this for me automatically?\n\nThe `nest upgrade` command (new in the v12 CLI) is designed to apply migration steps and flag changes like this one for review, but a decorator-level fix — re-declaring `@Optional()` on a subclass's own constructor — is a code change specific to your class hierarchy. Treat the tooling as a way to *find* affected classes faster, not as something that edits them for you unsupervised.\n\n### If I just added a constructor to log something, with no new dependencies, do I still need to redeclare `@Optional()`?\n\nYes, if that constructor forwards a parameter the parent marked optional. The trigger isn't \"you added a new dependency\" — it's \"this subclass now has a constructor at all.\" Even a constructor that only forwards existing parameters and adds a `console.log` moves the optional-marker lookup to the subclass itself.\n\n## Cheat sheet\n\n| Situation | v11 | v12 (12.1+) |\n| --- | --- | --- |\n| Subclass has no constructor of its own | Inherited `@Optional()` applies | Inherited `@Optional()` still applies (fixed in 12.1.0; briefly broken only in 12.0.0–12.0.4) |\n| Subclass declares its own constructor without redeclaring `@Optional()` | Inherited `@Optional()` still applies | Inherited `@Optional()` is ignored — parameter looks required |\n| Subclass declares its own constructor and redeclares `@Optional()` | Works | Works |\n| Parameter *types* across the subclass boundary | Inherited | Still inherited (unchanged) |\n\n```ts\n\u002F\u002F The pattern to use any time you give a subclass its own constructor\n\u002F\u002F that forwards a parameter the parent class marked @Optional().\n@Injectable()\nexport class TenantApiKeyGuard extends BaseApiKeyGuard {\n  constructor(@Optional() options?: ApiKeyModuleOptions) {\n    super(options); \u002F\u002F forward it — don't just swallow the param\n  }\n}\n```\n\n- `Reflect.getOwnMetadata(key, Target)` → own metadata only, no prototype walk.\n- `Reflect.getMetadata(key, Target)` → walks the prototype chain.\n- NestJS 12.1+ reads `@Optional()` constructor markers from whichever class declares the constructor being resolved (`getOwnMetadata` on that owner) — not blindly from the literal subclass. Parameter *types* still use the chain-walking `getMetadata`.\n- A childless subclass is safe. A subclass with its own constructor must redeclare `@Optional()` for every forwarded optional parameter.\n- Requires Node.js **v20.19+** or **v22.12+**.\n- Verified against `@nestjs\u002Fcore` **12.1.2** — re-check `npm view @nestjs\u002Fcore dist-tags` before relying on a patch-version detail, and avoid pinning an exact `12.0.x` release.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-optional-not-inherited\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## Key takeaways\n\n- NestJS 12.1+ reads `@Optional()` constructor markers from whichever class declares the constructor being resolved, using `Reflect.getOwnMetadata` on that owner — not blindly from the exact subclass instance.\n- A subclass with **no** constructor of its own still inherits the marker correctly (the lookup walks up to find the owner); a subclass that **declares its own constructor** does not, unless it redeclares `@Optional()` itself.\n- Parameter *types* are read differently and are still inherited either way, so Nest still sees the parameter — it just no longer sees it as optional once the subclass owns its own constructor, which is why the error is \"can't resolve,\" not \"unknown parameter.\"\n- NestJS 12.0.0–12.0.4 briefly broke the childless-subclass case too, as a regression; that was fixed in 12.1.0 and is not the intended or current rule.\n- The fix is always the same shape: redeclare `@Optional()` on the subclass's own constructor and forward to `super()`.\n\nA guard whose only recent change was a constructor added purely to forward an existing parameter — that's what made the `TenantApiKeyGuard` error so disorienting to debug the first time, because the instinct is to suspect the *new* code, not the fact that adding a constructor at all moved where Nest looks for an old marker. Once you know the optional marker now travels with whoever owns the constructor, it stops being mysterious: audit every injectable subclass the moment it gains a constructor of its own. Have you hit this one yet on a v12 upgrade — and did your first guess point at the right class?\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [NestJS Guards: CanActivate, ExecutionContext & Reflector](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-guards-canactivate-reflector)\n- [NestJS Request Lifecycle Explained (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-request-lifecycle)\n- [NestJS Testing Module: Provider Overrides (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-testing-provider-overrides)\n\n\u003C!-- related:end -->\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":110,"canonical":474,"description":111},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnestjs-weekly-optional-not-inherited","01a11150-cc6e-767b-8731-e2db60bcbad7",{"name":477,"part":478,"total":478,"items":479},"NestJS Deep Dive",8,[480,485,489,493,497,501,505,509],{"slug":481,"title":482,"publishedAt":483,"readingMinutes":484},"nestjs-weekly-request-lifecycle","NestJS Request Lifecycle Explained (with Cheat Sheet)","2026-08-28T17:01:41.941Z",13,{"slug":486,"title":487,"publishedAt":488,"readingMinutes":103},"nestjs-weekly-dependency-injection-providers-scopes","NestJS Dependency Injection Explained (with Cheat Sheet)","2026-09-13T08:40:18.434Z",{"slug":490,"title":491,"publishedAt":492,"readingMinutes":96},"nestjs-weekly-module-encapsulation-exports","NestJS Module Encapsulation Explained (with Cheat Sheet)","2026-09-13T08:40:50.070Z",{"slug":494,"title":495,"publishedAt":496,"readingMinutes":484},"nestjs-weekly-testing-provider-overrides","NestJS Testing Module: Provider Overrides (with Cheat Sheet)","2026-09-18T17:23:03.842Z",{"slug":498,"title":499,"publishedAt":500,"readingMinutes":103},"nestjs-weekly-guards-canactivate-reflector","NestJS Guards: CanActivate, ExecutionContext & Reflector","2026-09-26T12:25:39.001Z",{"slug":502,"title":503,"publishedAt":504,"readingMinutes":103},"nestjs-weekly-standard-schema-validation","NestJS 12: Standard Schema Validation Without class-validator","2026-10-02T06:11:02.679Z",{"slug":506,"title":507,"publishedAt":508,"readingMinutes":484},"nestjs-weekly-lifecycle-hook-order","NestJS 12 Lifecycle Hooks: The Order You Can't Assume","2026-10-23T06:10:32.276Z",{"slug":46,"title":110,"publishedAt":113,"readingMinutes":96},{"id":511,"locked":18},"01a11150-cc97-71fc-98dc-19054d7d9900",[513],{"id":45,"slug":46,"title":48,"_count":514},{"questions":51},[516],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":518,"questionCount":51},{"questions":51},[]]