[{"data":1,"prerenderedAt":3480},["ShallowReactive",2],{"study-navigation":3,"question-bank":321,"study:\u002Fstudy\u002Fmemory-leaks\u002Fthree-lifetimes":2306,"study-surround:\u002Fstudy\u002Fmemory-leaks\u002Fthree-lifetimes":3477},[4],{"title":5,"path":6,"stem":7,"children":8,"page":320},"Study","\u002Fstudy","study",[9,81,133,175,202,234,266,293],{"title":10,"path":11,"stem":12,"children":13,"description":15},"Nuxt Layers","\u002Fstudy\u002Flayers","study\u002F1.layers\u002Findex",[14,16,21,26,31,36,41,46,51,56,61,66,71,76],{"title":10,"path":11,"stem":12,"description":15},"What a layer is, why a frontend team standardises on them, and the mental model that makes every other page in this section obvious.",{"title":17,"path":18,"stem":19,"description":20},"Anatomy of a layer","\u002Fstudy\u002Flayers\u002Fanatomy","study\u002F1.layers\u002F01.anatomy","A layer is a Nuxt app directory that another app merges in. What goes where, how Nuxt resolves its srcDir, and how to see the resolved layer list.",{"title":22,"path":23,"stem":24,"description":25},"extends and layer sources","\u002Fstudy\u002Flayers\u002Fextends-and-sources","study\u002F1.layers\u002F02.extends-and-sources","The three places a layer can come from, the giget syntax for git layers, private repositories, and the auto-registered layers\u002F directory.",{"title":27,"path":28,"stem":29,"description":30},"Priority and merging","\u002Fstudy\u002Flayers\u002Fpriority-and-merging","study\u002F1.layers\u002F03.priority-and-merging","Which layer wins when two define the same file or option, how c12 and defu merge configs, and how to prove the order instead of memorising it.",{"title":32,"path":33,"stem":34,"description":35},"Paths and aliases inside a layer","\u002Fstudy\u002Flayers\u002Fpaths-and-aliases","study\u002F1.layers\u002F04.paths-and-aliases","Why ~ and @ break inside a layer, the import.meta.url pattern, the",{"title":37,"path":38,"stem":39,"description":40},"Config merging in practice","\u002Fstudy\u002Flayers\u002Fconfig-merging","study\u002F1.layers\u002F05.config-merging","How modules, runtimeConfig, app.config, route rules, Vite options and environment overrides behave when a layer and a project both declare them.",{"title":42,"path":43,"stem":44,"description":45},"Overriding what a layer ships","\u002Fstudy\u002Flayers\u002Foverriding","study\u002F1.layers\u002F06.overriding","How a project replaces, wraps, extends or removes components, pages, layouts, composables, plugins, middleware and server routes inherited from a layer.",{"title":47,"path":48,"stem":49,"description":50},"Modules in layers","\u002Fstudy\u002Flayers\u002Fmodules-in-layers","study\u002F1.layers\u002F07.modules-in-layers","Layers as module presets, how Nuxt deduplicates modules across layers, how module options merge, and how module authors make their modules layer-aware.",{"title":52,"path":53,"stem":54,"description":55},"Server code and Nitro in layers","\u002Fstudy\u002Flayers\u002Fserver-and-nitro","study\u002F1.layers\u002F08.server-and-nitro","What a layer can ship under server\u002F and shared\u002F, how Nitro merges handlers and config across layers, and why every route you ship is a security decision for every app.",{"title":57,"path":58,"stem":59,"description":60},"Publishing and consuming a layer","\u002Fstudy\u002Flayers\u002Fpublishing-and-consuming","study\u002F1.layers\u002F09.publishing-and-consuming","The layer starter and its playground, npm packaging rules, workspace versus registry versus git tags, the dev experience of each, and what counts as a breaking change.",{"title":62,"path":63,"stem":64,"description":65},"Testing a layer and CI","\u002Fstudy\u002Flayers\u002Ftesting-and-ci","study\u002F1.layers\u002F10.testing-and-ci","The playground as a test bench, fixture apps that extend the layer, asserting that overrides win, type-checking, and a CI matrix that catches Nuxt version skew before consumers do.",{"title":67,"path":68,"stem":69,"description":70},"Layers vs modules, and layers as architecture","\u002Fstudy\u002Flayers\u002Flayers-vs-modules","study\u002F1.layers\u002F11.layers-vs-modules","The decision rule, the cases where a team needs both, domain-driven design with one layer per domain, and the places where a layer is the wrong tool.",{"title":72,"path":73,"stem":74,"description":75},"Pitfalls catalogue","\u002Fstudy\u002Flayers\u002Fpitfalls","study\u002F1.layers\u002F12.pitfalls","Eighteen ways layers go wrong in practice, each with the symptom you will see, the cause underneath and the fix, plus code for the six that cost the most time.",{"title":77,"path":78,"stem":79,"description":80},"Designing a team layer","\u002Fstudy\u002Flayers\u002Fdesigning-a-team-layer","study\u002F1.layers\u002F13.designing-a-team-layer","The senior-craft page for layers - defining a small public surface, theming through app.config, override recipes, performance and security costs per app, governance, and rolling out breaking changes.",{"title":82,"path":83,"stem":84,"children":85,"description":87},"Module authoring","\u002Fstudy\u002Fmodule-authoring","study\u002F2.module-authoring\u002Findex",[86,88,93,98,103,108,113,118,123,128],{"title":82,"path":83,"stem":84,"description":87},"What a Nuxt module really is, the three worlds every line of module code lives in, the three primitives everything reduces to, and why a Layers role is a module role in disguise.",{"title":89,"path":90,"stem":91,"description":92},"The mental model","\u002Fstudy\u002Fmodule-authoring\u002Fmental-model","study\u002F2.module-authoring\u002F01.mental-model","What defineNuxtModule does with your definition, the three worlds a line of module code can live in, how Nuxt orders and deduplicates modules, and when onInstall and onUpgrade fire.",{"title":94,"path":95,"stem":96,"description":97},"The kit API, grouped","\u002Fstudy\u002Fmodule-authoring\u002Fkit-api","study\u002F2.module-authoring\u002F02.kit-api","The @nuxt\u002Fkit helpers a module author should know from memory, grouped by what they do, with what each one does underneath and the source files worth reading.",{"title":99,"path":100,"stem":101,"description":102},"Crossing the boundary","\u002Fstudy\u002Fmodule-authoring\u002Foptions-to-runtime","study\u002F2.module-authoring\u002F03.options-to-runtime","The four channels that carry a module option from the build world into the browser — public and private runtime config, appConfig and generated templates — and how to choose between them.",{"title":104,"path":105,"stem":106,"description":107},"Rules of the runtime directory","\u002Fstudy\u002Fmodule-authoring\u002Fruntime-directory","study\u002F2.module-authoring\u002F04.runtime-directory","What src\u002Fruntime may and may not import, how module-builder transpiles it file by file, the .client\u002F.server suffixes, and how to make Nitro inline your server runtime so",{"title":109,"path":110,"stem":111,"description":112},"Hooks","\u002Fstudy\u002Fmodule-authoring\u002Fhooks","study\u002F2.module-authoring\u002F05.hooks","How unjs\u002Fhookable powers the three hook families, a goal-to-hook cheat sheet for module authors, and how to declare your own hooks so other modules can cooperate with yours.",{"title":114,"path":115,"stem":116,"description":117},"Templates and types","\u002Fstudy\u002Fmodule-authoring\u002Ftemplates-and-types","study\u002F2.module-authoring\u002F06.templates-and-types","Generating code into .nuxt with addTemplate, adding declarations with addTypeTemplate, regenerating only your own templates on watch, and getting the nuxt\u002Fschema augmentation right.",{"title":119,"path":120,"stem":121,"description":122},"Dependencies and composition","\u002Fstudy\u002Fmodule-authoring\u002Fdependencies-and-composition","study\u002F2.module-authoring\u002F07.dependencies-and-composition","installModule versus the declarative moduleDependencies field, detecting other modules with hasNuxtModule, choosing dependency versus peerDependency, and cooperating through hooks.",{"title":124,"path":125,"stem":126,"description":127},"Testing a module","\u002Fstudy\u002Fmodule-authoring\u002Ftesting","study\u002F2.module-authoring\u002F08.testing","The module test pyramid with @nuxt\u002Ftest-utils — runtime unit tests, fixture E2E, build-level assertions, type tests, a nightly CI matrix and the non-functional tests a toolkit needs.",{"title":129,"path":130,"stem":131,"description":132},"Building, versioning and publishing","\u002Fstudy\u002Fmodule-authoring\u002Fbuilding-and-publishing","study\u002F2.module-authoring\u002F09.building-and-publishing","What nuxt-module-build emits, the package.json fields that decide whether your module resolves, semver for modules, deprecating an option safely, and getting listed on nuxt.com\u002Fmodules.",{"title":134,"path":135,"stem":136,"children":137,"description":139},"Under the hood","\u002Fstudy\u002Funder-the-hood","study\u002F3.under-the-hood\u002Findex",[138,140,145,150,155,160,165,170],{"title":134,"path":135,"stem":136,"description":139},"The startup sequence, the server engine, the unjs toolbelt, auto-imports and build-time transforms — the machinery a layer and module author has to reason about rather than use.",{"title":141,"path":142,"stem":143,"description":144},"The startup sequence","\u002Fstudy\u002Funder-the-hood\u002Fstartup-sequence","study\u002F3.under-the-hood\u002F01.startup-sequence","What actually happens between typing nuxt dev and getting a URL — c12 config loading, schema resolution, the hookable instance, module order, app resolution, the Vite and Nitro builds.",{"title":146,"path":147,"stem":148,"description":149},"Nitro and h3","\u002Fstudy\u002Funder-the-hood\u002Fnitro-and-h3","study\u002F3.under-the-hood\u002F02.nitro-and-h3","The server engine underneath Nuxt — presets and .output, route rules, cached handlers, storage, per-request state, server plugins, and how a module extends all of it.",{"title":151,"path":152,"stem":153,"description":154},"The unjs toolbelt","\u002Fstudy\u002Funder-the-hood\u002Funjs-toolbelt","study\u002F3.under-the-hood\u002F03.unjs-toolbelt","The unjs packages Nuxt is assembled from, what each one does for a module or layer author, and where you actually meet them while writing a toolkit.",{"title":156,"path":157,"stem":158,"description":159},"Auto-imports and how they bite","\u002Fstudy\u002Funder-the-hood\u002Fauto-imports","study\u002F3.under-the-hood\u002F04.auto-imports","How unimport builds two separate registries and rewrites your identifiers at build time, why module runtime code must import from",{"title":161,"path":162,"stem":163,"description":164},"Vite, unplugin and build-time transforms","\u002Fstudy\u002Funder-the-hood\u002Fvite-and-transforms","study\u002F3.under-the-hood\u002F05.vite-and-transforms","The Vite plugin hooks a module author uses, how addBuildPlugin and unplugin give one implementation for every builder, source-map-safe edits with magic-string, and dev-server hygiene.",{"title":166,"path":167,"stem":168,"description":169},"Rendering modes and data","\u002Fstudy\u002Funder-the-hood\u002Frendering-and-data","study\u002F3.under-the-hood\u002F06.rendering-and-data","Universal, SPA, hybrid and static rendering; islands and lazy hydration; the real option list for useAsyncData and useFetch; the payload, plugins and unhead.",{"title":171,"path":172,"stem":173,"description":174},"Layers vs modules","\u002Fstudy\u002Funder-the-hood\u002Flayers-vs-modules","study\u002F3.under-the-hood\u002F07.layers-vs-modules","The decision rule in one sentence, why a company toolkit is almost always both, and where the deep treatment lives.",{"title":176,"path":177,"stem":178,"children":179,"description":181},"Hydration errors","\u002Fstudy\u002Fhydration","study\u002F4.hydration\u002Findex",[180,182,187,192,197],{"title":176,"path":177,"stem":178,"description":181},"What hydration is, why a mismatch costs more than a console warning, and why the author of a layer or module owns the problem for every app that consumes it.",{"title":183,"path":184,"stem":185,"description":186},"What hydration is","\u002Fstudy\u002Fhydration\u002Fwhat-hydration-is","study\u002F4.hydration\u002F1.what-hydration-is","renderToString on the server, createSSRApp().mount() walking the DOM on the client, how Nuxt ships state so both renders agree, and what Vue does when they do not.",{"title":188,"path":189,"stem":190,"description":191},"Catalogue of causes","\u002Fstudy\u002Fhydration\u002Fcatalogue-of-causes","study\u002F4.hydration\u002F2.catalogue-of-causes","The eighteen ways a Nuxt app renders differently in Node and in the browser, what each one looks like in the console, and the Nuxt-native fix for each.",{"title":193,"path":194,"stem":195,"description":196},"Diagnosing, step by step","\u002Fstudy\u002Fhydration\u002Fdiagnosing","study\u002F4.hydration\u002F3.diagnosing","A seven-step workflow from reading the warning to a production build that talks, and the E2E test that turns the manual check into a CI gate.",{"title":198,"path":199,"stem":200,"description":201},"Author responsibilities","\u002Fstudy\u002Fhydration\u002Fauthor-responsibilities","study\u002F4.hydration\u002F4.author-responsibilities","The rules a layer or module author follows so that nothing they ship can cause a mismatch in a consumer's app, and how they prove it in the playground.",{"title":203,"path":204,"stem":205,"children":206,"description":208},"Memory leaks","\u002Fstudy\u002Fmemory-leaks","study\u002F5.memory-leaks\u002Findex",[207,209,214,219,224,229],{"title":203,"path":204,"stem":205,"description":208},"Why a Nuxt server leaks where an SPA does not, what a leak actually is, why cross-request state pollution is its security cousin, and why a layer or module author owns the risk for every consuming app.",{"title":210,"path":211,"stem":212,"description":213},"Three lifetimes","\u002Fstudy\u002Fmemory-leaks\u002Fthree-lifetimes","study\u002F5.memory-leaks\u002F01.three-lifetimes","Process, request and client session — what lives in each, who owns it, and the one question that tells you where a value belongs.",{"title":215,"path":216,"stem":217,"description":218},"Server-side leaks","\u002Fstudy\u002Fmemory-leaks\u002Fserver-side-leaks","study\u002F5.memory-leaks\u002F02.server-side-leaks","The thirteen ways a Nuxt\u002FNitro server holds on to request-lifetime objects, what each one looks like in production, and the before\u002Fafter for the ones that actually happen.",{"title":220,"path":221,"stem":222,"description":223},"Client-side leaks","\u002Fstudy\u002Fmemory-leaks\u002Fclient-side-leaks","study\u002F5.memory-leaks\u002F03.client-side-leaks","The eleven ways a long-lived browser tab keeps hold of unmounted components — listeners, timers, observers, third-party instances, orphaned effects and growing global state — with the Nuxt and VueUse fixes.",{"title":225,"path":226,"stem":227,"description":228},"Detecting and measuring","\u002Fstudy\u002Fmemory-leaks\u002Fdetecting-and-measuring","study\u002F5.memory-leaks\u002F04.detecting-and-measuring","How to prove a leak instead of guessing — health-route baselines, the Chrome DevTools snapshot workflow, production-safe Node flags, the navigate-20-times client test, and a memory watermark test for CI.",{"title":230,"path":231,"stem":232,"description":233},"Patterns to adopt","\u002Fstudy\u002Fmemory-leaks\u002Fpatterns-to-adopt","study\u002F5.memory-leaks\u002F05.patterns-to-adopt","The rules a layer or module author follows so the leak catalogues become someone else's reading — lifetimes, scopes, bounded caches, cleanup contracts, and the review checklist every PR goes through.",{"title":235,"path":236,"stem":237,"children":238,"description":240},"Performance","\u002Fstudy\u002Fperformance","study\u002F6.performance\u002Findex",[239,241,246,251,256,261],{"title":235,"path":236,"stem":237,"description":240},"Where the time actually goes in a Nuxt app, the senior answer shape, and why a layer author's first performance question is what the layer costs every consuming app.",{"title":242,"path":243,"stem":244,"description":245},"How to talk about performance","\u002Fstudy\u002Fperformance\u002Fhow-to-talk-about-performance","study\u002F6.performance\u002F01.how-to-talk-about-performance","The five layers, the instrument for each, and how to structure an answer so an interviewer hears judgement rather than a list of tricks.",{"title":247,"path":248,"stem":249,"description":250},"Build and dev performance","\u002Fstudy\u002Fperformance\u002Fbuild-and-dev","study\u002F6.performance\u002F02.build-and-dev","What module setup, pre-bundling and template generation cost your teammates, how to measure build time properly, and the levers that actually move it.",{"title":252,"path":253,"stem":254,"description":255},"Server-side performance","\u002Fstudy\u002Fperformance\u002Fserver-side","study\u002F6.performance\u002F03.server-side","Waterfalls, payload size, the caching layers Nitro gives you, and the rule that keeps caching from becoming a security incident.",{"title":257,"path":258,"stem":259,"description":260},"Client-side performance","\u002Fstudy\u002Fperformance\u002Fclient-side","study\u002F6.performance\u002F04.client-side","Code splitting, lazy hydration, images and fonts, third-party scripts, and the Core Web Vitals each of them moves.",{"title":262,"path":263,"stem":264,"description":265},"The author's performance checklist","\u002Fstudy\u002Fperformance\u002Fauthor-checklist","study\u002F6.performance\u002F05.author-checklist","What a layer or module costs every consuming app, how to measure that cost, and how to keep it in CI so it cannot grow unnoticed.",{"title":267,"path":268,"stem":269,"children":270,"description":272},"Senior craft","\u002Fstudy\u002Fsenior-craft","study\u002F7.senior-craft\u002Findex",[271,273,278,283,288],{"title":267,"path":268,"stem":269,"description":272},"What changes when the thing you build is used by other teams rather than by end users - API design you cannot easily take back, release discipline, security, and review.",{"title":274,"path":275,"stem":276,"description":277},"Designing the API","\u002Fstudy\u002Fsenior-craft\u002Fdesigning-the-api","study\u002F7.senior-craft\u002F01.designing-the-api","Options, conventions, escape hatches and documentation for a module, and the equivalent public surface for a layer.",{"title":279,"path":280,"stem":281,"description":282},"Team process, security and operations","\u002Fstudy\u002Fsenior-craft\u002Fteam-process-and-security","study\u002F7.senior-craft\u002F02.team-process-and-security","Monorepo layout, release discipline, the security posture expected of someone who ships code into every app, and how support actually works.",{"title":284,"path":285,"stem":286,"description":287},"Code-review checklist","\u002Fstudy\u002Fsenior-craft\u002Fcode-review-checklist","study\u002F7.senior-craft\u002F03.code-review-checklist","The two checklists a platform team reviews against - one for modules, one for layers - and the reasoning behind each line.",{"title":289,"path":290,"stem":291,"description":292},"Rollout and breaking changes","\u002Fstudy\u002Fsenior-craft\u002Frollout-and-breaking-changes","study\u002F7.senior-craft\u002F04.rollout-and-breaking-changes","What counts as breaking for a layer and for a module, the deprecate-warn-codemod-remove sequence, and how to roll a change out to twenty apps without an incident.",{"title":294,"path":295,"stem":296,"children":297,"description":299},"The 2026 landscape","\u002Fstudy\u002Flandscape-2026","study\u002F8.landscape-2026\u002Findex",[298,300,305,310,315],{"title":294,"path":295,"stem":296,"description":299},"The version snapshot as of September 2026, why an interviewer tests currency, and how to talk about what changed without reciting release notes.",{"title":301,"path":302,"stem":303,"description":304},"What Nuxt 4.5 changed","\u002Fstudy\u002Flandscape-2026\u002Fnuxt-4-5","study\u002F8.landscape-2026\u002F01.nuxt-4-5","Vite 8 on Rolldown, the Rspack builder, experimental SSR streaming and the security releases, read from a layer and module author's point of view.",{"title":306,"path":307,"stem":308,"description":309},"The Nuxt 5 changes","\u002Fstudy\u002Flandscape-2026\u002Fnuxt-5-changes","study\u002F8.landscape-2026\u002F02.nuxt-5-changes","Every behaviour change the upgrade guide documents, each paired with what a layer or module author has to audit.",{"title":311,"path":312,"stem":313,"description":314},"Vue 3.5 and the ecosystem","\u002Fstudy\u002Flandscape-2026\u002Fvue-and-ecosystem","study\u002F8.landscape-2026\u002F03.vue-and-ecosystem","The Vue features that matter for SSR, the status of 3.6, the modules an interviewer expects you to know by name, and the CLI commands you should use without thinking.",{"title":316,"path":317,"stem":318,"description":319},"The v5 readiness plan","\u002Fstudy\u002Flandscape-2026\u002Fv5-readiness-plan","study\u002F8.landscape-2026\u002F04.v5-readiness-plan","A concrete checklist for making a module and a layer Nuxt 5 ready, and turning the findings into an interview story.",false,[322,336,349,366,384,396,411,423,439,449,458,474,482,492,501,510,519,535,548,561,571,578,587,600,608,618,628,635,650,660,674,685,698,706,713,726,739,746,758,767,777,792,805,813,823,832,841,854,863,871,882,891,896,907,913,927,933,940,950,962,969,976,984,996,1004,1010,1015,1021,1033,1041,1048,1054,1063,1070,1085,1090,1100,1111,1118,1123,1134,1145,1152,1157,1165,1181,1189,1194,1204,1216,1221,1227,1232,1241,1257,1262,1272,1279,1287,1295,1306,1312,1322,1333,1339,1350,1361,1366,1371,1380,1385,1396,1406,1417,1427,1438,1449,1454,1465,1475,1480,1492,1502,1515,1522,1531,1543,1550,1560,1576,1583,1593,1599,1611,1622,1633,1641,1650,1660,1673,1683,1692,1697,1703,1714,1722,1728,1735,1742,1749,1756,1768,1780,1791,1806,1811,1822,1832,1847,1852,1863,1869,1878,1889,1900,1910,1917,1926,1933,1944,1953,1958,1964,1972,1982,1993,2000,2009,2018,2028,2039,2050,2055,2060,2066,2075,2080,2091,2098,2104,2111,2117,2128,2137,2146,2157,2163,2170,2181,2188,2194,2206,2212,2225,2235,2246,2254,2261,2270,2282,2288,2300],{"id":323,"type":324,"difficulty":325,"subsection":326,"tags":327,"prompt":329,"answer":330,"docs":331,"section":335,"sectionTitle":10},"layers-001","open",1,"index",[328],"fundamentals","What is a Nuxt layer, and how is it different from a Nuxt module?\n","A layer is a partial Nuxt application that another app extends. It ships **conventions and app code**: a `nuxt.config.ts` preset (modules, `runtimeConfig` defaults, route rules), `app.config.ts`, components, composables, pages, layouts, middleware, plugins and `server\u002F` handlers. Nuxt scans every layer's directories with the same scanners it uses for the project and deep-merges every layer's config with c12 + defu, the project sitting at `_layers[0]` with the highest priority. Nothing in a layer *executes* at build time; it is merged.\n\nA module is **build-time code**: a function that runs once in Node during `nuxt dev`\u002F`nuxt build`, mutates `nuxt.options`, registers hooks and injects or generates files. If you catch yourself wanting an `if` inside a layer, you want a module, or a config option the consumer sets. A team toolkit is usually both: a layer that lists the module in its `modules`, so consumers get everything from one `extends` entry.\n",[332],{"title":333,"url":334},"Layers — getting started","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fgetting-started\u002Flayers","layers",{"id":337,"type":338,"difficulty":325,"subsection":326,"tags":339,"prompt":340,"options":341,"correct":346,"answer":348,"section":335,"sectionTitle":10},"layers-002","mcq",[328],"Which of these is something a layer **cannot** do on its own?\n",[342,343,344,345],"Ship `server\u002Fapi\u002F*` route handlers","Provide default `runtimeConfig` values through its `nuxt.config.ts`","Run code at build time to generate a virtual file","Register Nuxt modules through its `nuxt.config.ts`",[347],2,"A layer declares files and config; nothing in it runs at build time. Generating virtual files, transforming code or reacting to build hooks is a module's job. A layer can, however, list modules in its `nuxt.config`, which is how a preset layer pulls in the build-time behaviour it needs, and Nuxt deduplicates a module that several layers declare. Server routes and `runtimeConfig` defaults are plain files and config, so both are ordinary layer content.\n",{"id":350,"type":351,"difficulty":347,"subsection":352,"language":353,"tags":354,"prompt":358,"starter":359,"solution":360,"answer":361,"docs":362,"section":335,"sectionTitle":10},"layers-003","code","extends-and-sources","ts",[355,356,357],"extends","giget","config","Write the `nuxt.config.ts` of a consuming app that extends three layers: a local `..\u002Fbase` layer, the npm package `@team\u002Fnuxt-layer-auth`, and the GitHub repository `team\u002Fnuxt-layer-analytics` pinned to tag `v2.1.0`. The git layer must also install its own dependencies, and you want to import from it later as `#layers\u002Fanalytics`.\n","export default defineNuxtConfig({\n  \u002F\u002F TODO\n})\n","export default defineNuxtConfig({\n  extends: [\n    '..\u002Fbase',\n    '@team\u002Fnuxt-layer-auth',\n    ['github:team\u002Fnuxt-layer-analytics#v2.1.0', {\n      install: true,\n      meta: { name: 'analytics' },\n    }],\n  ],\n})\n","`extends` accepts strings or `[source, options]` tuples. Local paths are relative to the project's `nuxt.config.ts`; npm layers are referenced by package name and resolved from `node_modules` (their `main` points at `nuxt.config.ts`); git sources use the giget syntax `github:owner\u002Frepo#ref` (also `gitlab:`, `bitbucket:`, and `\u002Fsub\u002Fdir`). `install: true` makes c12 install the downloaded layer's dependencies, `auth` (or the `GIGET_AUTH` env var) unlocks private repositories, and `meta.name` names the layer so the `#layers\u002Fanalytics` alias exists. Say out loud that among these three the **first** entry has the highest priority, that any `layers\u002F*` folder would sit above all of them, and that the project wins over everything.\n",[363],{"title":364,"url":365},"Authoring Nuxt layers — publishing","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fgoing-further\u002Flayers",{"id":367,"type":338,"difficulty":325,"subsection":368,"tags":369,"prompt":371,"options":372,"correct":377,"answer":379,"docs":380,"section":335,"sectionTitle":10},"layers-004","anatomy",[328,370],"dx","You create `layers\u002Ftheme\u002Fapp\u002Fcomponents\u002FThemeBadge.vue`, restart `nuxt dev`, and `\u003CThemeBadge>` is still unresolved. Nothing else was changed. What is the most likely cause?\n",[373,374,375,376],"`layers\u002Ftheme` has no `nuxt.config.ts`, so Nuxt does not treat the folder as a layer at all","Folders under `layers\u002F` must also be listed in `extends` before Nuxt scans them","Components inside a layer must be declared with `components: [{ path, prefix }]` in the layer's config","The layer's components directory must be `components\u002F` at the layer root, not `app\u002Fcomponents\u002F`",[378],0,"Every layer must have a `nuxt.config.ts`, even an empty one; without it the folder is silently ignored, which is why \"the layer does nothing\" is almost always this. `layers\u002F*` auto-registers since Nuxt 3.12 without any `extends` entry, a layer's `app\u002Fcomponents` is scanned with the default component directories (an explicit `components` entry is only needed for a prefix), and Nuxt resolves `srcDir` per layer, so `app\u002Fcomponents\u002F` is the correct Nuxt 4 location.\n",[381],{"title":382,"url":383},"The layers\u002F directory","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Flayers",{"id":385,"type":324,"difficulty":347,"subsection":368,"tags":386,"prompt":388,"answer":389,"docs":390,"section":335,"sectionTitle":10},"layers-005",[328,387],"kit","Walk me through what Nuxt actually does with a layer directory: how it decides the layer's `srcDir`, what `nuxt.options._layers` contains, and why the layer starter template ships a `.playground\u002F` app.\n","c12 loads the layer's `nuxt.config.ts` and Nuxt applies the config schema defaults **per layer**, so each layer gets its own `rootDir` and `srcDir`. If the layer has an `app\u002F` folder that becomes its `srcDir` (Nuxt 4 layout); a layer still on Nuxt 3-style top-level `pages\u002F`, `components\u002F`, `plugins\u002F` keeps its root as `srcDir`, which is why layers of both generations coexist in one app. The ordinary scanners then collect components, composables, pages, layouts, middleware, plugins, `server\u002F` handlers, `shared\u002F` utils and types and `public\u002F` assets from those directories.\n\n`nuxt.options._layers` is the resolved list: index 0 is the project, then the layers in priority order, each with `cwd`, `config` (that layer's *own* config, not the merged one), `configFile` and `meta` (the `$meta.name`). Kit's `getLayerDirectories()` maps it to `{ root, app, server, shared, public, modules, appPages, appLayouts, appMiddleware, appPlugins }` per layer, project first.\n\nThe starter (`npm create nuxt -- --template layer`) ships `.playground\u002F` with `extends: ['..']` because nothing *runs* a layer; it is merged into somebody's app, and during development the playground is that somebody. `package.json` has `main: '.\u002Fnuxt.config.ts'` because that is what consumers resolve. The trap is that paths and `.env` values which happen to work while you develop inside the layer repository (running `nuxt dev` in the layer folder makes `~` point at the layer's own `app\u002F`) break in the first real consumer.\n",[391,394],{"title":392,"url":393},"Kit — getLayerDirectories","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fkit\u002Flayers",{"title":395,"url":365},"Authoring Nuxt layers",{"id":397,"type":338,"difficulty":347,"subsection":352,"tags":398,"prompt":400,"options":401,"correct":406,"answer":407,"docs":408,"section":335,"sectionTitle":10},"layers-006",[356,399,355],"c12","A consuming app has `extends: [['github:team\u002Fnuxt-layer-analytics#v2.1.0', { install: true }]]`. What does `install: true` change, and why does the option exist?\n",[402,403,404,405],"c12 runs the package manager inside the downloaded copy (under `node_modules\u002F.c12\u002F`) so the layer's own `dependencies` exist, because a remote layer is downloaded, not installed","It adds the layer to the project's `package.json` dependencies so the next `pnpm install` fetches it from the registry","It installs the layer into the project's `node_modules` like `pnpm add`, making its exports importable from app code","It forces a fresh download of the tag on every build instead of using the cache",[378],"Remote layers are fetched by giget into `node_modules\u002F.c12\u002F` and merged from there; your project's install never sees their `package.json`, so a layer that imports, say, `dayjs` fails until `install: true` runs the package manager in that directory. Even then the docs are explicit that you \"will not be able to access its dependencies outside of Nuxt\": they resolve from the layer's files, not from your app code. Nothing is added to your `package.json`, nothing lands in your own `node_modules`, and the cache is untouched. Teams that need real dependency management publish the layer to npm instead.\n",[409],{"title":410,"url":365},"Authoring Nuxt layers — git repository",{"id":412,"type":324,"difficulty":347,"subsection":352,"tags":413,"prompt":416,"answer":417,"docs":418,"section":335,"sectionTitle":10},"layers-007",[356,399,414,415],"ci","security","Walk me through what happens, step by step, when Nuxt resolves `extends: ['github:team\u002Fnuxt-layer-analytics#v2.1.0']` from a private repository in CI. What has to be in place, and what goes wrong most often?\n","`defineNuxtConfig` is loaded by c12, which walks `extends`. For a giget-style source (`github:`, `gitlab:`, `bitbucket:`, optional `\u002Fsub\u002Fdir` and `#ref`, default branch `main`) c12 calls giget's `downloadTemplate`, which fetches the tarball for that ref and unpacks it under `node_modules\u002F.c12\u002F`. c12 then loads *that* directory's `nuxt.config.ts` as a layer, resolves the layer's own `extends` recursively, and the merge continues as with any local layer.\n\nFor a private repository the download needs a token: `GIGET_AUTH=\u003Ctoken>` in the CI environment, or `auth: process.env.GITHUB_TOKEN` in the tuple form `[source, { auth }]`. Self-hosted instances are pointed at with `GIGET_GITHUB_URL` \u002F `GIGET_GITLAB_URL`. If the layer has runtime `dependencies`, add `install: true`, because a downloaded layer is not installed by your package manager.\n\nWhat goes wrong: a missing or under-scoped token surfaces as a failed download, not as a clear \"unauthorised\"; a moved tag keeps serving the cached copy under `node_modules\u002F.c12\u002F` until that folder is removed; and the layer's dependencies are still not resolvable from app code. Treat tags as immutable or pin a commit hash, keep the token in CI secrets rather than in the config, and move to npm once a layer has dependencies of its own.\n",[419,422],{"title":420,"url":421},"unjs\u002Fgiget","https:\u002F\u002Fgithub.com\u002Funjs\u002Fgiget",{"title":333,"url":334},{"id":424,"type":338,"difficulty":325,"subsection":425,"tags":426,"prompt":428,"options":429,"correct":434,"answer":435,"docs":436,"section":335,"sectionTitle":10},"layers-008","priority-and-merging",[427,355],"merge","A project has `extends: ['.\u002Fa', '.\u002Fb']` and also a `layers\u002Fc` folder. All three define `app\u002Fcomponents\u002FHero.vue`; the project does not. Which one renders?\n",[430,431,432,433],"`.\u002Fa`, because the first `extends` entry has the highest priority among all layers","`.\u002Fb`, because later `extends` entries override earlier ones, like the CSS cascade","`layers\u002Fc`, because auto-scanned `layers\u002F` folders sit above every `extends` entry","None: Nuxt fails the build because one component name resolves to three files",[347],"The documented order, highest first, is project files, then auto-scanned `~~\u002Flayers\u002F*` (alphabetical, Z above A), then `extends` entries with the first entry highest. `layers\u002Fc` therefore beats both `.\u002Fa` and `.\u002Fb`, and `.\u002Fa` would beat `.\u002Fb`. Same-name components across layers are not an error: the higher-priority file silently wins, which is exactly why you test the order in a playground instead of trusting intuition. Option 1 is right about `extends` order but wrong about it beating `layers\u002F`.\n",[437],{"title":438,"url":334},"Layers — getting started (priority order)",{"id":440,"type":351,"difficulty":325,"subsection":425,"language":441,"tags":442,"prompt":443,"starter":444,"solution":445,"answer":446,"docs":447,"section":335,"sectionTitle":10},"layers-009","bash",[427,370],"A project has `layers\u002Fbase`, `layers\u002Ftheme` and `layers\u002Fui`, all auto-scanned. You need `ui` to override `theme`, and `theme` to override `base`. Rename the folders so the alphabetical rule produces exactly that order and show the resulting `tree -L 2 layers`. The `#layers\u002Fbase` alias must keep working.\n","# current state\n# layers\u002F\n# ├── base\n# ├── theme\n# └── ui\n","# Auto-scanned layers\u002F are ordered alphabetically and *later* names win (Z beats A).\n# Numeric prefixes make the intended order explicit: 3.ui > 2.theme > 1.base.\nmv layers\u002Fbase  layers\u002F1.base\nmv layers\u002Ftheme layers\u002F2.theme\nmv layers\u002Fui    layers\u002F3.ui\n\ntree -L 2 layers\n# layers\n# ├── 1.base\n# │   ├── app\n# │   └── nuxt.config.ts   # keeps `$meta: { name: 'base' }` so the alias stays #layers\u002Fbase\n# ├── 2.theme\n# │   ├── app\n# │   └── nuxt.config.ts\n# └── 3.ui\n#     ├── app\n#     └── nuxt.config.ts\n","Auto-scanned layers are sorted by folder name with later names winning, so `ui > theme > base` already holds by accident; the point of the prefixes is to make the order survive the next folder someone adds (`layers\u002Fwidgets` would otherwise jump to the top). The project still beats all three. An auto-scanned layer is named after its folder only as a **fallback**: `$meta: { name: 'base' }` in the layer's `nuxt.config.ts` wins, so the alias stays `#layers\u002Fbase` rather than becoming `#layers\u002F1.base`. The alternative is to state the order explicitly with `extends: ['.\u002Flayers\u002Fui', '.\u002Flayers\u002Ftheme', '.\u002Flayers\u002Fbase']` (first entry highest); Nuxt deduplicates a layer that is both auto-scanned and listed.\n",[448],{"title":382,"url":383},{"id":450,"type":324,"difficulty":347,"subsection":425,"tags":451,"prompt":454,"answer":455,"docs":456,"section":335,"sectionTitle":10},"layers-010",[427,452,453],"components","scenario","Two layers both ship `app\u002Fcomponents\u002FAppHeader.vue`: `@team\u002Fnuxt-layer-base` (installed from npm and listed in `extends`) and `layers\u002Fmarketing` (auto-scanned). The project has no `AppHeader.vue`. A developer reports that \"the wrong header\" renders. Which one renders, why, and what do you do about it?\n","`layers\u002Fmarketing` renders. Auto-scanned `~~\u002Flayers\u002F*` folders sit above every `extends` entry, so the marketing `AppHeader` shadows the base one; components are deduplicated by resolved name and the lower-priority file is dropped without a warning. Had both been auto-scanned, alphabetical order would decide (Z above A); had both been in `extends`, the first entry would win.\n\n\"Wrong\" is a design signal: two layers competing for one global name means the boundary between them is unclear. I first confirm the order instead of arguing about it, by printing `getLayerDirectories()` from a throwaway local module (project first, earlier overrides later). Then one of three fixes: make the order explicit (`extends: ['.\u002Flayers\u002Fmarketing', '@team\u002Fnuxt-layer-base']` or numeric prefixes), stop the collision by naming (`MarketingHeader`, and a `Team` prefix for everything the base layer exports), or, if marketing really is a customisation of the base header, wrap the original via `#layers\u002Fbase\u002Fapp\u002Fcomponents\u002FAppHeader.vue` instead of copying it. Left alone, the next layer someone adds with a later letter silently flips the header again.\n",[457],{"title":438,"url":334},{"id":459,"type":338,"difficulty":347,"subsection":425,"tags":460,"prompt":462,"options":463,"correct":468,"answer":469,"docs":470,"section":335,"sectionTitle":10},"layers-011",[427,461,357],"defu","A layer's `nuxt.config.ts` declares `modules: ['@nuxt\u002Fui']`, `css: [join(currentDir, 'app\u002Fassets\u002Fbase.css')]` and `runtimeConfig: { public: { flags: { beta: false, legacy: true } } }`. The project declares `modules: ['@nuxt\u002Fcontent']`, `css: ['~\u002Fassets\u002Fapp.css']` and `runtimeConfig: { public: { flags: { beta: true } } }`. What is the merged result?\n",[464,465,466,467],"`modules` is `['@nuxt\u002Fcontent', '@nuxt\u002Fui']`, `css` contains **both** files, `flags` is `{ beta: true, legacy: true }`","`modules` is `['@nuxt\u002Fcontent', '@nuxt\u002Fui']`, `css` is only `app.css` because the project's array replaces the layer's, `flags` is `{ beta: true, legacy: true }`","`modules` is `['@nuxt\u002Fui', '@nuxt\u002Fcontent']` because layers register first, `css` contains both files, `flags` is `{ beta: true }` because objects are replaced","Build error: `runtimeConfig.public.flags.beta` is declared with two different values",[378],"Configs are merged with defu, project first: plain objects deep-merge key by key (`beta` takes the project's `true`, `legacy` is inherited), scalars take the higher-priority value, and arrays are **concatenated** with the higher-priority items first (`['@nuxt\u002Fcontent', '@nuxt\u002Fui']`, which Nuxt then deduplicates for modules). Nothing ever replaces an array, which is why the app ends up loading both stylesheets. Conflicting values are never an error; the higher layer simply wins.\n",[471],{"title":472,"url":473},"unjs\u002Fdefu","https:\u002F\u002Fgithub.com\u002Funjs\u002Fdefu",{"id":475,"type":324,"difficulty":476,"subsection":425,"tags":477,"prompt":478,"answer":479,"docs":480,"section":335,"sectionTitle":10},"layers-012",3,[427,387,370],"You are reviewing a layer whose author hard-coded assumptions about which layer wins. How do you *prove* the resolved layer order and see what each layer contributed, rather than reciting the rule? And where does `nuxt.options` mislead you?\n","I put the answer in code, not in memory. A ten-line local module in the consuming app calls `getLayerDirectories(nuxt)` from `nuxt\u002Fkit` and logs each entry's `root`: index 0 is the project and earlier entries override later ones, so the printout *is* the priority order, including the alphabetical `layers\u002F` ordering and the `extends` order people get backwards. For components I put the same-named file in two layers with a visible marker and check which renders; for config I print the option in question.\n\n`nuxt.options` is the trap. Inside a module it is already the **merged** result of c12 + defu: `nuxt.options.css` shows the final concatenated list but not who added which entry, and `nuxt.options.modules` has been deduplicated. To see one layer's contribution, read `nuxt.options._layers[i].config`, which is that layer's own resolved config with its `$production` \u002F `$development` overrides already applied, and compare it with the merged value. Also remember the merge is not symmetric: `undefined` in a higher layer never overrides a defined default, and arrays only grow, so \"my override did nothing\" for `css` or `plugins` is the expected behaviour, not a priority bug.\n",[481],{"title":392,"url":393},{"id":483,"type":324,"difficulty":325,"subsection":484,"tags":485,"prompt":487,"answer":488,"docs":489,"section":335,"sectionTitle":10},"layers-013","paths-and-aliases",[486,370],"aliases","Why does `css: ['~\u002Fassets\u002Fcss\u002Fmain.css']` in a layer's `nuxt.config.ts` work while you develop the layer and break in the first consuming app? What do you write instead?\n","`~`, `@`, `~~` and `@@` are global aliases that resolve against the **consuming project's** `srcDir` and `rootDir`, never against the layer, both in `nuxt.config.ts` and in a layer's runtime code. While you run `nuxt dev` inside the layer folder, the layer *is* the project, so `~\u002Fassets\u002Fcss\u002Fmain.css` happens to point at its own `app\u002Fassets`. In a consumer, `~` is the consumer's `app\u002F`, the file is not there, and Vite fails to resolve the import, or worse, silently picks up a consumer file of the same name.\n\nInside the layer's config I anchor every path to the file itself: `const currentDir = dirname(fileURLToPath(import.meta.url))`, then `css: [join(currentDir, 'app\u002Fassets\u002Fcss\u002Fmain.css')]`, and the same for `components: [{ path }]`, explicitly listed local modules and Nitro storage bases. In runtime code I use relative imports, auto-imports (the layer's `app\u002Fcomposables` is scanned anyway) or the named alias `#layers\u002F\u003Cname>\u002F...`. The same rule explains two neighbours: `@import \"~\u002F...\"` inside a layer stylesheet breaks identically (use `.\u002Ftokens.css`), and only the project's `.env` is ever read. Prevention is a consumer fixture that lives *outside* the layer folder and builds in CI.\n",[490],{"title":491,"url":365},"Authoring Nuxt layers — relative paths and aliases",{"id":493,"type":351,"difficulty":347,"subsection":484,"language":353,"tags":494,"prompt":495,"starter":496,"solution":497,"answer":498,"docs":499,"section":335,"sectionTitle":10},"layers-014",[486,357],"Write `nuxt.config.ts` for a layer named `base` that registers its own stylesheet `app\u002Fassets\u002Fcss\u002Fmain.css`, its own local module `modules\u002Fteam-devtools.ts`, the `@nuxt\u002Fui` module, and a 60-second cache rule for `\u002Fapi\u002F_team\u002F**`. Every path must resolve correctly when the layer is installed from npm into an app in another directory.\n","import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nexport default defineNuxtConfig({\n  \u002F\u002F TODO\n})\n","import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n\u002F\u002F Anchor every path to this file: `~` and `@` resolve against the consuming project.\nconst currentDir = dirname(fileURLToPath(import.meta.url))\n\nexport default defineNuxtConfig({\n  $meta: { name: 'base' },                          \u002F\u002F consumers get the #layers\u002Fbase alias\n  modules: [\n    '@nuxt\u002Fui',                                     \u002F\u002F npm module: by package name\n    join(currentDir, 'modules\u002Fteam-devtools.ts'),   \u002F\u002F local module: by absolute path\n  ],\n  css: [join(currentDir, 'app\u002Fassets\u002Fcss\u002Fmain.css')],\n  routeRules: {\n    '\u002Fapi\u002F_team\u002F**': { cache: { maxAge: 60 } },     \u002F\u002F route patterns are not paths, nothing to resolve\n  },\n})\n","`import.meta.url` is the URL of *this* config file wherever it lives (`layers\u002Fbase\u002F`, a pnpm workspace package or `node_modules\u002F@team\u002Fnuxt-layer-base`), so `join(currentDir, …)` produces an absolute path that no alias can misdirect. `fileURLToPath(new URL('.\u002Fapp\u002Fassets\u002Fcss\u002Fmain.css', import.meta.url))` is the equivalent one-liner; inside a module you would use kit's `createResolver(import.meta.url)`. Common mistakes: `~\u002Fmodules\u002Fteam-devtools` (resolves to the consumer), forgetting `$meta.name` (no `#layers\u002Fbase` for wrap-and-override), and writing `@import \"~\u002F...\"` inside `main.css` itself. Nuxt also scans each layer's `modules\u002F` directory, so the explicit entry is optional; the absolute path deduplicates against the scan. Say out loud that `routeRules` keys are patterns, not files, which is why they need no resolution, and that the consumer can override the `\u002Fapi\u002F_team\u002F**` rule because route rules deep-merge by key.\n",[500],{"title":491,"url":365},{"id":502,"type":351,"difficulty":347,"subsection":484,"language":353,"tags":503,"prompt":504,"starter":505,"solution":506,"answer":507,"docs":508,"section":335,"sectionTitle":10},"layers-015",[486,370],"This layer `nuxt.config.ts` works when `nuxt dev` runs inside the layer folder, but a consuming app fails with `Failed to resolve import \"~\u002Fassets\u002Fmain.css\"` and `Cannot find module '~~\u002Fmodules\u002Fteam-devtools'`. Fix it without changing what the layer provides.\n","export default defineNuxtConfig({\n  css: ['~\u002Fassets\u002Fmain.css'],\n  components: [{ path: '~\u002Fcomponents', prefix: 'Team' }],\n  modules: ['~~\u002Fmodules\u002Fteam-devtools'],\n})\n","import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst currentDir = dirname(fileURLToPath(import.meta.url))\n\nexport default defineNuxtConfig({\n  \u002F\u002F `~` meant the consumer's app\u002F directory; anchor to the layer instead.\n  css: [join(currentDir, 'app\u002Fassets\u002Fmain.css')],\n  components: [{ path: join(currentDir, 'app\u002Fcomponents'), prefix: 'Team' }],\n  \u002F\u002F The layer's own modules\u002F directory is scanned by Nuxt, so the explicit entry is redundant.\n  \u002F\u002F If you want it listed anyway, use the absolute path: join(currentDir, 'modules\u002Fteam-devtools')\n})\n","Every alias in the starter pointed at the consuming project: `~\u002Fassets\u002Fmain.css` became `\u003Cconsumer>\u002Fapp\u002Fassets\u002Fmain.css`, the `Team` components directory would have re-registered the consumer's own components with a prefix, and `~~\u002Fmodules\u002Fteam-devtools` looked for a module in the consumer's root. The fix is mechanical: derive `currentDir` from `import.meta.url` and build absolute paths with `join`. Remember that Nuxt 4 layers keep app code under `app\u002F`, so the stylesheet is `app\u002Fassets\u002Fmain.css`, not `assets\u002Fmain.css`. Mention that the bug is invisible when you run the layer as its own app, which is why a consumer fixture outside the layer folder belongs in CI, and that a `.playground` created by the layer template has its own `srcDir`, so it would have caught this.\n",[509],{"title":491,"url":365},{"id":511,"type":324,"difficulty":476,"subsection":484,"tags":512,"prompt":514,"answer":515,"docs":516,"section":335,"sectionTitle":10},"layers-016",[486,452,513],"design","A product app must keep the base layer's `TeamButton` behaviour but add a tracking attribute and wider letter-spacing. Copying the component was rejected in review. How do you override it while still rendering the original, and what has to be true about the layer for that to work?\n","Wrap-and-override. The project adds a file that resolves to the same component name, `app\u002Fcomponents\u002FTeam\u002FButton.vue` → `TeamButton`, so it wins over the layer's by priority. Inside it I import the original *by path* through the named-layer alias, `import BaseTeamButton from '#layers\u002Fbase\u002Fapp\u002Fcomponents\u002FTeam\u002FButton.vue'`, render it with `v-bind=\"$attrs\"` plus my extra attribute and class, set `defineOptions({ inheritAttrs: false })`, and forward slots with `\u003Ctemplate v-for=\"(_, name) in $slots\" #[name]=\"props\">\u003Cslot :name=\"name\" v-bind=\"props ?? {}\" \u002F>\u003C\u002Ftemplate>`. The layer's file is no longer auto-registered under that name (the project's replaced it), but importing it explicitly is fine.\n\nTwo conditions: the layer must be **named**, either by living under `layers\u002F` (folder name) or via `$meta: { name: 'base' }` in its `nuxt.config.ts` (or `meta: { name }` in the `extends` tuple), and the consumer must be on Nuxt ≥ 3.16, where `#layers\u002F\u003Cname>` was introduced. The alternatives are worse: `~\u002Fcomponents\u002F...` points back at the project (a self-import), and a relative path into `node_modules\u002F@team\u002F...` depends on the package manager's layout.\n\nThe trade-off: the wrapped file path becomes part of the layer's public API, so moving `Team\u002FButton.vue` is a breaking change, which is why a team layer documents its override recipes and keeps those paths stable.\n",[517],{"title":518,"url":383},"The layers\u002F directory (#layers alias)",{"id":520,"type":324,"difficulty":347,"subsection":521,"tags":522,"prompt":526,"answer":527,"docs":528,"section":335,"sectionTitle":10},"layers-017","config-merging",[523,524,525],"runtime-config","app-config","modules","How do `runtimeConfig`, `app.config` and module options behave when a layer and a consuming app both declare them, and how do you decide which of the three a given value belongs in?\n","All three deep-merge with defu and the project on top, so a layer supplies defaults and an app changes individual keys while inheriting the rest. They differ in **when** the value can change. `runtimeConfig` is overridable per deployment with `NUXT_\u003CKEY>` \u002F `NUXT_PUBLIC_\u003CKEY>` environment variables at runtime, coerced to the type of the default, so it carries per-environment values and, in its private part, secrets that the layer must *not* default to real values. `app.config` is bundled and reactive (`useAppConfig()`, `updateAppConfig()`), cannot be changed by environment variables and is shipped to the client, so it carries theme tokens and UI defaults. Module options (`ui: {…}`, `content: {…}`) are read once at build time by the module's `setup`; the layer sets sensible defaults, the project overrides keys, and since Nuxt 4.3 sets the key to `false` to drop the module.\n\nThe deciding question is not \"does it change?\" but \"does it change *per environment*?\": if yes, `runtimeConfig`; if it is a design or behaviour default, `app.config`; if it configures build output, a module option. What breaks: a URL in `app.config` ships the same value to staging and production, and a secret in a layer's `runtimeConfig` default lands in git and, if public, in every payload.\n",[529,532],{"title":530,"url":531},"Runtime config","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fgoing-further\u002Fruntime-config",{"title":533,"url":534},"app.config.ts","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fapp\u002Fapp-config",{"id":536,"type":338,"difficulty":325,"subsection":521,"tags":537,"prompt":538,"options":539,"correct":544,"answer":545,"docs":546,"section":335,"sectionTitle":10},"layers-018",[523,524],"A base layer must ship a default API base URL that every product app adjusts per deployment (staging vs production) **without rebuilding**. Which channel is right?\n",[540,541,542,543],"`runtimeConfig.public.apiBase` with a non-secret default in the layer's `nuxt.config.ts`; each deployment sets `NUXT_PUBLIC_API_BASE`","`app.config.ts` in the layer, because it is reactive and the project can override it","`process.env.API_BASE` read directly inside the layer's composable","A `.env` file shipped inside the layer that sets `NUXT_PUBLIC_API_BASE`",[378],"Only `runtimeConfig` is replaced at runtime from the environment (`NUXT_` prefix, `PUBLIC_` for public keys), and only for keys that already exist as defaults, which is exactly what the layer provides. `app.config` is bundled, so \"reactive\" does not mean \"per environment\": staging and production would ship the same value. `process.env` is not available in client code and is inlined at build time on the server bundle. A `.env` inside a layer is never read; only the project's `.env` is loaded, and only by `nuxt dev`\u002F`build`, never by the running server.\n",[547],{"title":530,"url":531},{"id":549,"type":338,"difficulty":347,"subsection":521,"tags":550,"prompt":551,"options":552,"correct":557,"answer":558,"docs":559,"section":335,"sectionTitle":10},"layers-019",[523],"The layer declares `runtimeConfig: { public: { teamApi: { baseUrl: 'https:\u002F\u002Fapi.internal' } } }`. Which environment variable overrides that value at runtime in a consuming app?\n",[553,554,555,556],"`NUXT_PUBLIC_TEAM_API_BASE_URL`","`NUXT_PUBLIC_TEAMAPI_BASEURL`","`NUXT_TEAM_API_BASE_URL`","`TEAM_API_BASE_URL`, because public keys drop the `NUXT_` prefix",[378],"The rules: uppercase, `NUXT_` prefix, `PUBLIC_` for the public section, and an underscore at every nesting level *and* every camelCase boundary, so `public.teamApi.baseUrl` is `NUXT_PUBLIC_TEAM_API_BASE_URL`. The value is cast with destr to the type of the default (a `false` default lets `=true` become a boolean). The variable only works because the layer declared the key: environment variables cannot create runtime config keys, so a layer that omits a default has made that value un-overridable. Non-prefixed variables are ignored entirely.\n",[560],{"title":530,"url":531},{"id":562,"type":324,"difficulty":476,"subsection":521,"tags":563,"prompt":565,"answer":566,"docs":567,"section":335,"sectionTitle":10},"layers-020",[461,513,564],"plugins","A consuming app wants to drop one global stylesheet and one plugin that the base layer ships. The developer tried every override they could think of in `nuxt.config.ts` and \"nothing happens\". Explain why, and what this means for how you design a layer.\n","Because defu has no subtraction. Arrays such as `css`, `plugins`, `modules` and `vite.plugins` are **concatenated**, project entries first; `undefined` and `null` in a higher layer are skipped rather than applied, so there is no value the project can write that removes the layer's entry. Scalars and object keys can be overridden, array items cannot.\n\nThe design consequence is that everything with a cost must be opt-in or switchable at the layer's level. A plugin reads a flag and returns early (`useAppConfig().team.tracking` or `useRuntimeConfig().public.team.tracking`); a stylesheet beyond design tokens moves behind a module option or into a separate feature layer (`@team\u002Fnuxt-layer-analytics`) that consumers choose in `extends`; a module inherited from a layer can be switched off with `image: false` in the project since Nuxt 4.3; inherited pages can be removed with the `pages:extend` hook. The exception to the array rule is `app.config`, where an extended layer can write an array key as a function (`items: () => [...]`) to *replace* the lower layer's default instead of merging; the docs restrict that merger form to extended layers, not the project's own `app.config`.\n\nThe escape hatch, a local module in the project that filters `nuxt.options.css` or removes entries from `app.plugins` in the `app:resolve` hook, works but couples the app to the layer's file paths, so it is the signal to fix the layer.\n",[568,569],{"title":472,"url":473},{"title":570,"url":534},"app.config.ts — merging strategy",{"id":572,"type":324,"difficulty":347,"subsection":521,"tags":573,"prompt":574,"answer":575,"docs":576,"section":335,"sectionTitle":10},"layers-021",[399,357],"How do `$development`, `$production` and `$env.\u003Cname>` behave inside a *layer's* `nuxt.config.ts`? What can a layer do with them that it could not otherwise, and what is the catch?\n","c12 applies environment-specific overrides **per config file** before the cross-layer merge: it picks the layer's own `$development`, `$production` or `$test` block from the current mode, or `$env.\u003Cname>` when the CLI runs with `--envName \u003Cname>`, folds it into that layer's config, and only then merges all layers with defu. A layer can therefore ship \"in production, prerender `\u002F` and cache `\u002Fapi\u002F_team\u002F**`; in development, enable verbose logging\" without the consuming app knowing the mechanism exists, and each product app inherits the right behaviour per mode.\n\nThe catch is twofold. First, these blocks are selected at **build time** (dev, build, generate) and by mode, not per deployment: staging and production are both `$production`, so anything that differs between them still belongs in `runtimeConfig` with `NUXT_*` variables. Second, the layer's `$production` values become ordinary layer values before the priority merge, so a project's plain `routeRules` entry still beats the layer's production-only one; the layer cannot use `$production` to force anything on a consumer. Keep the blocks small, and keep secrets out of them, since the config is committed.\n",[577],{"title":395,"url":365},{"id":579,"type":324,"difficulty":325,"subsection":580,"tags":581,"prompt":583,"answer":584,"docs":585,"section":335,"sectionTitle":10},"layers-022","overriding",[452,582,328],"pages","A layer ships `app\u002Fcomponents\u002FTeam\u002FButton.vue`, `app\u002Fpages\u002Fsettings.vue` and `app\u002Flayouts\u002Fdefault.vue`. How does a consuming app replace each of them, and what key does Nuxt use to decide that two files are \"the same\"?\n","The app puts a file that produces the **same key** at a higher priority, and the layer's file is dropped. The key differs per kind. Components are keyed by resolved name: the layer's `Team\u002FButton.vue` becomes `TeamButton` through path-prefix naming, so either `app\u002Fcomponents\u002FTeam\u002FButton.vue` or `app\u002Fcomponents\u002FTeamButton.vue` in the project replaces it (if the layer registered its directory with `prefix: 'Team'` and `pathPrefix: false`, match the resulting name, not the path). Pages are keyed by relative path inside the pages directory, so `app\u002Fpages\u002Fsettings.vue` in the project replaces the layer's route `\u002Fsettings`. Layouts and route middleware are keyed by name (`default`, `auth`), server handlers by route and method.\n\nTwo things to say out loud: plugins are the exception, they are only deduplicated by absolute source path, so a same-named plugin in the project runs *in addition* to the layer's; and the override is silent, no warning tells you the layer's file was shadowed, so a fixture test that asserts the override is worth writing.\n",[586],{"title":333,"url":334},{"id":588,"type":338,"difficulty":347,"subsection":580,"tags":589,"prompt":590,"options":591,"correct":596,"answer":597,"docs":598,"section":335,"sectionTitle":10},"layers-023",[564],"The base layer ships `app\u002Fplugins\u002Ftracking.client.ts`. A product app creates its own `app\u002Fplugins\u002Ftracking.client.ts` expecting to replace it. What happens?\n",[592,593,594,595],"Both plugins run: plugins are collected from every layer and deduplicated only by absolute source path, so a same-named file in another layer is a second plugin","The project's plugin replaces the layer's, because the project has the highest priority for same-path files","The layer's plugin wins, because layer plugins are registered before the project's and the first registration is kept","Nuxt throws a duplicate-plugin error at build time",[378],"Components, layouts, middleware and pages are keyed by name or relative path, so a higher layer shadows a lower one. Plugins are not: Nuxt gathers `plugins\u002F*` from every layer, lowest layer first, and only removes exact duplicates of the same absolute file, so both tracking plugins execute (layer first, then the project's, unless `order` or `dependsOn` says otherwise). The fix belongs in the layer: read a flag from `useAppConfig()` or `useRuntimeConfig()` and return early, so consumers can switch tracking off instead of trying to shadow it.\n",[599],{"title":395,"url":365},{"id":601,"type":324,"difficulty":347,"subsection":580,"tags":602,"prompt":604,"answer":605,"docs":606,"section":335,"sectionTitle":10},"layers-024",[582,603,453],"middleware","The auth layer ships `app\u002Fpages\u002Flogin.vue`, `app\u002Fpages\u002Faccount\u002Findex.vue`, `app\u002Flayouts\u002Fauth.vue` and `app\u002Fmiddleware\u002Fauth.global.ts`. One product app must restyle the login page, drop the account page entirely, and keep the global middleware but skip it under `\u002Fpublic\u002F**`. How do you do each without forking the layer?\n","**Restyle login:** add `app\u002Fpages\u002Flogin.vue` in the project; pages are keyed by relative path, so it replaces the layer's route. If I only want to wrap it, I import the original via `#layers\u002Fauth\u002Fapp\u002Fpages\u002Flogin.vue` and render it, but I re-declare `definePageMeta` in my file, because page meta is extracted from the file that is registered, not from what it renders. The `auth` layout stays inherited unless I add `app\u002Flayouts\u002Fauth.vue`.\n\n**Drop the account page:** there is no \"remove\" in the merge, so I use the build-time hook in the project's `nuxt.config.ts`: `hooks: { 'pages:extend'(pages) { … } }`, walk `pages` (and `children`) and `splice` the entries whose `file` matches `account\u002Findex.vue`. A `routeRules` redirect is the cheap alternative, but the page is still compiled and shipped.\n\n**Skip middleware under `\u002Fpublic\u002F**`:** route middleware is keyed by name, so `app\u002Fmiddleware\u002Fauth.global.ts` in the project replaces the layer's. Inside it I import the original with `import original from '#layers\u002Fauth\u002Fapp\u002Fmiddleware\u002Fauth.global'`, return early when `to.path.startsWith('\u002Fpublic\u002F')`, and otherwise `return original(to, from)`. Better still, the layer exposes `runtimeConfig.public.team.auth.publicRoutes` and reads it itself, so no app has to know the file path. Each override is silent, so I add a fixture test per behaviour.\n",[607],{"title":395,"url":365},{"id":609,"type":351,"difficulty":476,"subsection":610,"language":353,"tags":611,"prompt":612,"starter":613,"solution":614,"answer":615,"docs":616,"section":335,"sectionTitle":10},"layers-025","modules-in-layers",[387,525,427],"Write the local module `modules\u002Fteam-blocks.ts` that ships with the base layer. It must register an `app\u002Fblocks\u002F` components directory from **every** layer as well as the project, with the prefix `Block` and without path prefixing, skipping layers that have no such folder. A block the consuming app defines must win over a layer block of the same name.\n","import { defineNuxtModule } from 'nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'team-blocks' },\n  setup(_options, nuxt) {\n    \u002F\u002F TODO: register app\u002Fblocks for every layer\n  },\n})\n","import { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { addComponentsDir, defineNuxtModule, getLayerDirectories } from 'nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'team-blocks' },\n  setup(_options, nuxt) {\n    \u002F\u002F getLayerDirectories() is ordered project first, then layers by priority.\n    \u002F\u002F Reverse it so the lowest-priority layer registers first and the project registers last.\n    for (const layer of [...getLayerDirectories(nuxt)].reverse()) {\n      const path = join(layer.app, 'blocks')\n      if (!existsSync(path)) continue\n\n      addComponentsDir({ path, prefix: 'Block', pathPrefix: false })\n    }\n  },\n})\n","The point of the task is that a module must never assume one `srcDir`. `getLayerDirectories(nuxt)` from `nuxt\u002Fkit` returns one entry per layer with `root`, `app`, `server`, `shared`, `public`, `modules` and the `app*` directories already resolved, ordered exactly like `nuxt.options._layers`: project at index 0, earlier entries overriding later ones. Reversing that list is what makes the override work, because the directory registered last wins the name; register in the given order and the layer's `BlockHero` beats the app's. `existsSync` avoids registering directories that do not exist, and `[...list]` copies before `reverse()`, which mutates in place. Common mistakes: reading `nuxt.options.srcDir` (only the project is scanned), hard-coding `'app\u002Fblocks'` relative to `process.cwd()`, and forgetting `pathPrefix: false`, which would turn `blocks\u002FHero.vue` into `BlockHero` in one layer and something else in another. `@nuxt\u002Fcontent` does the same thing for its `components\u002Fcontent` folders.\n",[617],{"title":392,"url":393},{"id":619,"type":351,"difficulty":347,"subsection":610,"language":353,"tags":620,"prompt":621,"starter":622,"solution":623,"answer":624,"docs":625,"section":335,"sectionTitle":10},"layers-026",[525,357,427],"`@team\u002Fnuxt-layer-base` lists `@nuxt\u002Fui`, `@nuxt\u002Feslint` and `@nuxt\u002Fimage` in its `modules`, and sets `eslint: { config: { stylistic: true } }`. Write the consuming app's `nuxt.config.ts` so that the app also uses `@nuxt\u002Fcontent`, does **not** run `@nuxt\u002Fimage` at all, and turns the layer's stylistic ESLint rules off while inheriting the rest of the layer's ESLint options.\n","export default defineNuxtConfig({\n  extends: ['@team\u002Fnuxt-layer-base'],\n  \u002F\u002F TODO\n})\n","export default defineNuxtConfig({\n  extends: ['@team\u002Fnuxt-layer-base'],\n\n  \u002F\u002F Arrays concatenate: the final list is content, ui, eslint, image (deduplicated by module).\n  modules: ['@nuxt\u002Fcontent'],\n\n  \u002F\u002F You cannot remove an item from the merged `modules` array.\n  \u002F\u002F Switching an inherited module off is done through its config key.\n  image: false,\n\n  \u002F\u002F Module options are plain objects, so they deep-merge and the project wins key by key.\n  eslint: {\n    config: { stylistic: false },\n  },\n})\n","Two different mechanisms, and confusing them is the usual bug. `modules` is an array, so defu concatenates the project's entries with the layer's and Nuxt deduplicates by module name; nothing the app writes can *subtract* `@nuxt\u002Fimage` from that list. The documented escape is the module's config key: `image: false` stops it from being installed. Options are ordinary objects, so `eslint.config.stylistic` overrides only that key and every other ESLint option the layer set is inherited. Worth saying out loud: the layer still declares `@nuxt\u002Fimage` in its `dependencies`, so it is installed on disk and only skipped at build time; and if a layer option were an *array* (a list of ESLint presets, say) the project's entries would be added to it rather than replacing it.\n",[626],{"title":627,"url":365},"Authoring Nuxt layers (multi-layer support for modules)",{"id":629,"type":324,"difficulty":347,"subsection":610,"tags":630,"prompt":631,"answer":632,"docs":633,"section":335,"sectionTitle":10},"layers-027",[525,513],"Your base layer lists `@nuxt\u002Fui`, `@nuxt\u002Feslint` and `@nuxt\u002Fimage` in its `modules`. One app already lists `@nuxt\u002Fui` itself, a second wants no image optimisation at all, and a third wants `@nuxt\u002Fimage` only when its own feature flag is on. What happens in each case?\n","The first app is fine: module lists from every layer are concatenated and then **deduplicated by module name**, so `@nuxt\u002Fui` is installed once, and its options from both configs are merged with defu before that single `setup()` runs, with the app winning on conflicting keys. The layer must declare these modules in its `package.json` `dependencies`, because consumers install them transitively.\n\nThe second app sets `image: false`. Setting a module's config key to `false` is the documented way to drop an inherited module, and it exists precisely because arrays only grow.\n\nThe third app cannot have what it wants from a layer. \"Install this module only when X\" is an `if`, and a layer never executes; it is loaded by c12 and merged. The options are: keep `@nuxt\u002Fimage` in the base layer and let that app opt out, move it to a smaller feature layer (`@team\u002Fnuxt-layer-media`) that only interested apps extend, or put the conditional registration inside the team module, which can branch in `setup()` and, since Nuxt 4.1, declare `moduleDependencies` with version constraints and defaults. Baking an `if` into a layer is the single clearest sign you actually wanted a module.\n",[634],{"title":627,"url":365},{"id":636,"type":351,"difficulty":347,"subsection":637,"language":353,"tags":638,"prompt":641,"starter":642,"solution":643,"answer":644,"docs":645,"section":335,"sectionTitle":10},"layers-028","server-and-nitro",[639,523,640],"nitro","ssr","Ship a health endpoint from the base layer at `\u002Fapi\u002F_team\u002Fhealth` that returns `{ ok, release }`. `release` must come from a default the layer declares, each app must be able to change it on the **built** server with an environment variable and no rebuild, and process details must never reach production responses. Write both the handler and the `runtimeConfig` the layer needs.\n","\u002F\u002F layers\u002Fbase\u002Fserver\u002Fapi\u002F_team\u002Fhealth.get.ts\nexport default defineEventHandler(() => {\n  \u002F\u002F TODO\n})\n","\u002F\u002F layers\u002Fbase\u002Fserver\u002Fapi\u002F_team\u002Fhealth.get.ts\nexport default defineEventHandler((event) => {\n  \u002F\u002F Pass the event: this reads the per-request runtime config, so NUXT_PUBLIC_TEAM_RELEASE\n  \u002F\u002F applied to the running server is picked up without a rebuild.\n  const config = useRuntimeConfig(event)\n\n  return {\n    ok: true,\n    release: config.public.teamRelease,\n    memory: import.meta.dev ? process.memoryUsage() : undefined,\n  }\n})\n\n\u002F\u002F layers\u002Fbase\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n  runtimeConfig: {\n    teamApiToken: '',                    \u002F\u002F private; each app supplies NUXT_TEAM_API_TOKEN\n    public: { teamRelease: 'dev' },      \u002F\u002F public; each app supplies NUXT_PUBLIC_TEAM_RELEASE\n  },\n})\n","The layer declares the **key and its type**; the app supplies the value. Environment variables can only override runtime config keys that already have a default, so omitting `teamRelease` from the layer would make it permanently un-overridable, and a string default is what lets `NUXT_PUBLIC_TEAM_RELEASE` be coerced correctly. `useRuntimeConfig(event)` rather than the bare call is the detail interviewers listen for: it resolves the config for that request so runtime overrides apply on the built server. The `import.meta.dev` guard matters more here than in an app, because this route ships to every consumer: `process.memoryUsage()` in production is information disclosure in twenty deployments at once. Also mention the `_team` namespace, which keeps the route from colliding with an app's own `\u002Fapi\u002Fhealth`, and that an app can still override the handler by creating `server\u002Fapi\u002F_team\u002Fhealth.get.ts` itself, since handlers are keyed by route and method.\n",[646,649],{"title":647,"url":648},"server directory","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fserver",{"title":530,"url":531},{"id":651,"type":324,"difficulty":476,"subsection":637,"tags":652,"prompt":653,"answer":654,"docs":655,"section":335,"sectionTitle":10},"layers-029",[639,415,513],"What server-side code do you put in a base layer that twenty apps extend, and what do you refuse to put there?\n","In: the cross-cutting pieces nobody should write twice. A namespaced health and version route under `\u002Fapi\u002F_team\u002F**`, a Nitro plugin that assigns and echoes a request id, security headers, a typed `server\u002Futils\u002FteamFetch.ts` for the internal API, and DTO types in `shared\u002Ftypes` so the Vue side and Nitro share one definition. Nitro scans every layer's `server\u002F` exactly as it scans the project's, so handlers are keyed by route and method and the higher-priority layer wins, while `server\u002Fmiddleware` and `server\u002Fplugins` from *every* layer run.\n\nOut: anything that assumes the layer owns the process. No debug or cache-reset endpoints, no environment dumps, no `process.memoryUsage()` outside `import.meta.dev`, no secrets as `runtimeConfig` defaults (the layer declares `teamApiToken: ''` and each app sets `NUXT_TEAM_API_TOKEN`), and no per-request state in module scope, because a Nitro plugin runs once per server process while its handlers serve every request. I also keep one owner for cross-cutting middleware, since two layers each shipping a `Content-Security-Policy` header collide silently.\n\nThe framing I use: every route a layer ships is production code in twenty deployments. Input validated, auth by default, no output that is only safe in a playground. And because Nuxt 5 moves Nitro from 2 to 3, layer server code sticks to Nuxt's auto-imports rather than deep `nitropack` imports, so the upgrade is not twenty simultaneous migrations.\n",[656,657],{"title":647,"url":648},{"title":658,"url":659},"shared directory","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fshared",{"id":661,"type":351,"difficulty":347,"subsection":662,"language":663,"tags":664,"prompt":667,"starter":668,"solution":669,"answer":670,"docs":671,"section":335,"sectionTitle":10},"layers-030","publishing-and-consuming","json",[665,666],"npm","publishing","Write the `package.json` for `@team\u002Fnuxt-layer-base` version 2.3.0, ready to publish to a private registry. It ships `nuxt.config.ts`, `app\u002F`, `server\u002F`, `shared\u002F` and `public\u002F`, imports `@nuxt\u002Fui` and `defu` at runtime, is developed with a `.playground` app on Nuxt 4.5 but supported down to Nuxt 4.3, and must not ship the playground or the tests.\n","{\n  \"name\": \"@team\u002Fnuxt-layer-base\",\n  \"version\": \"2.3.0\"\n}\n","{\n  \"name\": \"@team\u002Fnuxt-layer-base\",\n  \"version\": \"2.3.0\",\n  \"type\": \"module\",\n  \"main\": \".\u002Fnuxt.config.ts\",\n  \"files\": [\"nuxt.config.ts\", \"app\", \"server\", \"shared\", \"public\"],\n  \"scripts\": {\n    \"dev\": \"nuxt dev .playground\",\n    \"build\": \"nuxt build .playground\",\n    \"test\": \"vitest run\",\n    \"release\": \"changelogen --release --push && pnpm publish\"\n  },\n  \"dependencies\": {\n    \"@nuxt\u002Fui\": \"^4.11.0\",\n    \"defu\": \"^6.1.4\"\n  },\n  \"peerDependencies\": {\n    \"nuxt\": \"^4.3.0\"\n  },\n  \"devDependencies\": {\n    \"nuxt\": \"^4.5.0\",\n    \"vitest\": \"^5.0.0\",\n    \"@nuxt\u002Ftest-utils\": \"^4.3.0\"\n  }\n}\n","Three things carry the answer. `main` points at `nuxt.config.ts`: the config file **is** the entry point, because consumers resolve the package and c12 loads that file as a layer; there is no build step, layers ship source. Every dependency the layer's code or its listed modules need goes in `dependencies`, since consumers install them transitively; putting `@nuxt\u002Fui` in `devDependencies` is the classic first-publish bug. And `peerDependencies.nuxt` states the lowest version you actually test, which is what stops a layer built against 4.6 from failing mysteriously in an app on 4.3.\n\n`files` is the other half: include `server\u002F` (forgetting it is common, because the playground inside the repo keeps working) and exclude `.playground` and `test`. Verify with `npm pack` and read the tarball rather than trusting the list.\n",[672],{"title":673,"url":365},"Authoring Nuxt layers (publishing)",{"id":675,"type":324,"difficulty":347,"subsection":662,"tags":676,"prompt":678,"answer":679,"docs":680,"section":335,"sectionTitle":10},"layers-031",[665,677,370],"monorepo","A layer is used by two apps in one monorepo, and the company is about to start six more apps in separate repositories. Walk me through how you would distribute it, and what changes for the people consuming it.\n","The `extends` entry barely changes; everything else does. Today the apps depend on `\"@team\u002Fnuxt-layer-base\": \"workspace:*\"` in a pnpm workspace: edits hot-reload in every app, one lockfile pins everything, and there is nothing to publish. That stops working the moment consumers live in other repositories, because a workspace link cannot cross repos and nothing pins a version.\n\nSo I publish to the internal registry: `type: module`, `main` pointing at `nuxt.config.ts`, runtime dependencies declared, `nuxt` as a peer with the lowest version we test, changelog generated on release. Consumers write `extends: ['@team\u002Fnuxt-layer-base']` and a semver range in `package.json`, Renovate opens their upgrade PRs, and I can finally answer \"which app is on which version\" from their lockfiles.\n\nWhat consumers lose is the feedback loop: an npm layer only changes when you publish and they bump, so I tell them to point `extends` at a local checkout while debugging the layer, since `node_modules` is not watched. Git tags via giget are the fallback when there is no registry: real versions for free, but every consumer's CI needs `GIGET_AUTH`, `install: true` for the layer's dependencies, and an understanding of the download cache. I would keep the monorepo apps on `workspace:*` and publish from the same source, so both audiences share one layer.\n",[681,684],{"title":682,"url":683},"pnpm workspaces","https:\u002F\u002Fpnpm.io\u002Fworkspaces",{"title":673,"url":365},{"id":686,"type":338,"difficulty":325,"subsection":662,"tags":687,"prompt":688,"options":689,"correct":694,"answer":695,"docs":696,"section":335,"sectionTitle":10},"layers-032",[665,666],"Which `package.json` shape is correct for a Nuxt layer published to npm?\n",[690,691,692,693],"`type: module` with `main` pointing at `.\u002Fnuxt.config.ts`, runtime dependencies in `dependencies`, and the source shipped uncompiled","`main` pointing at a bundled `.\u002Fdist\u002Findex.mjs` produced by a build step, because npm consumers cannot compile TypeScript","`main` pointing at `.\u002Fapp\u002Findex.ts`, which re-exports the layer's components and composables","No `main` at all: Nuxt resolves the package directory and looks for a `nuxt.config.ts` inside it",[378],"A layer has no build and no JavaScript entry point: the thing a consumer needs to load is its `nuxt.config.ts`, so that is what `main` points at, and the rest of the package is plain source that Vite and Nitro compile in the consuming app's build. Bundling defeats the whole model, since components and server handlers are found by directory scanning, not by imports. An `index.ts` barrel is equally wrong for the same reason. And the resolution is not magic: without `main`, the package does not resolve to the config file. The other required piece, which the wrong options quietly skip, is that everything the layer imports at runtime must be in `dependencies`, because consumers get it transitively.\n",[697],{"title":673,"url":365},{"id":699,"type":324,"difficulty":476,"subsection":662,"tags":700,"prompt":702,"answer":703,"docs":704,"section":335,"sectionTitle":10},"layers-033",[701,414,453],"versioning","The base layer's next release needs an API that only exists from Nuxt 4.6, but three of the twenty consuming apps are pinned to 4.3. How do you handle the release?\n","First I name the cost: raising the floor is a **breaking change** for those three apps, and the failure mode is bad. A layer using an API the consumer's Nuxt does not have blows up inside the app's build with an error that points at the app, not at us, so the three teams lose an afternoon before anyone suspects the layer.\n\nSo the release is gated on `peerDependencies.nuxt`. If I raise it to `^4.6.0`, it goes out as a major with a migration note, and the CI matrix's lowest job moves to 4.6 at the same time, because a peer range we do not test is a guess. Until those three apps upgrade they stay on the previous major, which needs a stated support window and backported security fixes.\n\nBefore accepting that, I check whether the feature is worth it. Often it is not: I can keep the 4.3 floor and do without the new API, or move the code that needs it into the team *module*, where `setup()` can branch on the running version and degrade gracefully, which a layer can never do. If the feature is worth it, I use the adoption data (each app's lockfile, read in CI) to find the three apps, open their upgrade PRs myself, and only then cut the major. The rule I keep is that the layer's floor is a promise to the slowest consumer, not to the fastest.\n",[705],{"title":673,"url":365},{"id":707,"type":324,"difficulty":347,"subsection":662,"tags":708,"prompt":709,"answer":710,"docs":711,"section":335,"sectionTitle":10},"layers-034",[701,513],"What counts as a breaking change in a layer, as opposed to a minor? Give me the checklist you would use in review.\n","A layer's public surface is *files and names*, so a break is anything that forces a consumer to edit code or config. Breaking: renaming or removing a component, composable, util, page route, layout or named middleware; removing or renaming a `runtimeConfig` key, which silently turns an app's `NUXT_*` variable into a no-op; renaming an `app.config` key, or changing a default in a way that visibly changes rendering; removing a module from the preset that apps relied on; raising the minimum Nuxt in `peerDependencies`; moving a file that consumers import through `#layers\u002Fbase\u002F...` to wrap it, which makes that path part of the API; and a type-level change to the `AppConfigInput` augmentation that fails consumers' `nuxt typecheck`.\n\nMinor: adding components, composables, routes and `app.config` keys with defaults that preserve current behaviour; loosening a peer range; internal refactors behind unchanged names.\n\nTwo things people misfile. Adding a global stylesheet or a plugin is *not* neutral: arrays concatenate, so every consumer pays for it and cannot remove it, which makes it at least a minor with a note and often a discussion. And a bug fix that changes rendering is a break for whoever was relying on the bug. I write the migration note before the release, not after.\n",[712],{"title":673,"url":365},{"id":714,"type":338,"difficulty":347,"subsection":662,"tags":715,"prompt":716,"options":717,"correct":722,"answer":723,"docs":724,"section":335,"sectionTitle":10},"layers-035",[370,665],"A developer is chasing a bug inside `@team\u002Fnuxt-layer-base`, which their app installs from the registry. They edit files under `node_modules\u002F@team\u002Fnuxt-layer-base` and nothing changes in the running dev server. What is the right way to iterate?\n",[718,719,720,721],"Keep editing inside `node_modules` and restart `nuxt dev` after each change; the layer will be picked up on the next boot","Publish a patch version for every change, since a registry layer is only ever loaded from the tarball","Point `extends` at a local checkout of the layer, or link it into the workspace with `workspace:*`, for the duration of the debugging session","Copy the layer's files into the app's `layers\u002F` folder and delete them again afterwards",[347],"`node_modules` is not watched, so edits there do not trigger a reload, and they are wiped by the next install anyway. The supported loop is to make the layer a local dependency while you work on it: a relative `extends` path or a workspace link gives you the same hot reload you get from `layers\u002F*`, because the files are inside the watched tree. Republishing for each change is technically true and operationally absurd, which is exactly why teams keep a workspace link around. Copying into `layers\u002F` also \"works\" but changes the priority order (auto-scanned folders sit above every `extends` entry), so you end up debugging a different resolution than the one your users have.\n",[725],{"title":673,"url":365},{"id":727,"type":351,"difficulty":476,"subsection":728,"language":353,"tags":729,"prompt":731,"starter":732,"solution":733,"answer":734,"docs":735,"section":335,"sectionTitle":10},"layers-036","testing-and-ci",[730,414],"testing","Write the fixture config and the `@nuxt\u002Ftest-utils` e2e test that prove a consumer override wins. The fixture is a tiny app at `test\u002Ffixtures\u002Foverride\u002F` that extends the layer (the repository root) and ships its own `app\u002Fcomponents\u002FTeam\u002FButton.vue` rendering `data-source=\"consumer\"`; the layer's version renders `data-source=\"layer\"`. The test must also confirm the layer's `\u002Fapi\u002F_team\u002Fhealth` route still answers with the fixture's own runtime value.\n","import { describe, expect, it } from 'vitest'\nimport { setup } from '@nuxt\u002Ftest-utils\u002Fe2e'\n\ndescribe('consumer overrides', async () => {\n  \u002F\u002F TODO\n})\n","\u002F\u002F test\u002Ffixtures\u002Foverride\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n  extends: ['..\u002F..\u002F..'],                                  \u002F\u002F the layer itself\n  runtimeConfig: { public: { teamRelease: 'test' } },      \u002F\u002F overrides the layer default\n})\n\n\u002F\u002F test\u002Foverride.test.ts\nimport { fileURLToPath } from 'node:url'\nimport { describe, expect, it } from 'vitest'\nimport { $fetch, setup } from '@nuxt\u002Ftest-utils\u002Fe2e'\n\ndescribe('consumer overrides', async () => {\n  \u002F\u002F Builds and boots the fixture app once for the whole describe block.\n  await setup({ rootDir: fileURLToPath(new URL('.\u002Ffixtures\u002Foverride', import.meta.url)) })\n\n  it('renders the fixture TeamButton, not the layer one', async () => {\n    const html = await $fetch('\u002F')\n    expect(html).toContain('data-source=\"consumer\"')\n    expect(html).not.toContain('data-source=\"layer\"')\n  })\n\n  it('still serves the layer route with the fixture runtime value', async () => {\n    const res = await $fetch\u003C{ ok: boolean, release: string }>('\u002Fapi\u002F_team\u002Fhealth')\n    expect(res).toMatchObject({ ok: true, release: 'test' })\n  })\n})\n","A layer has no entry function, so the only way to test it is to extend it from an app and run that app: `setup({ rootDir })` from `@nuxt\u002Ftest-utils\u002Fe2e` builds and boots the fixture, and `$fetch` returns server-rendered HTML for a page and parsed JSON for an API route. Asserting both the positive and the negative (`toContain` **and** `not.toContain`) is what makes this a priority test rather than a rendering test: if the layer's component leaks through, the override silently stopped working.\n\nTwo details to mention. `await setup(...)` goes directly inside the async `describe` callback, before the `it` blocks, because it registers Vitest hooks. And this fixture still lives inside the layer's repository, so it cannot catch alias mistakes; one fixture in a separate workspace package is what catches those. When this test breaks after a refactor, the cause is usually a changed resolved *name* — a moved file, a new `prefix`, `pathPrefix` toggled — not the component itself.\n",[736],{"title":737,"url":738},"Nuxt testing guide (@nuxt\u002Ftest-utils)","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fgetting-started\u002Ftesting",{"id":740,"type":324,"difficulty":476,"subsection":728,"tags":741,"prompt":742,"answer":743,"docs":744,"section":335,"sectionTitle":10},"layers-037",[730,414,453],"A consumer opens a ticket that says \"it works in your playground but not in my app\". How do you turn that into a test, and what classes of bug does a playground structurally miss?\n","The playground is a Nuxt app that lives *inside* the layer's repository, so it accidentally satisfies assumptions no real consumer satisfies. `~` and `@` resolve to the layer's own `app\u002F`, so an alias in the layer's `nuxt.config` works; the layer's `.env` is the project's `.env`, so runtime defaults look fine; the layer's source is inside the watched tree, so hot reload works; Tailwind's automatic source detection starts at the playground root and finds the layer's classes, which it will not do for a layer installed under `node_modules`; and there is exactly one layer, so no priority conflict exists.\n\nThe test that catches all of that is a **fixture app outside the layer folder** — ideally a separate workspace package — driven by `@nuxt\u002Ftest-utils\u002Fe2e`: `setup({ rootDir })` builds and boots it, `$fetch` asserts the rendered HTML and the layer's server routes, a second fixture ships its own copy of a layer component so the override assertion proves the priority rule, `createPage` fails on any console message matching `\u002Fhydration\u002Fi`, and `nuxt typecheck` runs against the fixture so a broken `AppConfigInput` augmentation is caught before consumers see it. CI runs that across a matrix: the lowest Nuxt in `peerDependencies`, `latest`, and nightly allowed to fail.\n\nFor the ticket itself I triage in the other direction: ask for their resolved layer order (`getLayerDirectories()` printed from a throwaway local module) before debugging anything, because \"your layer broke my app\" is often a second layer shadowing mine.\n",[745],{"title":737,"url":738},{"id":747,"type":324,"difficulty":347,"subsection":748,"tags":749,"prompt":752,"answer":753,"docs":754,"section":335,"sectionTitle":10},"layers-038","pitfalls",[750,751,453],"css","tailwind","A consumer says the layer's CSS is missing after installing it from npm: the components render but they are completely unstyled, and it worked when they tried the layer from a local checkout. What do you check?\n","Two candidates, and the local-checkout detail picks one of them.\n\nIf the components render with the right markup but no styling, this is Tailwind v4 source detection. Automatic detection starts at the project root and **skips `node_modules`**, so the utility classes used inside `@team\u002Fnuxt-layer-base` are never seen and the corresponding rules are never generated. From a local checkout the layer sat inside the scanned tree, which is exactly why it worked. The fix is an explicit `@source \"..\u002F..\u002F..\u002Fnode_modules\u002F@team\u002Fnuxt-layer-base\";` in the consumer's CSS entry, or shipping it from the layer's own stylesheet so no consumer has to know.\n\nThe other candidate is the stylesheet not being loaded at all, which produces the same symptom: either the layer's `css` entry used `~\u002Fassets\u002F...` and now resolves into the consumer's project, or `app\u002F` was missing from the published `files` array. `npm pack` and the browser's network tab separate the two in a minute.\n\nWhat I change afterwards is the process, not just the line: this class of bug is invisible in the playground, so the layer's CI gets a fixture that installs the packed tarball rather than the source.\n",[755],{"title":756,"url":757},"Tailwind v4 — detecting classes in source files","https:\u002F\u002Ftailwindcss.com\u002Fdocs\u002Fdetecting-classes-in-source-files",{"id":759,"type":351,"difficulty":325,"subsection":748,"language":750,"tags":760,"prompt":761,"starter":762,"solution":763,"answer":764,"docs":765,"section":335,"sectionTitle":10},"layers-039",[750,751],"An app installs `@team\u002Fnuxt-layer-base` from npm and the layer's components come out unstyled. Fix the app's CSS entry `app\u002Fassets\u002Fcss\u002Fmain.css` so Tailwind v4 generates the classes the layer uses.\n","@import \"tailwindcss\";\n@import \"@nuxt\u002Fui\";\n","@import \"tailwindcss\";\n@import \"@nuxt\u002Fui\";\n\n\u002F* Tailwind v4 detects classes automatically from the project root but skips node_modules,\n   so an npm-installed layer is invisible until its directory is registered explicitly.\n   The path is relative to THIS file: app\u002Fassets\u002Fcss -> app\u002Fassets -> app -> project root. *\u002F\n@source \"..\u002F..\u002F..\u002Fnode_modules\u002F@team\u002Fnuxt-layer-base\";\n","Tailwind v4 has no `content` array any more: it walks the project tree from the root, ignoring `node_modules` and anything in `.gitignore`. A layer under `layers\u002F` is therefore scanned for free, and the same layer installed from the registry is not, which is why this bug only appears after publishing. `@source` adds a directory back, and the path resolves relative to the CSS file it is written in, so the `..\u002F..\u002F..\u002F` matters. Mention the alternative worth preferring: put the `@source` line in the **layer's** stylesheet so consumers inherit the fix. And mention the tell that separates this from a missing stylesheet: here the markup and class attributes are correct in the DOM and only the rules are absent.\n",[766],{"title":756,"url":757},{"id":768,"type":324,"difficulty":476,"subsection":769,"tags":770,"prompt":772,"answer":773,"docs":774,"section":335,"sectionTitle":10},"layers-040","layers-vs-modules",[771,513,453],"architecture","A team wants each product domain — catalog, checkout, account — as its own layer inside one Nuxt app, owned by a different squad. Talk me through whether that works and what you would put in place before agreeing to it.\n","It works, and the docs list modular architecture and DDD in large projects as a use case. Each domain becomes `layers\u002F1.catalog`, `layers\u002F2.checkout`, `layers\u002F3.account` with its own `app\u002Fpages`, components, composables and `server\u002Fapi`, and the project is just the shell. Each squad gets file-based routing and CODEOWNERS over a directory, with no build tooling and no publishing.\n\nWhat I insist on first, because all of it is invisible until it hurts. **One router and one Nitro server**: two squads both adding `app\u002Fpages\u002Findex.vue` do not get two home pages, the higher-priority layer silently wins, so routes and API paths are namespaced by domain and somebody is explicitly named owner of the shell pages. **Order must be deliberate**: auto-scanned folders sort alphabetically with later names winning, so numeric prefixes, or list them in `extends` where the first entry wins. **One dependency graph and one bundle**: a heavy library added by checkout is in everyone's build and everyone's `node_modules`, so bundle budgets belong in CI. **No cross-domain imports**, enforced by an ESLint `no-restricted-imports` rule on `#layers\u002F\u003Csibling>`, or the boundaries exist only in the folder names.\n\nAnd I say where it stops: layers give ownership boundaries inside *one deployable*. If the squads need independent release cadence or independent uptime, this is the wrong tool and they want separate apps that share a base layer.\n",[775],{"title":776,"url":334},"Layers — getting started (use cases)",{"id":778,"type":338,"difficulty":325,"subsection":769,"tags":779,"prompt":780,"options":781,"correct":786,"answer":787,"docs":788,"section":335,"sectionTitle":10},"layers-041",[525,328],"Which of these requirements cannot be met by a layer and needs a module?\n",[782,783,784,785],"Ship a `useTeamAuth()` composable together with the login pages and middleware that use it","Provide default `routeRules` that cache `\u002Fapi\u002F_team\u002F**` for sixty seconds","Provide theme tokens that each app overrides in its own `app.config.ts`","Register `@nuxt\u002Fimage` only when the consuming app sets `teamToolkit.images` to true",[476],"The rule of thumb is that if it needs an `if`, it is a module. A layer never executes: c12 loads its `nuxt.config`, defu merges it, and the directories are scanned, so there is no place to branch on the consumer's options. A module's `setup(options, nuxt)` runs in Node at build time and can decide. The other three are exactly what layers are for — app code discovered by convention, and config that merges — and trying to express them as a module would mean generating files to do what directory scanning already does for free. The composable and pages option is the tempting distractor for people who think \"reusable package\" implies \"module\".\n",[789],{"title":790,"url":791},"Module author guide","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fmodules",{"id":793,"type":351,"difficulty":476,"subsection":794,"language":795,"tags":796,"prompt":797,"starter":798,"solution":799,"answer":800,"docs":801,"section":335,"sectionTitle":10},"layers-042","designing-a-team-layer","vue",[701,452,370],"`\u003CTeamPrimaryButton>` is being folded into `\u003CTeamButton variant=\"primary\">`. Write the deprecation-safe replacement for `layers\u002Fbase\u002Fapp\u002Fcomponents\u002FTeam\u002FPrimaryButton.vue`: consumers on the old name must keep working unchanged for one major, must see a warning in development only, and must not get a duplicated set of attributes.\n","\u003Ctemplate>\n  \u003Cbutton class=\"btn btn--primary\" v-bind=\"$attrs\">\n    \u003Cslot \u002F>\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\n\u002F\u002F Deprecated in 2.4, removed in 3.0. Use \u003CTeamButton variant=\"primary\">.\nimport TeamButton from '.\u002FButton.vue'\n\n\u002F\u002F Dev-only: the warning is compiled out of the consumer's production bundle.\nif (import.meta.dev) {\n  console.warn(\n    '[nuxt-layer-base] \u003CTeamPrimaryButton> is deprecated and will be removed in 3.0. '\n    + 'Use \u003CTeamButton variant=\"primary\">. See MIGRATION.md#2-4.',\n  )\n}\n\n\u002F\u002F We forward $attrs explicitly, so turn off the automatic fallthrough.\ndefineOptions({ inheritAttrs: false })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CTeamButton variant=\"primary\" v-bind=\"$attrs\">\n    \u003C!-- forward every slot, not just the default one -->\n    \u003Ctemplate v-for=\"(_, name) in $slots\" #[name]=\"slotProps\">\n      \u003Cslot :name=\"name\" v-bind=\"slotProps ?? {}\" \u002F>\n    \u003C\u002Ftemplate>\n  \u003C\u002FTeamButton>\n\u003C\u002Ftemplate>\n","The old name stays registered, so no consumer edits anything at upgrade time; it is now a thin wrapper that renders the new component. `import.meta.dev` is what keeps the warning out of twenty production bundles — never `process.env.NODE_ENV` in layer code — and the message names the replacement and the migration anchor, because a warning without an instruction just becomes noise people filter. `defineOptions({ inheritAttrs: false })` prevents the classic double-binding: without it, `$attrs` would land on the inner component *and* fall through. The generic slot forwarding keeps named slots working, which a naive `\u003Cslot \u002F>` silently drops.\n\nWhat to say beyond the code: this is one step of a sequence, not the whole thing. Deprecate and warn, publish the migration note, ship a codemod if the rename touches many files, watch adoption, and only then remove in the next major — and you cannot honestly remove it until you can say which apps still use it.\n",[802],{"title":803,"url":804},"components directory (naming, prefixes)","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fapp\u002Fcomponents",{"id":806,"type":324,"difficulty":476,"subsection":794,"tags":807,"prompt":809,"answer":810,"docs":811,"section":335,"sectionTitle":10},"layers-043",[701,808,453],"governance","How would you roll out a breaking change to a base layer that twenty applications extend?\n","Not as a release, as a sequence. It starts before any code: a one-page RFC that states the problem with the apps that asked for it, the public surface after the change, the override recipe, the measured cost, and the rollout plan. Then adoption data, because the whole rollout is gated on knowing who uses what — a CI job that reads each app's lockfile for the layer version, plus grep across the repositories for the symbol I am about to change.\n\nThen the actual sequence. Ship the new thing first in a **minor**, so both shapes exist. Keep the old name as a thin wrapper that delegates to the new one and warns through `import.meta.dev` with the migration anchor in the message. Publish the migration note and, if the rename touches many files, a codemod so upgrading is a command rather than an afternoon. Take one canary app — preferably one my team owns — through the upgrade to find what the note forgot. Then let Renovate open the rest, watch adoption move, and only cut the **major** that removes the old name when the number of apps still on it is zero, with a stated support window and backported security fixes for the previous major.\n\nWhat breaks if you skip steps: a straight rename lands as a red build in twenty repositories at once, in an error that points at *their* page, and every team pays the debugging cost separately. And a deprecation you cannot measure is a guess, so the old name never actually gets removed.\n",[812],{"title":395,"url":365},{"id":814,"type":351,"difficulty":347,"subsection":794,"language":353,"tags":815,"prompt":817,"starter":818,"solution":819,"answer":820,"docs":821,"section":335,"sectionTitle":10},"layers-044",[524,816,370],"types","Write the base layer's theme API: an `app.config.ts` with the team's tokens (brand name, a density of `compact` or `comfortable`, an analytics toggle defaulting to off) plus the type augmentation that gives consumers autocomplete and an error on a typo when they override it in their own `app.config.ts`.\n","\u002F\u002F layers\u002Fbase\u002Fapp\u002Fapp.config.ts\nexport default defineAppConfig({\n  \u002F\u002F TODO\n})\n","\u002F\u002F layers\u002Fbase\u002Fapp\u002Fapp.config.ts\nexport default defineAppConfig({\n  team: {\n    brand: 'Acme',\n    density: 'comfortable',\n    analytics: { enabled: false },\n  },\n  ui: { colors: { primary: 'emerald', neutral: 'zinc' } },\n})\n\n\u002F\u002F layers\u002Fbase\u002Fapp\u002Ftypes\u002Fapp-config.d.ts\ndeclare module 'nuxt\u002Fschema' {\n  interface AppConfigInput {\n    team?: {\n      brand?: string\n      density?: 'compact' | 'comfortable'\n      analytics?: { enabled?: boolean }\n    }\n  }\n}\n\n\u002F\u002F a .d.ts with only declarations is ambient; this makes it a module so the augmentation applies\nexport {}\n","`app.config` is the right channel for this because it is bundled, reactive through `useAppConfig()` and `updateAppConfig()`, deep-merged across layers with the project winning, and typed — everything a theme API needs. It is also the wrong channel for anything that differs per deployment, since no environment variable can touch it; that belongs in `runtimeConfig`.\n\nThe augmentation is the part candidates forget. Without it, a consumer writing `team: { densty: 'compact' }` gets no error and silently inherits the default, and `useAppConfig().team` is untyped in their editor. Every key is optional, because a consumer overrides one key and inherits the rest. Two practical notes: the trailing `export {}` turns the file into a module so the `declare module` augmentation is applied rather than replacing `nuxt\u002Fschema`, and the file must be inside the published `files` list — a missing `.d.ts` produces the consumer-side error \"Property `team` does not exist on type AppConfig\", which is a breaking change nobody meant to ship.\n",[822],{"title":533,"url":534},{"id":824,"type":324,"difficulty":476,"subsection":794,"tags":825,"prompt":827,"answer":828,"docs":829,"section":335,"sectionTitle":10},"layers-045",[513,826,808],"api","How would you design the base layer for a team of twenty apps? Start with what its public surface is.\n","A layer is an API whose surface is *files*, so I write that surface down first: component names, composable and util names, `app.config` keys, `runtimeConfig` keys, page routes and layout names, server route paths, and the modules in the preset. Everything else is internal and marked as such — an `_internal\u002F` folder, `pathPrefix` kept out of the public naming, no README mention — because anything a consumer can reach by name, they will reach, and then I cannot move it.\n\nEverything public is prefixed: `TeamButton`, `useTeamAuth`, `team.*` in `app.config`, `NUXT_TEAM_*` in the environment, `\u002Fapi\u002F_team\u002F**` for routes. Prefixes stop collisions with the apps and with the other layers they extend, and they make adoption greppable, which is what later makes deprecations measurable.\n\nCustomisation is documented as three levels, in order of preference: configure through `app.config` keys the layer exposes; wrap by placing a file at the same path and rendering the original through `#layers\u002Fbase\u002F...`; replace outright, accepting that the layer's future fixes stop arriving. Publishing those recipes is what keeps overrides from becoming support tickets, and it is also why the wrapped file paths become part of the API.\n\nStructurally: one `base` layer with the preset, design system and cross-cutting server pieces, optional feature layers (`auth`, `analytics`) that apps compose themselves rather than extending each other, and a firm answer of \"no\" to \"just add a prop for our app\", because every consumer pays for that prop forever.\n",[830,831],{"title":395,"url":365},{"title":533,"url":534},{"id":833,"type":324,"difficulty":476,"subsection":794,"tags":834,"prompt":836,"answer":837,"docs":838,"section":335,"sectionTitle":10},"layers-046",[835,415,808],"performance","What does extending your base layer actually cost each of the twenty apps, and how do you keep that cost under control?\n","Three budgets, all of which the consumer pays and none of which they can opt out of after the fact, because arrays only concatenate.\n\n**Bundle and startup.** Globally registered components land in the entry chunk, every plugin the layer ships runs on every SSR request and every page load, and the layer's CSS and fonts are downloaded by every app whether or not they use the design system. I measure this rather than argue about it: build an empty fixture and the basic fixture, compare the client entry size and a cold TTFB, and put a budget on the delta in the layer's own CI so a regression fails the layer's build rather than the apps'.\n\n**Security.** Server routes, `server\u002Fmiddleware` and Nitro plugins run in production in twenty deployments. Middleware from every layer runs on every request, so one owner for cross-cutting middleware; no debug endpoints; input validated; secrets only as empty `runtimeConfig` keys the app fills from its environment; dependencies watched by Renovate and advisories, since the layer's `dependencies` become the consumers' transitively.\n\n**Governance.** One owning team, a one-page RFC for any change to the public surface, a CHANGELOG with migration notes, adoption tracked per app.\n\nThe design consequence of all three is the same rule: anything expensive is opt-in. A plugin reads a flag from `app.config` and returns early, a heavy integration lives in a separate feature layer apps choose to extend, and a module in the preset can be switched off with `image: false`.\n",[839],{"title":840,"url":791},"Module author guide — best practices",{"id":842,"type":338,"difficulty":325,"subsection":580,"tags":843,"prompt":844,"options":845,"correct":850,"answer":851,"docs":852,"section":335,"sectionTitle":10},"layers-047",[452,328],"The base layer ships an `app\u002Fapp.vue` containing a skip link and `\u003CNuxtLayout>\u003CNuxtPage \u002F>\u003C\u002FNuxtLayout>`. A product app has its own `app\u002Fapp.vue`. What renders?\n",[846,847,848,849],"Both: the layer's `app.vue` wraps the app's, the same way layouts nest","The app's `app.vue` only; the layer's is ignored entirely, and an app with no `app.vue` falls back to the highest-priority layer that has one","The layer's `app.vue`, because a layer's shell is resolved before the project's files are scanned","Neither: Nuxt reports two root components and falls back to its built-in shell",[325],"`app.vue`, `error.vue` and `app\u002Frouter.options.ts` follow the same same-name-wins rule as everything else: the project's file replaces the layer's, and there is no merging or nesting — that only happens with layouts. If the project has none, the highest-priority layer that ships one is used, which is what makes a layer `app.vue` a legitimate way to provide a default shell. The consequence to say out loud is the trap: the moment a consumer adds their own `app.vue`, your skip link and any providers in it disappear silently. So either document the required shell in the README, or move anything cross-cutting into a plugin or a layout, where it survives.\n",[853],{"title":395,"url":365},{"id":855,"type":324,"difficulty":476,"subsection":580,"tags":856,"prompt":859,"answer":860,"docs":861,"section":335,"sectionTitle":10},"layers-048",[857,858,513],"composables","imports","How does a consuming app override a composable that comes from your layer, how is that different from overriding a component, and how do you design a layer's composables so overriding stays safe?\n","Composables and utils are matched as **auto-imports, by exported name**, not by file path. Two routes to an override, with different consequences. If the app puts a file at the same path — `app\u002Fcomposables\u002FuseTeamAuth.ts` — it wins cleanly by priority and the layer's file is not scanned for that name. If the app exports the same name from a *differently named* file, the higher-priority layer still wins the import but Nuxt logs a duplicate-import warning, and which file that is depends on the resolved layer order, so it changes the day someone adds another layer. I never design around the second case.\n\nThe difference from components is that a component override is visible — you can see which template rendered, and `components:extend` will tell you the resolved file path. An auto-import override is invisible: the call site is identical, so the app can be running the layer's implementation and believe it is running its own, or the reverse. `server\u002Futils` has the same single namespace on the Nitro side.\n\nDesigning for it: prefix every exported name (`useTeamAuth`, `teamFormatDate`), keep genuinely internal helpers out of the auto-imported directories or behind an `_` name so nobody collides with them, and offer the wrap path explicitly — the app's file imports the original through `#layers\u002Fbase\u002Fapp\u002Fcomposables\u002FuseTeamAuth` and decorates it, which keeps the layer's fixes flowing. And because the override is silent, the layer's fixture tests assert the *contract* of each public composable, so a rename shows up as a failing test rather than as a consumer's bug report.\n",[862],{"title":395,"url":365},{"id":864,"type":324,"difficulty":476,"subsection":425,"tags":865,"prompt":866,"answer":867,"docs":868,"section":335,"sectionTitle":10},"layers-049",[427,808,370],"An app extends `@team\u002Fnuxt-layer-base` and `@team\u002Fnuxt-layer-auth` from npm and also has its own `layers\u002Ftheme` and `layers\u002Flegacy` folders. A developer adds `layers\u002Fwidgets`. What can silently change, and how do you make the resolved order something the team can reason about?\n","The resolved order today is: project, then the auto-scanned folders alphabetically with **later names winning** (`theme` above `legacy`), then the `extends` entries with the **first** highest (`base` above `auth`, if that is the array order). Adding `layers\u002Fwidgets` inserts it between `theme` and `legacy` by alphabet, so it now shadows `legacy` and both npm layers for any component name, route, layout, named middleware or server handler path it happens to reuse. Nothing warns: same-name-wins is silent by design. Config is affected too, though less dramatically — the new layer's scalars sit above `legacy` and both npm layers, and anything it adds to `css`, `plugins` or `modules` is simply appended to everyone's list.\n\nWhat makes this reasonable rather than lucky is stating the order instead of deriving it. Numeric prefixes (`1.legacy`, `2.theme`, `3.widgets`) make an insertion a deliberate choice of number; or list the folders in `extends` in the order you mean, since Nuxt deduplicates a layer that is both auto-scanned and listed. Then make shadowing require intent: prefix each layer's component and composable names so a collision has to be typed out, and namespace routes and `server\u002Fapi` paths per layer.\n\nFinally, assert it. A fixture test that a consumer override wins, and a CI step that prints `getLayerDirectories()` and compares it with a committed snapshot, turn \"somebody added a folder\" from a silent behaviour change into a failing build.\n",[869,870],{"title":438,"url":334},{"title":392,"url":393},{"id":872,"type":324,"difficulty":347,"subsection":484,"tags":873,"prompt":874,"answer":875,"docs":876,"section":335,"sectionTitle":10},"layers-050",[816,370,730],"How do consumers get TypeScript types for what your layer ships — its composables, its `app.config` keys and its `runtimeConfig` keys — and what tends to break once the layer is installed from npm?\n","Most of it is free. Nuxt generates `.nuxt\u002Ftsconfig.*.json` with path mappings for **every** layer, so the layer's source type-checks inside any consumer and inside its own playground, and whatever the layer contributes to the auto-import scan is typed at the call site in the consuming app. `runtimeConfig` types follow the declared defaults merged across layers, which is another reason to declare every key with a typed default: a key with no default has no type and no `NUXT_*` override. The layer package's own `tsconfig.json` should do nothing clever — extend the generated one from the playground.\n\nThe one thing that is not automatic is `app.config`. Its shape comes from an `AppConfigInput` augmentation of `nuxt\u002Fschema`, shipped as a `.d.ts` inside the layer, and that file has to actually reach consumers: if `app\u002F` or the `.d.ts` is missing from the published `files` array, the consumer gets \"Property 'team' does not exist on type AppConfig\" in their own `app.config.ts`, which is a breaking change nobody meant to release. The `#layers\u002F\u003Cname>` alias has a similar prerequisite — the layer must be named, by its folder under `layers\u002F` or by `$meta: { name }`.\n\nBecause types are part of the contract, `nuxt typecheck` runs in the layer's CI against the playground *and* against a fixture outside the layer folder, which is the only place a missing published file shows up.\n",[877,880],{"title":878,"url":879},"nuxt typecheck","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fcommands\u002Ftypecheck",{"title":881,"url":365},"Authoring Nuxt layers (relative paths and aliases)",{"id":883,"type":324,"difficulty":325,"subsection":326,"tags":884,"prompt":885,"answer":886,"docs":887,"section":890,"sectionTitle":82},"module-authoring-001",[328],"What is a Nuxt module, and when does it run?\n","A module is build-time code: a function Nuxt calls once, in Node, during `nuxt dev`, `nuxt build` or `nuxt generate`, sequentially with the other modules in the order of `nuxt.options.modules`. It never runs in the browser and never runs inside the production Nitro server.\n\nEverything it can do reduces to three primitives: mutate `nuxt.options`, register hooks, and generate or register files. Kit helpers such as `addPlugin`, `addImports`, `addServerHandler` and `addTemplate` are ergonomic wrappers over those three.\n\nAnything the consumer's app executes at runtime is code the module *injected* from its `runtime\u002F` directory. That boundary is where most module bugs live: a value that exists in the build world does not exist at runtime unless the module deliberately carried it across through runtime config, `appConfig` or a generated template.\n",[888],{"title":889,"url":791},"Module Author Guide","module-authoring",{"id":892,"type":324,"difficulty":347,"subsection":326,"tags":893,"prompt":894,"answer":895,"section":890,"sectionTitle":82},"module-authoring-002",[335,513],"You are building a toolkit for an internal frontend team. How do you decide what goes in a layer and what goes in a module?\n","A layer is declarative: it ships app code and config that Nuxt merges into the consuming project — components, composables, pages, server routes, `nuxt.config` defaults, design tokens. It cannot execute anything at build time.\n\nA module is imperative build-time code. The moment the toolkit has to *do* something — generate a registry from the consumer's files, inject a plugin at a specific position, validate options, branch on which other modules are installed, extend Nitro — that logic has to be a module.\n\nIn practice a company toolkit is both: a layer that lists its module in `modules`, so consumers get conventions, app code and build behaviour from a single `extends` entry. Two mechanics make it safe. Layer configs merge with `defu`, so `modules` arrays concatenate, and Nuxt deduplicates modules by `meta.name`, so the layer and the project can both list it and it runs once. And module options resolve as `defu(inline, nuxt.config[configKey], defaults)`, so the project beats the layer, which beats my defaults.\n",{"id":897,"type":338,"difficulty":325,"subsection":326,"tags":898,"prompt":899,"options":900,"correct":905,"answer":906,"section":890,"sectionTitle":82},"module-authoring-003",[335],"Your team layer needs to scan a folder in the consuming app and generate a route registry at build time. Why can the layer not do this on its own?\n",[901,902,903,904],"Layers are resolved after modules, so the registry would always be one build behind","A layer only contributes app code and merged config; executing build-time logic requires a module","Layers cannot use `#build` aliases, so the generated file would never resolve","Layers are development-only, so the registry would be missing from production builds",[325],"A layer is data, not behaviour: c12 loads its `nuxt.config` and Nuxt merges its directories into the resolution chain. There is no place in a layer to run code during the build. Scanning a directory, generating a module and registering it as a template are all `@nuxt\u002Fkit` operations, which means a module — usually one the layer lists in its own `modules` array, so the consumer still only writes one `extends` entry. The distractors are all false: layers are fully present in production, they participate in alias resolution, and ordering is not the problem.\n",{"id":908,"type":324,"difficulty":347,"subsection":909,"tags":910,"prompt":911,"answer":912,"section":890,"sectionTitle":82},"module-authoring-004","mental-model",[387,461],"Walk me through what `defineNuxtModule` does with the definition object you hand it.\n","It normalises the definition into a `(inlineOptions, nuxt)` function and wires up the bookkeeping around your `setup`. In order: it resolves `meta` (`name` — also the dedupe key, `configKey`, `version`, `compatibility`, `docs`); it deduplicates, so a module already recorded in `_requiredModules` returns `false` instead of running twice; it runs the compatibility check with `checkNuxtCompatibility`, which disables an incompatible module with a warning; it resolves options as `defu(inlineOptions, nuxt.options[configKey], defaults)` and then applies the `schema` defaults if you gave one; it registers the `hooks` map with `addHooks` *before* `setup`; it resolves `moduleDependencies`; it fires `module:before` and `module:done` around the call and records timings in `_installedModules`; and it calls `onInstall` or `onUpgrade` based on the `setups` record in the project's `.nuxtrc`, which needs both `meta.name` and `meta.version` to work at all.\n\nThe consequence worth stating: user config always wins over my defaults, and arrays concatenate.\n",{"id":914,"type":338,"difficulty":347,"subsection":909,"tags":915,"prompt":917,"options":918,"correct":923,"answer":924,"docs":925,"section":890,"sectionTitle":82},"module-authoring-005",[461,916],"options","Your module declares `defaults: { features: ['status-page'] }`. A consumer writes `toolkit: { features: ['audit'] }` in `nuxt.config`. What does `options.features` hold inside `setup`?\n",[919,920,921,922],"`['audit']`, because user config replaces module defaults","`['status-page']`, because module defaults are applied last","`['audit', 'status-page']`, because defu concatenates arrays","`[]`, because conflicting array values cancel out",[347],"Options resolve as `defu(inlineOptions, nuxt.options[configKey], defaults)`, and defu **concatenates** arrays rather than replacing them: the higher-priority source comes first. So the consumer gets a feature they explicitly did not ask for, which is one of the classic module surprises. The fixes are to keep array defaults empty and treat \"empty means default\" inside `setup`, to normalise after resolution, or to build your own merger with `createDefu` from defu giving arrays replace semantics. The same behaviour explains why layer and project `modules` and `css` arrays add up rather than override.\n",[926],{"title":472,"url":473},{"id":928,"type":324,"difficulty":476,"subsection":909,"tags":929,"prompt":931,"answer":932,"section":890,"sectionTitle":82},"module-authoring-006",[335,930],"ordering","A consumer reports that your module behaves differently depending on whether they list it themselves or inherit it from your team layer. What do you check?\n","First, whether it is running twice or running at a different position. Nuxt deduplicates by `meta.name` (falling back to `configKey`), so a named module listed by both the layer and the project runs once — but the *position* comes from the merged `modules` array, where project entries land before the layer's. If my `setup` reads `nuxt.options` for something another module contributes, I now see a different snapshot in the two cases.\n\nSo I check: is `meta.name` actually set (an anonymous inline module is never deduplicated, and its raw `nuxt.options.css.push()` calls run twice); is the consumer passing inline options in one place and `nuxt.config` options in the other, which changes the defu precedence; and am I reading state in `setup` that should be read in `modules:done`.\n\nThe fixes are ordering-independent by construction: declare relationships with `moduleDependencies`, detect with `hasNuxtModule` (which also sees modules listed after me), and move anything that needs the full picture into `modules:done` or `ready`.\n",{"id":934,"type":351,"difficulty":325,"subsection":909,"language":353,"tags":935,"prompt":936,"starter":937,"solution":938,"answer":939,"section":890,"sectionTitle":82},"module-authoring-007",[387],"Write the `src\u002Fmodule.ts` skeleton for `nuxt-team-toolkit`. It must declare a name, a `toolkit` config key, a version and a minimum Nuxt version, take a typed `apiBase` option with a default, expose that option to the browser, and inject a runtime plugin.\n","import { defineNuxtModule } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  \u002F\u002F TODO\n})\n","import { addPlugin, createResolver, defineNuxtModule } from '@nuxt\u002Fkit'\n\nexport interface ModuleOptions {\n  apiBase: string\n  features: string[]\n}\n\nexport default defineNuxtModule\u003CModuleOptions>().with({\n  meta: {\n    name: 'nuxt-team-toolkit',\n    configKey: 'toolkit',\n    version: '1.0.0',\n    compatibility: { nuxt: '>=4.0.0' },\n  },\n  defaults: { apiBase: 'https:\u002F\u002Fapi.internal', features: [] },\n  setup (options, nuxt) {\n    const { resolve } = createResolver(import.meta.url)\n\n    nuxt.options.runtimeConfig.public.toolkit = { apiBase: options.apiBase }\n    addPlugin(resolve('.\u002Fruntime\u002Fapp\u002Fplugins\u002Ftoolkit'))\n  },\n})\n","The points an interviewer looks for: `meta.name` is the dedupe key, so it must be set for a layer and a project to both list the module safely; `meta.version` is what makes `onInstall`\u002F`onUpgrade` possible later; `compatibility` turns an unsupported Nuxt version into a build-time warning; `createResolver(import.meta.url)` resolves paths relative to the module file, which is what keeps `.\u002Fruntime\u002F...` working from `dist` after publishing. The `.with({...})` form types `defaults` precisely, so `setup` receives options where defaulted keys are no longer optional. Common mistakes: hardcoding a relative path instead of resolving it, and writing the option into runtime config without `defu`, which overwrites a value the consumer set deliberately.\n",{"id":941,"type":324,"difficulty":347,"subsection":942,"tags":943,"prompt":944,"answer":945,"docs":946,"section":890,"sectionTitle":82},"module-authoring-008","kit-api",[387,564],"What does `addPlugin` do internally, and when is its default behaviour the wrong choice?\n","It normalises the `src` — a string becomes an object, aliases are resolved, the extension is looked up — removes any existing plugin entry with the same resolved path, infers `mode` from a `.client` or `.server` suffix in the filename, and **prepends** to `nuxt.options.plugins`.\n\nPrepending means module plugins run before the app's own plugins. That is what you want for a provider other code depends on, and exactly what you do not want when your plugin depends on something the app sets up — an auth client, a store, another module's injection. Then you pass `{ append: true }`, give the plugin an `order` (lower runs first; user plugins default to 0, and you should stay within roughly -20 to 20), or declare `dependsOn` inside the plugin itself, which is the most explicit of the three because the ordering lives with the code that needs it rather than in the registration call.\n\nThe dedupe-by-path detail is worth mentioning: calling `addPlugin` twice is harmless, while `nuxt.options.plugins.push()` is not.\n",[947],{"title":948,"url":949},"Kit reference","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fkit",{"id":951,"type":338,"difficulty":347,"subsection":942,"tags":952,"prompt":954,"options":955,"correct":960,"answer":961,"section":890,"sectionTitle":82},"module-authoring-009",[387,953],"boundary","Why does `useNuxt()` work inside a build helper that was never passed the `nuxt` instance, and what happens if you call it from `src\u002Fruntime`?\n",[956,957,958,959],"It reads a module-scoped singleton exported by `@nuxt\u002Fkit`; in runtime code it returns `undefined`","It resolves the instance from an unctx async context created during the build; in runtime code that context does not exist, so it throws","It reads `globalThis.__nuxt`, which Nuxt also defines at runtime, so the same call works on both sides","It is a Vue `inject` under the hood, so it works in any component setup, including at runtime",[325],"Kit keeps the current Nuxt instance in an [unctx](https:\u002F\u002Fgithub.com\u002Funjs\u002Functx) context backed by `AsyncLocalStorage`, with a global fallback, and Nuxt runs module installation inside it. That is why every kit helper can reach `nuxt` without you passing it around — and why calling one outside a module setup or a hook throws \"Nuxt instance is unavailable\". Runtime code is a different process entirely: there is no build context and no `nuxt.options` there, so the import is either unresolvable in the bundle or fatal at request time. `tryUseNuxt()` is the non-throwing variant for code that can work either way.\n",{"id":963,"type":324,"difficulty":476,"subsection":942,"tags":964,"prompt":967,"answer":968,"section":890,"sectionTitle":82},"module-authoring-010",[965,966],"vite","nuxt5","A module registers a Vite plugin with `addVitePlugin(plugin, { client: true })`. What is the problem for Nuxt 5, and how would you write it instead?\n","`addVitePlugin` accepts `dev`, `build`, `client`, `server` and `prepend`. In Nuxt 4 the client and server builds are two separate Vite instances, so `client: true` simply skipped registration in the server one. Nuxt 5 moves to Vite's Environment API, where a single Vite instance serves multiple environments and one plugin instance sees both. The `client` and `server` options are therefore deprecated, and `extendViteConfig` and the `vite:extendConfig` hook are deprecated along with them, because there is no longer one config per environment to extend.\n\nThe forward-compatible form is to let the plugin decide: `applyToEnvironment(env => env.name === 'client')` to restrict where it runs, and `configEnvironment` for per-environment config. Both already work on Nuxt 4, so this is a change you can make now rather than at migration time. I would also stop relying on `vite.define` differences between environments — note that Nuxt's defaults already include `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false`, which you override rather than invent.\n",{"id":970,"type":351,"difficulty":347,"subsection":942,"language":353,"tags":971,"prompt":972,"starter":973,"solution":974,"answer":975,"section":890,"sectionTitle":82},"module-authoring-011",[387],"In a single `setup`, register four things with kit: a plugin that must run **after** the app's own plugins, an auto-imported `useToolkit` composable, a components directory prefixed `Tk`, and a server route at `\u002Fapi\u002F_toolkit\u002Fhealth`.\n","import { createResolver, defineNuxtModule } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  setup () {\n    const { resolve } = createResolver(import.meta.url)\n    \u002F\u002F TODO\n  },\n})\n","import {\n  addComponentsDir, addImports, addPlugin, addServerHandler,\n  createResolver, defineNuxtModule,\n} from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  setup () {\n    const { resolve } = createResolver(import.meta.url)\n\n    \u002F\u002F addPlugin prepends by default, so append to run after the app's plugins\n    addPlugin(\n      { src: resolve('.\u002Fruntime\u002Fapp\u002Fplugins\u002Fanalytics'), order: 10 },\n      { append: true },\n    )\n\n    addImports({ name: 'useToolkit', from: resolve('.\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseToolkit') })\n    addComponentsDir({ path: resolve('.\u002Fruntime\u002Fapp\u002Fcomponents'), prefix: 'Tk' })\n    addServerHandler({ route: '\u002Fapi\u002F_toolkit\u002Fhealth', handler: resolve('.\u002Fruntime\u002Fserver\u002Fapi\u002Fhealth') })\n  },\n})\n","The one non-obvious call is the plugin: `addPlugin` prepends, so `{ append: true }` is what makes it run after the app's plugins, and `order` disambiguates against other appended plugins. Everything else is a thin wrapper: `addImports` feeds unimport through `imports:extend`, `addComponentsDir` pushes onto the `components:dirs` hook, and `addServerHandler` pushes onto `nuxt.options.serverHandlers`, which Nitro turns into a real handler. Every path goes through `createResolver(import.meta.url)` so it still resolves from `dist\u002F` after publishing. Mention that the server route is namespaced under `\u002Fapi\u002F_toolkit\u002F` — a module that claims `\u002Fapi\u002Fhealth` will collide with the consumer's own app.\n",{"id":977,"type":324,"difficulty":325,"subsection":978,"tags":979,"prompt":980,"answer":981,"docs":982,"section":890,"sectionTitle":82},"module-authoring-012","options-to-runtime",[523,953],"Your module takes an option. How does a composable running in the user's browser read it?\n","Through one of exactly four channels, and I choose by asking whether the value differs per environment, whether it is secret, and how big it is.\n\n`runtimeConfig.public.\u003CconfigKey>` is available on server and client and can be overridden at deploy time with `NUXT_PUBLIC_\u003CKEY>_\u003COPTION>`, but it is serialised into every SSR response, so I keep it to a few scalars. Private `runtimeConfig.\u003CconfigKey>` is server-only and never reaches the client — that is where tokens go. `appConfig` is reactive and typed and reaches both sides, but it is baked into the bundle at build time, so it cannot differ between staging and production; it suits theme and design tokens. And `addTemplate` generates real code imported as `#build\u002F\u003Cfilename>`, which is the right channel for anything large, derived or function-shaped, because the bundler tree-shakes it and it never touches the payload.\n\nThe runtime side reads them with `useRuntimeConfig()`, `useAppConfig()` and a plain import respectively.\n",[983],{"title":530,"url":531},{"id":985,"type":338,"difficulty":347,"subsection":978,"tags":986,"prompt":988,"options":989,"correct":994,"answer":995,"section":890,"sectionTitle":82},"module-authoring-013",[523,987],"env","Your module seeds `runtimeConfig.public.toolkit = { debug: '' }`. A consumer deploys the built server with `NUXT_PUBLIC_TOOLKIT_DEBUG=false`. What does `useRuntimeConfig().public.toolkit.debug` hold at runtime?\n",[990,991,992,993],"The boolean `false`, because Nuxt parses every environment variable as JSON","The string `false`, which is truthy — the value is coerced to the type of the existing default, and the default was a string","An empty string, because environment variables cannot override nested keys","`undefined`, because only keys declared in `nuxt.config` can be overridden, not keys a module seeds",[325],"Environment overrides arrive as strings and are parsed with destr **against the type of the existing default**. A default of `true` or `false` makes `NUXT_PUBLIC_TOOLKIT_DEBUG=false` a real boolean; a default of `''` keeps it a string, and `Boolean('false')` is `true`, so the feature is silently on in production. The rule for module authors is to seed every option with a default of the correct type and never with `undefined` or an empty string standing in for a boolean. Nested keys *are* overridable — the key just has to exist at build time, which is exactly why modules seed everything into runtime config even when the value is empty.\n",{"id":997,"type":351,"difficulty":347,"subsection":978,"language":353,"tags":998,"prompt":1000,"starter":1001,"solution":1002,"answer":1003,"section":890,"sectionTitle":82},"module-authoring-014",[523,999],"templates","In `setup`, carry four options across the build\u002Fruntime boundary, each through the right channel: `apiBase` (needed in the browser, must be overridable per environment), `apiToken` (server only), `theme` (reactive, same everywhere) and `features` (a potentially long list used to tree-shake behaviour).\n","export default defineNuxtModule\u003CModuleOptions>({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  defaults: { apiBase: 'https:\u002F\u002Fapi.internal', apiToken: '', theme: 'default', features: [] },\n  setup (options, nuxt) {\n    \u002F\u002F TODO: four channels\n  },\n})\n","import { addTemplate, defineNuxtModule } from '@nuxt\u002Fkit'\nimport { defu } from 'defu'\n\nexport default defineNuxtModule\u003CModuleOptions>({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  defaults: { apiBase: 'https:\u002F\u002Fapi.internal', apiToken: '', theme: 'default', features: [] },\n  setup (options, nuxt) {\n    \u002F\u002F public runtime config: NUXT_PUBLIC_TOOLKIT_API_BASE overrides it without a rebuild\n    nuxt.options.runtimeConfig.public.toolkit = defu(\n      nuxt.options.runtimeConfig.public.toolkit as Record\u003Cstring, unknown>,\n      { apiBase: options.apiBase },\n    )\n\n    \u002F\u002F private runtime config: server only, NUXT_TOOLKIT_API_TOKEN overrides it\n    nuxt.options.runtimeConfig.toolkit = defu(\n      nuxt.options.runtimeConfig.toolkit as Record\u003Cstring, unknown>,\n      { apiToken: options.apiToken },\n    )\n\n    \u002F\u002F appConfig: reactive, bundled, identical in every environment\n    nuxt.options.appConfig.toolkit = defu(nuxt.options.appConfig.toolkit, { theme: options.theme })\n\n    \u002F\u002F template: real code, tree-shakeable, never in the SSR payload\n    addTemplate({\n      filename: 'toolkit\u002Ffeatures.mjs',\n      getContents: () => [\n        `export const features = ${JSON.stringify(options.features)}`,\n        `export const hasFeature = name => features.includes(name)`,\n      ].join('\\n'),\n    })\n  },\n})\n","Two details separate a correct answer from a passable one. First, every write goes through `defu` with the existing value **first**, so a consumer who already set `runtimeConfig.public.toolkit.apiBase` in their own `nuxt.config` keeps winning; assigning directly silently discards deliberate configuration. Second, `features` goes into a template rather than public runtime config: anything in `runtimeConfig.public` is serialised into every SSR response, while a generated module is ordinary JavaScript the bundler can tree-shake and the browser can cache. Mention that the token must never appear in `runtimeConfig.public`, and that runtime code reads these with `useRuntimeConfig()` \u002F `useAppConfig()` inside setup, never hoisted to module scope.\n",{"id":1005,"type":324,"difficulty":476,"subsection":978,"tags":1006,"prompt":1008,"answer":1009,"section":890,"sectionTitle":82},"module-authoring-015",[835,1007],"payload","After a team installs your toolkit, every page's HTML grows by about 200 KB. Where do you look first, and how do you fix it?\n","Public runtime config. Everything under `runtimeConfig.public` is serialised into the `#__NUXT_DATA__` script of **every** SSR response, so a generated route map, an icon manifest or a translation table put there is paid for on every page load, by every user, uncompressed in your own accounting. I would `curl` a page, find the payload script and look for my own `configKey` before suspecting anything else; `useState` keys and `useAsyncData` results are the other two payload contributors worth ruling out.\n\nThe fix is to move the data out of the payload and into generated code: `addTemplate` with the same contents, imported as `#build\u002Ftoolkit\u002F…`. Now it is a JavaScript module — the bundler tree-shakes the parts nobody imports, the browser caches it with the rest of the chunks, and the HTML goes back to its old size. What stays in public runtime config is the handful of scalars that genuinely have to change per environment.\n\nI would then add a bundle\u002Fpayload budget assertion to the module's test suite so the regression cannot come back quietly.\n",{"id":1011,"type":324,"difficulty":325,"subsection":978,"tags":1012,"prompt":1013,"answer":1014,"section":890,"sectionTitle":82},"module-authoring-016",[415,523],"Your module needs an API token for a server-side call. Which channel do you put it in, and which do you rule out?\n","Private `runtimeConfig.\u003CconfigKey>` — `nuxt.options.runtimeConfig.toolkit = { apiToken: … }` — read on the server with `useRuntimeConfig().toolkit.apiToken`. It is never serialised into the client payload, and the deploy can override it with `NUXT_TOOLKIT_API_TOKEN` without rebuilding, which is what lets the same artifact go to staging and production.\n\nRuled out: `runtimeConfig.public`, which ships the token in the HTML of every response; `appConfig`, which is bundled into the client chunk *and* frozen at build time, so it is both a leak and unrotatable; and a generated template, which is bundled into whichever side imports it and is equally visible if that side is the browser.\n\nTwo extra points worth making: the default should be an empty string rather than a real value, so nobody ships a credential in the package, and the module should warn at build time if the token is missing rather than fail on the first request.\n",{"id":1016,"type":324,"difficulty":347,"subsection":1017,"tags":1018,"prompt":1019,"answer":1020,"section":890,"sectionTitle":82},"module-authoring-017","runtime-directory",[953,387],"Why can't code in `src\u002Fruntime` import `@nuxt\u002Fkit`?\n","Because kit is build-time code. Its helpers reach the current Nuxt instance through an unctx async context that only exists inside the build process, and it depends on Node APIs and the build toolchain. Bundled into the client you get an unresolvable import or an enormous chunk; on the server you get \"Nuxt instance is unavailable\", because there is no build context at request time.\n\nWhat runtime code may import is `#imports`, `#app`, `vue`, `vue-router`, `h3`, `#build\u002F\u003Ctemplate>` and its own sibling files. Anything it needs from the build world has to be handed over deliberately through runtime config, `appConfig` or a generated template.\n\nThe mechanical reason the boundary is real: `@nuxt\u002Fmodule-builder` **bundles** `src\u002Fmodule.ts` into `dist\u002Fmodule.mjs`, inlining kit, but only **transpiles** `src\u002Fruntime` file by file. Runtime imports therefore stay live in the published package — which is also why a library used from `src\u002Fruntime` must be a real `dependency`, not a `devDependency`.\n",{"id":1022,"type":338,"difficulty":347,"subsection":1017,"tags":1023,"prompt":1025,"options":1026,"correct":1031,"answer":1032,"section":890,"sectionTitle":82},"module-authoring-018",[639,1024],"build","A server util in `src\u002Fruntime\u002Fserver\u002Futils` imports from `#imports`. It works in `nuxt dev`, and in `nuxt build` the alias fails to resolve. What is the fix?\n",[1027,1028,1029,1030],"Push the runtime directory onto `nuxt.options.build.transpile`","Add the runtime directory to `nitro.externals.inline` from the `nitro:config` hook, so Nitro treats it as its own source instead of an external dependency","Import from `nitropack\u002Fruntime` instead, because `#imports` only exists in the app bundle","Register the util with `addImportsDir` instead of `addServerImportsDir`",[325],"In dev, Nitro resolves your untranspiled source through Vite, so the alias is rewritten and everything looks fine. In a real build your package is an external node module, its aliases are not rewritten, and `#imports` never resolves. Adding `resolver.resolve('.\u002Fruntime')` to `nitroConfig.externals.inline` from `nitro:config` makes Nitro compile the directory as part of its own bundle. `build.transpile` is the historical webpack-era workaround for the *app* build and does nothing for Nitro; `#imports` does exist in the server context (it is Nitro's own unimport registry, separate from the app's); and `addImportsDir` targets the app, not the server.\n",{"id":1034,"type":351,"difficulty":347,"subsection":1017,"language":353,"tags":1035,"prompt":1037,"starter":1038,"solution":1039,"answer":1040,"section":890,"sectionTitle":82},"module-authoring-019",[953,1036],"runtime","This runtime plugin breaks the build. Rewrite it so it follows the rules of the runtime directory, keeping the same behaviour: read `apiBase` from public runtime config, log it in the browser only, and provide it to the app.\n","import { useNuxt } from '@nuxt\u002Fkit'\n\nexport default defineNuxtPlugin(() => {\n  const nuxt = useNuxt()\n  const apiBase = nuxt.options.runtimeConfig.public.toolkit.apiBase\n\n  if (process.client) {\n    console.log('[toolkit]', apiBase)\n  }\n\n  return { provide: { toolkitApiBase: apiBase } }\n})\n","import { defineNuxtPlugin, useRuntimeConfig } from '#imports'\n\nexport default defineNuxtPlugin(() => {\n  \u002F\u002F per request on the server: read inside the plugin body, never at module scope\n  const { apiBase } = useRuntimeConfig().public.toolkit\n\n  if (import.meta.client) {\n    console.log('[toolkit]', apiBase)\n  }\n\n  return { provide: { toolkitApiBase: apiBase } }\n})\n","Three separate mistakes. `@nuxt\u002Fkit` and `nuxt.options` are build-world only — kit resolves the Nuxt instance from an async context that does not exist at runtime, so this either fails to bundle or throws per request; the value has to come from `useRuntimeConfig()`. `process.client` is Nuxt 2 vocabulary: the current form is `import.meta.client`, which is a compile-time constant, so the dead branch is stripped from the server bundle entirely. And `defineNuxtPlugin`\u002F`useRuntimeConfig` are imported explicitly from `#imports` rather than relying on auto-imports, because a consumer can set `imports: { autoImport: false }` and the module must still work. Worth adding out loud: `useRuntimeConfig()` must stay inside the plugin body, since on the server it is per-request state.\n",{"id":1042,"type":324,"difficulty":476,"subsection":1017,"tags":1043,"prompt":1046,"answer":1047,"section":890,"sectionTitle":82},"module-authoring-020",[1044,1045],"packaging","dependencies","Your unit tests, playground and E2E suite are all green, but the first consumer who installs the published package gets \"Cannot find package 'p-limit'\". What happened?\n","Something in `src\u002Fruntime` imports `p-limit`, and `p-limit` is a `devDependency`.\n\n`@nuxt\u002Fmodule-builder` bundles `src\u002Fmodule.ts`, so anything the build-world half imports is inlined into `dist\u002Fmodule.mjs` and a `devDependency` works there. `src\u002Fruntime` is only **transpiled**, file by file, so its import statements survive into the published output and become real imports in the consumer's app. Your own `node_modules` has the package, so the playground and every test that runs from source resolve it; the consumer's does not.\n\nThe fix is to move it to `dependencies`. The process fix is the important part of the answer: add a release step that runs `npm pack`, installs the tarball into a clean `nuxi init` app and boots it. That is the only check that exercises the real resolution graph, and it catches the whole family of problems — missing `files` entries, a broken `exports` map, and subpath imports into `dist\u002Fruntime` that were never declared.\n",{"id":1049,"type":324,"difficulty":325,"subsection":1050,"tags":1051,"prompt":1052,"answer":1053,"section":890,"sectionTitle":82},"module-authoring-021","hooks",[1050],"Name the three hook families in Nuxt and say where each one runs.\n","All three are [unjs\u002Fhookable](https:\u002F\u002Fgithub.com\u002Funjs\u002Fhookable) instances, and the difference is which process they live in.\n\n**Build hooks** on `nuxt` — `nuxt.hook('pages:extend', …)`. Node, once, during the build. Available to modules and to the `hooks` key of `nuxt.config`. This is where `nitro:config`, `modules:done`, `builder:watch`, `prepare:types` and `close` live.\n\n**App runtime hooks** on `nuxtApp` — `nuxtApp.hook('page:finish', …)` from a Nuxt plugin. Runs during SSR and in the browser: `app:created`, `vue:setup`, `app:rendered`, `app:beforeMount`, `app:mounted`, `page:start`, `link:prefetch`, `app:error`, `vue:error`.\n\n**Nitro runtime hooks** on `nitroApp.hooks` — registered from a Nitro plugin. Server only, per request: `request`, `beforeResponse`, `afterResponse`, `render:html`, `render:response`, `error`, `close`.\n\nListeners run serially and are awaited by default; `callHookParallel` exists when order does not matter, `hookOnce` for one-shot listeners, and `hook()` returns an unsubscribe function.\n",{"id":1055,"type":324,"difficulty":347,"subsection":1050,"tags":1056,"prompt":1057,"answer":1058,"docs":1059,"section":890,"sectionTitle":82},"module-authoring-022",[639],"What is the difference between `nitro:config` and `nitro:init`, and which do you use for what?\n","`nitro:config` fires **before** Nitro is created and receives the Nitro *config object*. It is the only place to change anything Nitro reads at construction time: `externals` (including `inline`, which is how you make your own `runtime\u002Fserver` directory resolvable), `virtual` modules, storage mounts, route rules, prerender options, `plugins`.\n\n`nitro:init` fires **after** Nitro exists and receives the *instance*. That is where you register Nitro runtime hooks from the build world — `nitro.hooks.hook('render:html', …)` — or inspect what Nitro actually resolved.\n\nGetting them the wrong way round fails quietly rather than loudly: setting `nitroConfig.externals` in `nitro:init` mutates an object nobody reads again, and reaching for `nitro.hooks` in `nitro:config` is a type error people work around with `any` and then wonder why nothing fires.\n\nFor per-request behaviour I would usually skip both and ship a Nitro plugin with `addNitroPlugin`, because that puts the hook registration in runtime code where it belongs and survives Nitro version changes better.\n",[1060],{"title":1061,"url":1062},"Nuxt hooks reference","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fadvanced\u002Fhooks",{"id":1064,"type":351,"difficulty":347,"subsection":1050,"language":353,"tags":1065,"prompt":1066,"starter":1067,"solution":1068,"answer":1069,"section":890,"sectionTitle":82},"module-authoring-023",[999,370],"Your module generates `#build\u002Ftoolkit\u002Froutes.mjs` from the files in the consumer's `toolkit-routes\u002F` directory. Make it regenerate during `nuxt dev` when a file in that directory changes, without regenerating every other template in the app.\n","import { addTemplate, defineNuxtModule, resolveFiles } from '@nuxt\u002Fkit'\nimport { join } from 'pathe'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  async setup (_options, nuxt) {\n    const dir = join(nuxt.options.rootDir, 'toolkit-routes')\n    const files = await resolveFiles(dir, '**\u002F*.ts')\n\n    addTemplate({\n      filename: 'toolkit\u002Froutes.mjs',\n      getContents: () => `export const routes = ${JSON.stringify(files)}`,\n    })\n    \u002F\u002F TODO: keep it up to date in dev\n  },\n})\n","import { addTemplate, defineNuxtModule, resolveFiles, updateTemplates } from '@nuxt\u002Fkit'\nimport { join } from 'pathe'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  async setup (_options, nuxt) {\n    const dir = join(nuxt.options.rootDir, 'toolkit-routes')\n    let files = await resolveFiles(dir, '**\u002F*.ts')\n\n    addTemplate({\n      filename: 'toolkit\u002Froutes.mjs',\n      getContents: () => `export const routes = ${JSON.stringify(files)}`,\n    })\n\n    nuxt.hook('builder:watch', async (_event, path) => {\n      if (!path.includes('toolkit-routes\u002F')) { return }\n\n      files = await resolveFiles(dir, '**\u002F*.ts')\n      await updateTemplates({ filter: t => t.filename.startsWith('toolkit\u002F') })\n    })\n  },\n})\n","Two things have to change. The scan result must live in a `let` the template closes over, so re-scanning actually changes what `getContents` produces — a `const` captured once regenerates the same string forever. And `updateTemplates` must be filtered: called bare it regenerates every template in the app, which in a large project turns one save into a visible dev-server stall. Checking the watched `path` first means an unrelated save does no work at all. The subtler point worth saying: `getContents` runs on every `app:templates`, so the expensive directory scan belongs in `setup` and in the watcher, never inside `getContents`.\n",{"id":1071,"type":338,"difficulty":476,"subsection":1050,"tags":1072,"prompt":1073,"options":1074,"correct":1079,"answer":1080,"docs":1081,"section":890,"sectionTitle":82},"module-authoring-024",[966,1050],"Under `future.compatibilityVersion: 5`, which statement about `callHook` is correct?\n",[1075,1076,1077,1078],"It may return `void` instead of a promise, so chaining `.then()` on the result can throw — `await` it instead","It now runs listeners in parallel by default, and `callHookSerial` restores the old behaviour","It was removed in favour of `emit`, because Nuxt 5 drops hookable","It throws at runtime unless the hook name is declared in the `NuxtHooks` interface",[378],"Nuxt 5 relaxes the contract so `callHook` can complete synchronously and return `void` rather than always allocating a promise. Code written as `nuxt.callHook('x').then(…)` therefore breaks on `undefined`, while `await nuxt.callHook('x')` is correct in both versions — awaiting a non-promise is simply the value. Hookable is still the implementation, listeners are still serial by default with `callHookParallel` as the opt-in, and declaring a hook in `NuxtHooks` is a typing convenience, not a runtime requirement.\n",[1082],{"title":1083,"url":1084},"unjs\u002Fhookable","https:\u002F\u002Fgithub.com\u002Funjs\u002Fhookable",{"id":1086,"type":324,"difficulty":476,"subsection":1050,"tags":1087,"prompt":1088,"answer":1089,"section":890,"sectionTitle":82},"module-authoring-025",[513,335],"How do you let another module — or a consuming team layer — extend your module without them importing your internals?\n","By declaring my own hook. I augment `NuxtHooks` from `@nuxt\u002Fschema` with the signature, then call it from my `setup` at the point where the contribution is still useful — usually `modules:done`, so every module has had a chance to register — passing a mutable registry object. `@nuxtjs\u002Fi18n` does this with `i18n:registerModule` and Tailwind with `tailwindcss:config`.\n\nThree payoffs. Other modules do not need to import my files, so my internal layout stays mine to change. A consuming **layer** can contribute through the `hooks` key of its `nuxt.config` without writing a module at all, which matters a lot for a layers-first team. And the hook name becomes the documented contract, so I can version it.\n\nThe same pattern has a runtime counterpart: augment `RuntimeNuxtHooks` in `#app` and call `nuxtApp.callHook('toolkit:ready', …)` from my plugin, or augment `NitroRuntimeHooks` for the server. I would also `await` every `callHook` rather than chaining `.then`, because Nuxt 5 lets it return `void`.\n",{"id":1091,"type":324,"difficulty":325,"subsection":1092,"tags":1093,"prompt":1094,"answer":1095,"docs":1096,"section":890,"sectionTitle":82},"module-authoring-026","templates-and-types",[999],"How are module templates generated and served, and how does runtime code import one?\n","`addTemplate({ filename, getContents })` registers a file in `buildDir` (`.nuxt`). `getContents` receives `{ nuxt, app, options }` and returns a string; it is called whenever templates are generated, which is on `app:templates`, so on every rebuild.\n\nTemplates are **virtual by default** — held in memory and handed to the bundler, never written to disk — which keeps `.nuxt` small and rebuilds fast. `write: true` puts a real file there for tools that need one, such as a linter or an external script.\n\nRuntime code imports a template by its filename under the `#build` alias: `filename: 'toolkit\u002Froutes.mjs'` becomes `import { routes } from '#build\u002Ftoolkit\u002Froutes.mjs'`. Because the result is ordinary JavaScript, the bundler analyses and tree-shakes it like any other module, which is why templates are the right channel for anything large or derived.\n\nThe full field list is `src`, `filename`, `dst`, `options`, `getContents`, `write` and `dependsOn`.\n",[1097],{"title":1098,"url":1099},"Kit — templates","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fkit\u002Ftemplates",{"id":1101,"type":338,"difficulty":347,"subsection":1092,"tags":1102,"prompt":1103,"options":1104,"correct":1109,"answer":1110,"section":890,"sectionTitle":82},"module-authoring-027",[816],"What does the second argument of `addTypeTemplate(template, context)` accept?\n",[1105,1106,1107,1108],"`{ nuxt?: boolean, nitro?: boolean }` — the contexts Nuxt generates separate tsconfigs for","`{ nuxt?: boolean, nitro?: boolean, node?: boolean, shared?: boolean }`","`{ write?: boolean, dst?: string }`, the same extra options `addTemplate` takes","An array of tsconfig paths the generated declaration should be referenced from",[378],"`addTypeTemplate` registers the template *and* adds a reference to it from the generated declaration entry points. Its context argument is `{ nuxt?, nitro? }` and nothing else, matching the tsconfigs Nuxt writes per context; older guides and blog posts list `node` and `shared`, which is the tempting distractor here. Getting it wrong is quiet rather than loud: the declaration is generated but referenced from a context where nobody looks, so completion silently does not appear. Pass `{ nitro: true }` for declarations that describe server code, and check the result by opening `.nuxt\u002Fnuxt.d.ts` and `.nuxt\u002Ftypes\u002F` after `nuxi prepare`.\n",{"id":1112,"type":351,"difficulty":476,"subsection":1092,"language":353,"tags":1113,"prompt":1114,"starter":1115,"solution":1116,"answer":1117,"section":890,"sectionTitle":82},"module-authoring-028",[999,816],"Generate a `#build\u002Ftoolkit\u002Ficons.mjs` module that exports one named constant per SVG file found in the consumer's `toolkit-icons\u002F` directory, with PascalCase names and correctly escaped paths, and declare its type so consumers get completion.\n","import { addTemplate, addTypeTemplate, defineNuxtModule, resolveFiles } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  async setup (_options, nuxt) {\n    \u002F\u002F TODO\n  },\n})\n","import { addTemplate, addTypeTemplate, defineNuxtModule, resolveFiles } from '@nuxt\u002Fkit'\nimport { genObjectFromRawEntries, genString } from 'knitwork'\nimport { pascalCase } from 'scule'\nimport { basename, extname, join } from 'pathe'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  async setup (_options, nuxt) {\n    const files = await resolveFiles(join(nuxt.options.rootDir, 'toolkit-icons'), '**\u002F*.svg')\n    const names = files.map(f => pascalCase(basename(f, extname(f))))\n\n    addTemplate({\n      filename: 'toolkit\u002Ficons.mjs',\n      getContents: () => {\n        const entries = files.map((f, i) => [names[i]!, genString(f)] as [string, string])\n        return `export const icons = ${genObjectFromRawEntries(entries)}`\n      },\n    })\n\n    addTypeTemplate({\n      filename: 'types\u002Ftoolkit-icons.d.ts',\n      getContents: () => [\n        `declare module '#build\u002Ftoolkit\u002Ficons.mjs' {`,\n        `  export const icons: Record\u003C${names.map(n => `'${n}'`).join(' | ') || 'string'}, string>`,\n        `}`,\n      ].join('\\n'),\n    }, { nuxt: true })\n  },\n})\n","The reason to reach for knitwork rather than template literals is escaping: a Windows path interpolated into `'...'` produces invalid escape sequences (`C:\\Users` → `\\U`), and a filename containing an apostrophe breaks the string outright. `genString` and `genObjectFromRawEntries` handle both, and `pascalCase` from scule names identifiers the same way Nuxt does. The type template needs its context argument — `{ nuxt: true }` or `{ nitro: true }`, those two keys only — because Nuxt writes separate tsconfigs per context and a declaration referenced from the wrong one is invisible. Also worth saying: the directory scan lives in `setup`, not inside `getContents`, which runs on every rebuild.\n",{"id":1119,"type":324,"difficulty":476,"subsection":1092,"tags":1120,"prompt":1121,"answer":1122,"section":890,"sectionTitle":82},"module-authoring-029",[639,999],"A server handler in your module imports `#build\u002Ftoolkit\u002Froutes.mjs`. It works in `nuxt dev` and breaks in the built output. Explain why, and how you would ship the same data to both sides.\n","`#build` is an alias of the **app** build, pointing into `.nuxt`. Nitro has its own build with its own virtual modules and does not know that alias, so the import only survives dev, where Vite is in the loop and the module is resolvable from disk.\n\nThe server equivalent is `addServerTemplate({ filename, getContents })`, which writes into `nitro.virtual` and is imported by the virtual id you chose — conventionally something like `#toolkit\u002Froutes`. So the fix is to register both templates and keep the generation in one shared function, so the app version and the Nitro version cannot drift.\n\nThe same split explains `addTypeTemplate`'s context argument: Nuxt 4 writes separate tsconfigs for the Nuxt and Nitro contexts, so a declaration referenced from the wrong one simply never appears.\n\nGenerally: if the data is large, generate it on both sides rather than routing it through `runtimeConfig.public`, which would put it in every SSR response just to make it reachable from the server.\n",{"id":1124,"type":324,"difficulty":347,"subsection":1125,"tags":1126,"prompt":1128,"answer":1129,"docs":1130,"section":890,"sectionTitle":82},"module-authoring-030","dependencies-and-composition",[1127],"composition","`installModule` or `moduleDependencies` — which do you reach for, and what is the difference?\n","`moduleDependencies` for anything real. It is a declarative field on the module definition, alongside `meta`, `defaults`, `schema`, `hooks`, `setup`, `onInstall` and `onUpgrade`, and Nuxt resolves it **before** my `setup` runs: it enforces a semver `version` constraint, orders the dependency ahead of me, and merges options so `defaults` sit under the consumer's config and `overrides` sit over it. `optional: true` means \"configure it if present, never install it\". Nuxt UI uses exactly this to pull in `@nuxt\u002Ficon`, `@nuxt\u002Ffonts` and `@nuxtjs\u002Fcolor-mode`.\n\n`installModule(name, options, nuxt)` is the old imperative form: it runs another module's `setup` inline, immediately, with no version constraint, no ordering logic and no distinction between \"my preference\" and \"my requirement\". It is now marked deprecated in the kit docs.\n\nFor detection rather than installation I use `hasNuxtModule`, which also sees modules listed after mine, with `hasNuxtModuleCompatibility` when I need a version check.\n",[1131],{"title":1132,"url":1133},"Kit — modules","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fkit\u002Fmodules",{"id":1135,"type":338,"difficulty":347,"subsection":1125,"tags":1136,"prompt":1137,"options":1138,"correct":1143,"answer":1144,"section":890,"sectionTitle":82},"module-authoring-031",[1127,461],"Your module declares `moduleDependencies: { '@nuxt\u002Ficon': { defaults: { size: '1em' }, overrides: { mode: 'svg' } } }` and the consumer writes `icon: { size: '2em', mode: 'css' }` in `nuxt.config`. What options does Nuxt Icon receive?\n",[1139,1140,1141,1142],"`{ size: '2em', mode: 'svg' }` — `defaults` merge under the user's config, `overrides` merge over it","`{ size: '1em', mode: 'svg' }` — a declaring module's options always take precedence","`{ size: '2em', mode: 'css' }` — the consumer's config always wins","`{ size: '1em', mode: 'css' }` — `defaults` wins, and `overrides` only applies when the key is absent",[378],"That asymmetry is the whole point of having two fields. `defaults` express a preference the consumer is free to change, so `size: '2em'` survives. `overrides` express a requirement of your integration, so `mode: 'svg'` replaces the consumer's `'css'`. The design consequence to say out loud: every entry in `overrides` is a support commitment and a future bug report (\"I set this and it is ignored\"), so keep that list minimal and document it. Where a wrong value would merely degrade rather than break, prefer a `default` plus a build-time warning.\n",{"id":1146,"type":351,"difficulty":347,"subsection":1125,"language":353,"tags":1147,"prompt":1148,"starter":1149,"solution":1150,"answer":1151,"section":890,"sectionTitle":82},"module-authoring-032",[1127],"Declare that `nuxt-team-toolkit` requires `@nuxt\u002Ficon` v1 or later with `mode: 'svg'` enforced and `size: '1em'` merely preferred, optionally integrates with `@nuxtjs\u002Fcolor-mode` v3+ without installing it, and registers translations with `@nuxtjs\u002Fi18n` only when that module is present.\n","import { defineNuxtModule } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  setup () {\n    \u002F\u002F TODO\n  },\n})\n","import { createResolver, defineNuxtModule, hasNuxtModule } from '@nuxt\u002Fkit'\n\nconst { resolve } = createResolver(import.meta.url)\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit', version: '1.4.0' },\n  moduleDependencies: {\n    '@nuxt\u002Ficon': {\n      version: '>=1',\n      defaults: { size: '1em' },   \u002F\u002F consumer config wins over this\n      overrides: { mode: 'svg' },  \u002F\u002F this wins over consumer config\n      optional: false,\n    },\n    '@nuxtjs\u002Fcolor-mode': {\n      version: '>=3',\n      defaults: { classSuffix: '' },\n      optional: true,              \u002F\u002F configured if present, never installed\n    },\n  },\n  setup (_options, nuxt) {\n    \u002F\u002F hasNuxtModule also sees modules listed *after* this one in the merged config\n    if (hasNuxtModule('@nuxtjs\u002Fi18n')) {\n      nuxt.hook('i18n:registerModule', register => register({\n        langDir: resolve('.\u002Fruntime\u002Flang'),\n        locales: [{ code: 'en', file: 'en.json' }],\n      }))\n    }\n  },\n})\n","The three mechanisms map onto three different relationships. A hard dependency goes in `moduleDependencies` with a `version` constraint so an incompatible major fails with a clear message instead of deep inside someone else's code; `overrides` is reserved for the one option the integration genuinely requires. An enhancement is the same field with `optional: true`, so a consumer who does not want colour mode is not given it. And a purely conditional integration uses `hasNuxtModule` plus that module's own hook — never an import of its internals, which would couple me to its file layout. Mention that `hasNuxtModule` checks the `modules` array too, so it does not depend on where the consumer put me.\n",{"id":1153,"type":324,"difficulty":476,"subsection":1125,"tags":1154,"prompt":1155,"answer":1156,"section":890,"sectionTitle":82},"module-authoring-033",[1044,1045],"How do you decide whether something belongs in `dependencies`, `devDependencies` or `peerDependencies` for a Nuxt module?\n","By asking which half of the module uses it and who should own the version.\n\n`@nuxt\u002Fkit` is a normal `dependency`, with a range matching the Nuxt major I support. The consumer should not have to install it, and it is only used at build time, where module-builder inlines it into `dist\u002Fmodule.mjs` anyway.\n\n`nuxt` and `vue` are `devDependencies` **plus** `peerDependencies`. I need them to build and test; the app owns the installed copy. Listing `vue` as a plain dependency lets a package manager install a second copy under my module, and two Vue instances mean broken `provide`\u002F`inject`, broken reactivity across the boundary and a duplicated runtime in the bundle — a failure that only ever appears in the consumer's app.\n\nAnything imported from `src\u002Fruntime` is a `dependency`, because that directory is transpiled rather than bundled, so its imports stay live in the published package. I move such a library to a documented `peerDependency` only when the host app genuinely should control the version — a charting or i18n library the app also uses directly — and then I say so in the README.\n",{"id":1158,"type":324,"difficulty":347,"subsection":730,"tags":1159,"prompt":1160,"answer":1161,"docs":1162,"section":890,"sectionTitle":82},"module-authoring-034",[730],"How do you test a Nuxt module?\n","As a pyramid, because most module bugs are build bugs that component tests cannot see.\n\nRuntime unit tests with `@nuxt\u002Ftest-utils`: `defineVitestConfig` with `environment: 'nuxt'`, then `mountSuspended` for components that use the Nuxt app context, `mockNuxtImport` for auto-imported composables and `registerEndpoint` to stub server routes.\n\nE2E against fixtures: `await setup({ rootDir: fileURLToPath(new URL('.\u002Ffixtures\u002Fbasic', import.meta.url)) })`, then `$fetch('\u002F')` for raw-HTML assertions — that is what proves something is server-rendered — and `createPage` for interaction. One fixture per interesting option scenario.\n\nThen the level most modules skip: build-level assertions. Read a generated template out of the fixture's `.nuxt`, snapshot the emitted `.d.ts`, and assert that a chunk is or is not in the client bundle.\n\nThen type tests with `vitest --typecheck` and `expectTypeOf`, plus `nuxt typecheck` on the playground, which is the only check that compiles my declarations the way a consuming app will.\n",[1163],{"title":1164,"url":738},"Testing",{"id":1166,"type":338,"difficulty":325,"subsection":730,"tags":1167,"prompt":1169,"options":1170,"correct":1175,"answer":1176,"docs":1177,"section":890,"sectionTitle":82},"module-authoring-035",[730,1168],"vitest","A test using `mockNuxtImport('useRuntimeConfig', () => configMock)` fails with \"Cannot access 'configMock' before initialization\". Why?\n",[1171,1172,1173,1174],"`mockNuxtImport` compiles to `vi.mock`, which Vitest hoists above the imports — declare the mock with `vi.hoisted()` and reference that","Auto-imported composables can only be mocked from a `setupFiles` entry, never inside a test file","`environment: 'nuxt'` is missing, so the auto-import registry was never loaded","`useRuntimeConfig` is a Nuxt built-in and must be stubbed with `mockComponent` instead",[378],"`mockNuxtImport` is sugar over `vi.mock`, and Vitest hoists `vi.mock` calls above every import and every top-level statement in the file. A `const` declared in the file body therefore does not exist yet when the factory runs. The fix is `const { configMock } = vi.hoisted(() => ({ configMock: vi.fn(...) }))`, which is hoisted alongside the mock. The error message points at the variable rather than at the hoisting, which is why this costs everyone an afternoon exactly once — and why it is a fair interview question for anyone who has actually written module tests.\n",[1178],{"title":1179,"url":1180},"nuxt\u002Ftest-utils","https:\u002F\u002Fgithub.com\u002Fnuxt\u002Ftest-utils",{"id":1182,"type":351,"difficulty":476,"subsection":730,"language":353,"tags":1183,"prompt":1185,"starter":1186,"solution":1187,"answer":1188,"section":890,"sectionTitle":82},"module-authoring-036",[730,1184],"e2e","Write an E2E test for `nuxt-team-toolkit` against a fixture. It must prove the greeting is server-rendered (not just present after hydration), check the module's health endpoint, and assert that the private API token never reaches the client.\n","import { describe, expect, it } from 'vitest'\nimport { setup } from '@nuxt\u002Ftest-utils\u002Fe2e'\n\ndescribe('nuxt-team-toolkit', async () => {\n  \u002F\u002F TODO\n})\n","import { fileURLToPath } from 'node:url'\nimport { describe, expect, it } from 'vitest'\nimport { $fetch, createPage, setup } from '@nuxt\u002Ftest-utils\u002Fe2e'\n\ndescribe('nuxt-team-toolkit', async () => {\n  await setup({\n    rootDir: fileURLToPath(new URL('.\u002Ffixtures\u002Fbasic', import.meta.url)),\n  })\n\n  it('server-renders the greeting', async () => {\n    \u002F\u002F raw HTML, not the DOM: this is what a crawler and the LCP measurement see\n    const html = await $fetch('\u002F')\n    expect(html).toContain('\u003Cspan data-toolkit>hello\u003C\u002Fspan>')\n  })\n\n  it('registers the health endpoint', async () => {\n    expect(await $fetch('\u002Fapi\u002F_toolkit\u002Fhealth')).toMatchObject({ ok: true })\n  })\n\n  it('keeps the private token out of the client', async () => {\n    const html = await $fetch('\u002F')\n    expect(html).not.toContain('super-secret-token')\n\n    const page = await createPage('\u002F')\n    expect(await page.content()).not.toContain('super-secret-token')\n  })\n})\n","The load-bearing choice is `$fetch` versus `createPage`. `createPage` returns the page **after** hydration, so a component that renders only in the browser passes a DOM assertion while emitting empty server HTML — asserting the string from `$fetch` is the only thing that proves server rendering. `setup()` must be awaited at the top of the `describe` (hence `async`), because it builds and boots a real Nuxt app from the fixture. For the token, check both surfaces: the SSR payload (a leak through `runtimeConfig.public`) and the hydrated page (a leak through a bundled template or `appConfig`). Worth adding: one fixture per option scenario beats one fixture with everything switched on.\n",{"id":1190,"type":324,"difficulty":476,"subsection":730,"tags":1191,"prompt":1192,"answer":1193,"section":890,"sectionTitle":82},"module-authoring-037",[414,966],"What does a CI matrix for a module look like, and what does it catch that your unit tests cannot?\n","Two axes at minimum: the latest Nuxt 4.x, and a nightly job that swaps in `nuxt@npm:nuxt-nightly@5x` — the alias the core modules use — allowed to fail. On both, I run `dev:prepare`, the unit and E2E suites, `vitest --typecheck` and `nuxt typecheck` on the playground.\n\nWhat it catches is the class of failure my own tests structurally cannot see, because they pin one version of the framework: a kit helper that changed signature, a hook that no longer fires, a deprecated option that finally went away, a generated declaration that stops compiling against a newer `nuxt\u002Fschema`. The nightly job turns a consumer's upgrade PR into my CI failure weeks earlier, which for an internal toolkit is the difference between a planned migration and an outage.\n\nI would add a release-time job that runs `npm pack`, installs the tarball into a clean `nuxi init` app and boots it, because every test above runs against `src` and therefore cannot see packaging mistakes — a `devDependency` imported from `src\u002Fruntime`, a missing `files` entry, a broken `exports` map.\n",{"id":1195,"type":324,"difficulty":325,"subsection":1196,"tags":1197,"prompt":1198,"answer":1199,"docs":1200,"section":890,"sectionTitle":82},"module-authoring-038","building-and-publishing",[1044],"What does `nuxt-module-build build` produce, and how do you check what actually ships to consumers?\n","It emits four things into `dist`: `module.mjs`, which is `src\u002Fmodule.ts` **bundled** with kit and any build-time dependency inlined; `module.json`, carrying the name and version that Nuxt reads for module meta and the upgrade lifecycle; `types.d.mts`, the `nuxt\u002Fschema` augmentation generated from the interfaces you export (`ModuleOptions`, `ModuleHooks`, `ModuleRuntimeConfig`, `ModulePublicRuntimeConfig`); and `runtime\u002F`, your runtime directory **transpiled file by file** at the same relative paths, which is why `resolve('.\u002Fruntime\u002F…')` still works after publishing.\n\nTo see what ships, run `npm pack` (or `--dry-run`): it prints the exact file list. Anything you expected and cannot see is a `files` mistake; anything present that you did not expect is bloat in every consumer's install. The stronger check is to install the tarball into a fresh `nuxi init` app and boot it, which is the only way to exercise the real resolution graph.\n",[1201],{"title":1202,"url":1203},"nuxt\u002Fmodule-builder","https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fmodule-builder",{"id":1205,"type":338,"difficulty":325,"subsection":1196,"tags":1206,"prompt":1208,"options":1209,"correct":1214,"answer":1215,"section":890,"sectionTitle":82},"module-authoring-039",[1207],"semver","Which of these is a breaking change for a Nuxt module?\n",[1210,1211,1212,1213],"Adding a new option with a backwards-compatible default","Changing the default value of an existing option","Adding a component to the directory registered with `addComponentsDir`","Generating an additional template that nothing imports yet",[325],"A breaking change is anything that forces consumers to edit code or config: an option removed or renamed, the minimum Nuxt version raised, a runtime API removed — and a changed default, which is the nastiest of the set because nothing errors. Types still compile, the build still passes, and the behaviour quietly differs in every consuming app until somebody notices in review three weeks later. New options and new components are additive minors; an unreferenced template is a patch. The right way to change a default is to ship the new value behind the option in a minor, warn that the default flips in the next major, and flip it there.\n",{"id":1217,"type":324,"difficulty":476,"subsection":1196,"tags":1218,"prompt":1219,"answer":1220,"section":890,"sectionTitle":82},"module-authoring-040",[513,335,1207],"You need to rename an option in a toolkit that a dozen internal apps consume through your team layer. Walk me through the release.\n","It is a breaking change, so it lands in a major — but not abruptly. In the current major I add the new option, keep the old one accepting values, map old to new at the top of `setup`, and warn once at build time with `useLogger`: a message naming the old option, the new option and the version in which the old one disappears. I mark the old field `@deprecated` in `ModuleOptions` so editors flag it before the build does, and I write the migration in the CHANGELOG with a copy-pasteable diff, generated from conventional commits with changelogen.\n\nBecause the toolkit is a layer plus a module, I version them together: the layer's `package.json` depends on the module with a range it actually supports, one changelog covers both, and the README compatibility table lists Nuxt version, layer version and module version in one row. Consumers upgrade one `extends` entry, so the coherence has to be mine.\n\nThen I search the internal repos for the old option, open the PRs myself where it is a one-line change, and only remove it in the next major once those have merged. `meta.compatibility` and the `peerDependencies` range go up at the same time, so an app that upgrades the toolkit without upgrading Nuxt gets a build-time warning rather than a runtime mystery.\n",{"id":1222,"type":324,"difficulty":325,"subsection":326,"tags":1223,"prompt":1224,"answer":1225,"section":1226,"sectionTitle":134},"under-the-hood-001",[328,370],"Which parts of a Nuxt application run at build time and which run at runtime, and why does that boundary matter when you ship tooling to other teams?\n","Build time is `nuxt.config`, every module's `setup`, kit helpers, templates and bundler plugins. All of it runs once in Node, during `nuxt dev`, `nuxt build` or `nuxt generate`, and none of it exists afterwards. Runtime is what ends up in the client bundle and in `.output\u002Fserver`: components, composables, plugins, server handlers — for a module, everything under `runtime\u002F`.\n\nThe boundary matters because a module's build-world values (options, `nuxt.options`, absolute paths, the file system, `pkg-types`, `jiti`, `c12`) simply do not exist at runtime. If the app needs one of them it has to be carried across deliberately, through `runtimeConfig`, `appConfig` or a generated template. Most \"works in my playground, breaks for the consumer\" bugs are a build-world value assumed at runtime, or a build-only dependency accidentally imported from `runtime\u002F` and dragged into the client bundle.\n","under-the-hood",{"id":1228,"type":324,"difficulty":347,"subsection":326,"tags":1229,"prompt":1230,"answer":1231,"section":1226,"sectionTitle":134},"under-the-hood-002",[370,525],"You maintain a layer and a module used by ten internal apps. What internals knowledge do you need that an application developer can happily live without?\n","Three things, and they map to the three ways a consumer's build differs from mine. First the **hook order**: my `setup` sees only the modules that ran before it, and `nuxt.options.nitro` stops being readable after Nitro is built, so a write in the wrong hook is a silent no-op — I need to know which stage reads what. Second the **two auto-import registries**: `addImports` targets the Vue app and `addServerImports` targets Nitro, so a composable I register does not exist in a consumer's server route, and the error they get names my function with no hint that a registry is involved. Third the **build\u002Fruntime boundary** and the merge rules, because a consumer's layer chain, their `imports.autoImport: false`, or their Rspack builder all change how my code is resolved.\n\nAn app developer gets one config and one failure mode; I get theirs, and I have to predict it before they report it.\n",{"id":1233,"type":324,"difficulty":347,"subsection":1234,"tags":1235,"prompt":1236,"answer":1237,"docs":1238,"section":1226,"sectionTitle":134},"under-the-hood-003","startup-sequence",[1024,1050],"Walk me through what actually happens when I run `nuxt build`.\n","c12 loads `nuxt.config.ts` through jiti, resolves the `extends` chain into the layer list, applies `.env`, `NUXT_*` overrides and the `$development` \u002F `$production` blocks, and merges everything with defu. That merged object is then resolved against Nuxt's untyped schema, where every option has a `$default` or `$resolve` — that step is what produces `nuxt.options`, which is why an option there is not always what you wrote.\n\nNuxt creates the hookable instance, then runs modules: `modules:before`, Nuxt's internal modules (pages, components, imports, Nitro), the user's modules in `nuxt.options.modules` order with each `setup` awaited, then `modules:done` and `ready`. App resolution follows — `app:resolve` fixes plugins, layouts, middleware and `app.vue`, `app:templates` lets modules generate files, and the templates land in `.nuxt`.\n\nThen the build: `build:before`, Vite's client and server bundles, Nitro through `nitro:config`, `nitro:init` and `nitro:build:before` into `.output\u002Fserver`, `nitro:build:public-assets`, `prerender:routes`, and finally `build:done` and `close`.\n",[1239],{"title":1240,"url":1062},"Nuxt — lifecycle hooks",{"id":1242,"type":338,"difficulty":325,"subsection":1234,"tags":1243,"prompt":1245,"options":1246,"correct":1251,"answer":1252,"docs":1253,"section":1226,"sectionTitle":134},"under-the-hood-004",[357,1244],"unjs","Which package is responsible for finding `nuxt.config.ts`, resolving the `extends` chain and applying `.env` and `NUXT_*` overrides?\n",[1247,1248,1249,1250],"`defu`, which deep-merges each layer's config into the project's","`untyped`, which resolves every option against Nuxt's schema","`c12`, which loads the config files, resolves `extends` and applies the environment overrides","`jiti`, which imports the TypeScript config and therefore owns the merge",[347],"c12 is the config loader: it finds the file, loads it through jiti, walks `extends` into the layer chain, reads `.env` and applies `NUXT_*` and `$env` overrides. The distractors are all real participants in the same pipeline, which is what makes them tempting — `jiti` only executes the TypeScript, `defu` performs the merge that c12 orchestrates, and `untyped` runs afterwards to resolve the merged object against the schema and produce `nuxt.options`. Being able to name which package owns which step is what turns \"config merging\" from Nuxt folklore into something you can debug.\n",[1254],{"title":1255,"url":1256},"unjs\u002Fc12","https:\u002F\u002Fgithub.com\u002Funjs\u002Fc12",{"id":1258,"type":324,"difficulty":476,"subsection":1234,"tags":1259,"prompt":1260,"answer":1261,"section":1226,"sectionTitle":134},"under-the-hood-005",[1050,525,370],"A consumer reports that your module's component-registry template is empty in their app, while it is correct in your playground. Your module builds the registry by reading `nuxt.options.components` inside `setup`. What is happening and how do you fix it?\n","Inside `setup` you can only see what the modules that ran **before** you have already written. Module order comes from `nuxt.options.modules`, and that array is the concatenation of the project's modules and every layer's, so the consumer's order is not yours. In their app, the module that registers the components they care about runs after you, and your read returns a shorter list.\n\nThe fix is to move the read to a stage where the picture is complete: build the registry in `modules:done` or `ready`, or better, react to `app:templates` \u002F `components:extend` so you are handed the resolved data instead of sampling a mutable object. If you genuinely need another module to have run first, declare `moduleDependencies` rather than hoping for an order. The general rule: `setup` is for *writing* into `nuxt.options`; reading the merged result belongs in a later hook.\n",{"id":1263,"type":324,"difficulty":325,"subsection":1264,"tags":1265,"prompt":1266,"answer":1267,"docs":1268,"section":1226,"sectionTitle":134},"under-the-hood-006","nitro-and-h3",[639,640],"What is Nitro, and what does it produce?\n","Nitro is Nuxt's server engine. It takes `server\u002Fapi`, `server\u002Froutes`, `server\u002Fmiddleware`, `server\u002Fplugins` and `server\u002Futils`, plus the Vue SSR render handler, and bundles them into a single deployable output in `.output`. The shape of that output is decided by a **preset** — `node-server`, `vercel`, `netlify`, `cloudflare-module`, `static` and many more — selected with `nitro.preset`, the `NITRO_PRESET` environment variable, or auto-detected from the CI environment. That is what makes the same codebase deployable to a Node container and to a Cloudflare worker.\n\nOn top of the bundle it provides the things a server app needs: route rules for per-path behaviour, cached handlers and cached functions backed by unstorage, storage mounts, an internal `$fetch` that calls your own routes with no network hop, and a plugin system with request-lifecycle hooks. Nuxt 4 runs Nitro 2 with h3 v1.\n",[1269],{"title":1270,"url":1271},"Nuxt — server engine","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fconcepts\u002Fserver-engine",{"id":1273,"type":351,"difficulty":347,"subsection":1264,"language":353,"tags":1274,"prompt":1275,"starter":1276,"solution":1277,"answer":1278,"section":1226,"sectionTitle":134},"under-the-hood-007",[639,525],"From your module's `setup`, extend Nitro so that (a) the resolved module options are available to your server code as a virtual module named `#toolkit\u002Fconfig`, and (b) an in-memory unstorage namespace called `toolkit` is mounted. Neither addition may clobber virtual modules or storage mounts that the consumer or another module has already configured.\n","import { defineNuxtModule, createResolver } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule\u003CToolkitOptions>({\n  meta: { name: '@acme\u002Ftoolkit', configKey: 'toolkit' },\n  setup(options, nuxt) {\n    \u002F\u002F TODO: extend Nitro\n  },\n})\n","import { defineNuxtModule, createResolver, addServerHandler } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule\u003CToolkitOptions>({\n  meta: { name: '@acme\u002Ftoolkit', configKey: 'toolkit' },\n  defaults: { apiBase: '\u002Fapi' },\n  setup(options, nuxt) {\n    const resolver = createResolver(import.meta.url)\n\n    nuxt.hook('nitro:config', (nitroConfig) => {\n      \u002F\u002F additive, never assigned over the top\n      nitroConfig.virtual ||= {}\n      nitroConfig.virtual['#toolkit\u002Fconfig'] = `export default ${JSON.stringify(options)}`\n\n      nitroConfig.storage ||= {}\n      nitroConfig.storage.toolkit ??= { driver: 'memory' }\n    })\n\n    addServerHandler({\n      route: '\u002Fapi\u002F_toolkit\u002Fhealth',\n      handler: resolver.resolve('.\u002Fruntime\u002Fserver\u002Fhealth'),\n    })\n  },\n})\n","Two things are being tested. The first is **where**: Nitro does not exist during `setup`, so anything that has to reach its config goes through the `nitro:config` hook (or `nuxt.options.nitro` in `setup`); a write in `nitro:init` or later is a silent no-op. The second is **additive mutation**: `nitroConfig.virtual = { ... }` and `nitroConfig.storage = { ... }` replace whatever the consumer or another module configured, which is a classic toolkit bug that only shows up in a real app. Use `||=` for the container and `??=` for your own key so an explicit consumer override wins.\n\nWorth saying out loud: serialising options into a virtual module is the standard way to get build-time values into server code without putting them in public runtime config, so secrets do not leak into the client bundle.\n",{"id":1280,"type":351,"difficulty":476,"subsection":1264,"language":353,"tags":1281,"prompt":1283,"starter":1284,"solution":1285,"answer":1286,"section":1226,"sectionTitle":134},"under-the-hood-008",[639,1282,835],"caching","Write a cached Nitro route at `server\u002Fapi\u002Frates.get.ts` that proxies an upstream exchange-rate API. It must cache for five minutes and serve stale content for up to an hour while revalidating, the cache key must be **bounded** (a fixed set of supported currencies, never one entry per raw query string), responses must vary by `accept-language`, and a request carrying an `x-preview-token` header must bypass the cache entirely.\n","export default defineEventHandler(async (event) => {\n  const { currency } = getQuery(event)\n  const config = useRuntimeConfig(event)\n  return await $fetch(`${config.ratesApi}\u002Flatest`, { query: { base: currency } })\n})\n","const SUPPORTED = ['EUR', 'USD', 'GBP', 'CZK'] as const\n\nexport default defineCachedEventHandler(async (event) => {\n  const { currency } = getQuery(event)\n  const base = SUPPORTED.includes(String(currency) as typeof SUPPORTED[number])\n    ? String(currency)\n    : 'EUR'\n  const config = useRuntimeConfig(event)\n  return await $fetch(`${config.ratesApi}\u002Flatest`, { query: { base } })\n}, {\n  name: 'rates',\n  maxAge: 60 * 5,\n  staleMaxAge: 60 * 60,\n  swr: true,\n  \u002F\u002F one entry per supported currency and nothing else\n  getKey: (event) => {\n    const { currency } = getQuery(event)\n    const value = String(currency ?? '').toUpperCase()\n    return SUPPORTED.includes(value as typeof SUPPORTED[number]) ? value : 'default'\n  },\n  varies: ['accept-language'],\n  shouldBypassCache: event => Boolean(getHeader(event, 'x-preview-token')),\n})\n","The point of the exercise is `getKey`. Without it the key is derived from the request, so `?currency=EUR&utm_source=…` and every other junk parameter creates a separate entry — on a long-lived server that is an unbounded cache, which is to say a memory leak. Mapping to a fixed allow-list makes the cache size provable. `varies` adds `accept-language` to the key so a localised response is not served to the wrong locale, without keying per user. `maxAge` plus `staleMaxAge` and `swr` gives fast responses with background revalidation, and `shouldBypassCache` keeps preview traffic out of the shared entry.\n\nMention that the default storage is in-memory and per-process, so `swr` only works across instances once `nitro.storage.cache` is mounted on a shared driver such as Redis.\n",{"id":1288,"type":351,"difficulty":347,"subsection":1264,"language":353,"tags":1289,"prompt":1291,"starter":1292,"solution":1293,"answer":1294,"section":1226,"sectionTitle":134},"under-the-hood-009",[639,640,1290],"observability","Write a Nitro server plugin that puts a request id on every request's context (reusing an incoming `x-request-id` header when present, otherwise generating one) and echoes it back as a response header. The id must be typed so that `event.context.requestId` type-checks in any handler, and the plugin must not accumulate state across requests.\n","export default defineNitroPlugin((nitroApp) => {\n  \u002F\u002F TODO\n})\n","declare module 'h3' {\n  interface H3EventContext {\n    requestId: string\n  }\n}\n\n\u002F\u002F the plugin body runs ONCE, when the server boots\nexport default defineNitroPlugin((nitroApp) => {\n  \u002F\u002F the hook callbacks run per request\n  nitroApp.hooks.hook('request', (event) => {\n    event.context.requestId = getHeader(event, 'x-request-id') || crypto.randomUUID()\n  })\n\n  nitroApp.hooks.hook('beforeResponse', (event) => {\n    setResponseHeader(event, 'x-request-id', event.context.requestId)\n  })\n})\n","The trap is the lifetime difference: `defineNitroPlugin`'s callback runs **once at server start**, and the hooks it registers run **per request**. Anything declared in the plugin body — a `Map`, a counter, a \"current user\" variable — is shared by every request the process handles, which is both a leak and a cross-request data leak. Request-scoped values belong on `event.context`, which is created per request and garbage-collected with it.\n\nThe `declare module 'h3'` augmentation of `H3EventContext` is what makes it a typed contract rather than an untyped bag, and it is what a consumer's handler picks up. In a module you would ship this file and register it with `addServerPlugin`, and export the augmentation from your types so consumers get it too.\n",{"id":1296,"type":338,"difficulty":347,"subsection":1264,"tags":1297,"prompt":1298,"options":1299,"correct":1304,"answer":1305,"section":1226,"sectionTitle":134},"under-the-hood-010",[640,639,415],"During SSR a component calls `$fetch('\u002Fapi\u002Fme')`, one of your own Nitro routes. Which statement is correct?\n",[1300,1301,1302,1303],"It makes a real HTTP request to the server's own port, and the operating system forwards the incoming cookies","It calls the route through Nitro's local fetch with no network hop, but sends none of the incoming request's cookies or headers","It is exactly equivalent to `useFetch`, except that the result is not added to the payload","It throws during SSR, which is precisely why `useFetch` exists",[325],"`$fetch` to a relative path resolves through Nitro's internal fetch, so there is no socket, no DNS and no extra latency — but there is also no request context, so the call is anonymous. The route sees no `Cookie` header and renders the logged-out state, then the client refetches with cookies and renders the logged-in one: a hydration mismatch that only affects signed-in users. Use `useRequestFetch()` in components or `event.$fetch` in server code to forward the incoming headers. Option 3 is the tempting one, but the payload is only half the difference — the missing context is what actually bites.\n",{"id":1307,"type":324,"difficulty":476,"subsection":1264,"tags":1308,"prompt":1310,"answer":1311,"section":1226,"sectionTitle":134},"under-the-hood-011",[639,1309,415],"memory","A consumer reports that after a few days the memory of the Node container running your toolkit grows steadily, and occasionally a user sees another user's name for a moment. Where do you look first in the server code?\n","Both symptoms point at the same thing: state that lives at **process lifetime** instead of request lifetime. I would look at three places. Module-scope variables in `server\u002Futils` or in a module's `runtime\u002Fserver` — a `let currentUser` or a hand-rolled `Map` cache is created once per process and shared by every request, which explains both the growth and the cross-request leak. Then the bodies of `defineNitroPlugin` callbacks, for the same reason: the body runs once at boot, only the hooks it registers run per request. Then cache keys: a `defineCachedEventHandler` or `defineCachedFunction` whose `getKey` includes a user id, a raw query string or a timestamp is an unbounded cache, which grows forever on the default in-memory driver.\n\nThe fix is mechanical: per-request values go on `event.context`, typed through `H3EventContext`; caches get a bounded `getKey` and an unstorage mount with an eviction policy; app-side shared state goes through `useState`, never a module-scope `ref`.\n",{"id":1313,"type":324,"difficulty":325,"subsection":1314,"tags":1315,"prompt":1316,"answer":1317,"docs":1318,"section":1226,"sectionTitle":134},"under-the-hood-012","unjs-toolbelt",[1244,525],"Nuxt is assembled from a lot of small unjs packages. Which ones do you use directly when writing a module, and what does each do for you?\n","On the build side: `defu` for merging options and runtime config, because it gives me the user-over-layer-over-default precedence for free; `pathe` for every path, so a Windows consumer does not get backslashes in generated code; `createResolver` from kit on top of it; `scule` for `pascalCase` \u002F `kebabCase` when deriving component and config names; `ufo` for building URLs; `knitwork` when I generate a template and `magic-string` when I rewrite existing source with a source map; `pkg-types` to read the host app's versions at setup so I can warn instead of crashing; and `consola` through kit's `useLogger` so my output is tagged and respects the consumer's log level.\n\nOn the runtime side it is a smaller set: `ofetch` behind `$fetch`, `destr` for safe parsing of env-shaped values, `ohash` for stable cache and data keys, and `unstorage` behind Nitro's storage and cache.\n",[1319],{"title":1320,"url":1321},"unjs — the ecosystem","https:\u002F\u002Funjs.io",{"id":1323,"type":338,"difficulty":347,"subsection":1314,"tags":1324,"prompt":1325,"options":1326,"correct":1331,"answer":1332,"section":1226,"sectionTitle":134},"under-the-hood-013",[1244,370],"Your module joins paths with `node:path`. A colleague on Windows reports `Failed to resolve import` for an alias your module registers. What is the cause and the fix?\n",[1327,1328,1329,1330],"`node:path` produced backslash separators, which are invalid in bundler ids and generated code; use `pathe` or `createResolver().resolve()`","Windows requires file URLs everywhere; wrap every path in `pathToFileURL` before registering it","The module must be published as CommonJS for Windows consumers to resolve it","Vite on Windows needs `resolve.preserveSymlinks: true` for aliases registered by modules",[378],"`node:path` is platform-dependent: on Windows `join` returns backslashes, and those end up in aliases, template contents and bundler `include` patterns where only forward slashes work. The module builds fine on macOS and fails in a colleague's dev server. `pathe` is a drop-in replacement with POSIX semantics on every platform, which is why Nuxt and kit use it throughout. The other options describe real Windows-adjacent problems but none of them produces this error from this cause — and reaching for `pathToFileURL` everywhere would break bundler ids in a different way.\n",{"id":1334,"type":324,"difficulty":347,"subsection":1314,"tags":1335,"prompt":1337,"answer":1338,"section":1226,"sectionTitle":134},"under-the-hood-014",[1244,525,1336],"bundle","Which of the packages your module depends on must never be imported from its `runtime\u002F` directory, and what happens if one is?\n","The build-time ones: `c12`, `jiti`, `mlly`, `pkg-types`, `giget`, `nypm`, `untyped`, `unbuild`\u002F`mkdist`, and `@nuxt\u002Fkit` itself. They assume Node — the file system, `node:module`, package resolution — and kit additionally needs the build-time Nuxt instance through unctx, which does not exist at runtime.\n\nThe failure depends on where the import lands. In a client component it drags Node-only code into the browser bundle: either a build error about an unresolved `node:` builtin, or a bundle that ships a polyfilled version of a file-system library the user downloads for nothing. In server runtime code it usually \"works\" in a Node preset and breaks in a worker preset such as `cloudflare-module`, so the consumer who deploys to the edge is the one who finds it. The rule I follow is that `runtime\u002F` may only import runtime-safe packages — `ofetch`, `ufo`, `destr`, `defu`, `ohash`, `unstorage` — plus Vue, `#imports` and `#app`; everything else stays in `src\u002F`, and anything a runtime file needs from the build world is carried across via runtime config or a generated template.\n",{"id":1340,"type":324,"difficulty":347,"subsection":1341,"tags":1342,"prompt":1344,"answer":1345,"docs":1346,"section":1226,"sectionTitle":134},"under-the-hood-015","auto-imports",[1343,1024],"unimport","How do Nuxt's auto-imports actually work, and why do unused ones cost nothing at runtime?\n","unimport builds a registry of `{ name, from, as }` entries: scanned from `app\u002Fcomposables` and `app\u002Futils` for the app, `server\u002Futils` for Nitro, plus every layer's equivalents and whatever modules register with `addImports` \u002F `addServerImports`. Then a build-time transform walks each source file, finds bare identifiers that match a registry entry and are not otherwise bound, and prepends a real `import` statement.\n\nNothing is attached to `globalThis` and nothing is eagerly loaded, so an auto-import you never write is never imported and never enters the module graph — which is exactly why the feature is free at runtime and costs only a little build time. It also explains two behaviours people find surprising: shadowing an auto-import with a local `const` just works, because the identifier is bound; and `#imports`, the virtual module that re-exports the registry, keeps working even when the transform is disabled.\n",[1347],{"title":1348,"url":1349},"Nuxt — auto-imports","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fconcepts\u002Fauto-imports",{"id":1351,"type":338,"difficulty":325,"subsection":1341,"tags":1352,"prompt":1353,"options":1354,"correct":1359,"answer":1360,"section":1226,"sectionTitle":134},"under-the-hood-016",[1343,370],"A consumer sets `imports: { autoImport: false }` in their `nuxt.config`. What stops working?\n",[1355,1356,1357,1358],"Everything: composables must now be imported from their real file paths, and `#imports` no longer exists","Only the identifier rewriting; the `#imports` virtual module and its types are unaffected","Only third-party modules' imports; Nuxt's own composables are still rewritten","Nothing at build time, but auto-imported names become `undefined` at runtime",[325],"`autoImport: false` disables the transform that rewrites bare identifiers — it does not delete the registry. `#imports` is still generated, still typed and still re-exports everything, so `import { useState } from '#imports'` keeps working. That is precisely why the rule for module and layer authors is to import explicitly from `#imports` in all `runtime\u002F` code: it costs one line and makes your package work for consumers who turn the transform off, which teams often do because they want explicit imports in review diffs.\n",{"id":1362,"type":324,"difficulty":476,"subsection":1341,"tags":1363,"prompt":1364,"answer":1365,"section":1226,"sectionTitle":134},"under-the-hood-017",[1343,639,370],"A consumer opens an issue: \"your `useToolkit()` works in my components but throws `useToolkit is not defined` in `server\u002Fapi\u002Freport.get.ts`.\" What happened, and what do you ship to fix it properly?\n","There are two auto-import registries, and I only populated one. `addImports` feeds the **app** registry, which serves components, pages and app plugins; server handlers, server middleware and server plugins are transformed against the **Nitro** registry, which is fed by `addServerImports`. The consumer's handler therefore kept the bare identifier and blew up at runtime.\n\nThe wrong fix is to tell them to import my app composable directly from its file path: that pulls Vue reactivity and the Nuxt app context into the server bundle, and `useState` or `useRuntimeConfig` without an event will misbehave or throw. The right fix is a deliberate server-side counterpart registered with `addServerImports` — a different implementation that takes the `event`, reads `useRuntimeConfig(event)` and returns plain values — plus genuinely shared pure helpers moved into `shared\u002F` behind `#shared`. Then I document which export belongs to which side, because the naming is the only clue a consumer has.\n",{"id":1367,"type":324,"difficulty":476,"subsection":1341,"tags":1368,"prompt":1369,"answer":1370,"section":1226,"sectionTitle":134},"under-the-hood-018",[335,1343,452],"Two layers in a consumer's chain both provide a `useAuth()` composable and a `Card.vue` component. How does Nuxt decide which one wins, and how would you make your toolkit's choice explicit?\n","Both collide on **name**, not path. For composables, unimport warns about the duplicate registration and one entry wins; entries registered through `addImports` and `imports.presets` carry a `priority`, and the higher one takes the name. For components, registration collides on the resolved component name — which depends on `pathPrefix` and any `prefix` — and the higher-priority source silently replaces the other, usually with no build-time warning at all. Layer priority (project, then auto-scanned `layers\u002F*`, then `extends` order) drives the default outcome.\n\nFor a toolkit I never rely on the default. I prefix my components (`prefix: 'Acme'`) so the namespace is mine, I register composables with an explicit `priority` when the intent is to override a base layer, and I use `imports:extend` to rename an entry out of the way when I detect a conflict rather than winning by accident. To verify, I read `.nuxt\u002Fimports.d.ts` and `.nuxt\u002Fcomponents.d.ts`, which show exactly what got registered in the consumer's build.\n",{"id":1372,"type":351,"difficulty":476,"subsection":1373,"language":353,"tags":1374,"prompt":1376,"starter":1377,"solution":1378,"answer":1379,"section":1226,"sectionTitle":134},"under-the-hood-019","vite-and-transforms",[1375,1024,370],"unplugin","Write a build plugin that replaces every occurrence of the literal `__TOOLKIT_VERSION__` with your package's version string. It must work for Vite, webpack and Rspack from a single implementation, must not touch files in `node_modules`, must not run the replacement on files that do not contain the token, and must emit a source map so consumers' stack traces stay accurate. Show the registration from the module too.\n","import { createUnplugin } from 'unplugin'\n\nexport const versionPlugin = createUnplugin\u003C{ version: string }>(options => ({\n  name: 'toolkit:version',\n  \u002F\u002F TODO\n}))\n","import { createUnplugin } from 'unplugin'\nimport MagicString from 'magic-string'\n\nconst TOKEN = '__TOOLKIT_VERSION__'\n\nexport const versionPlugin = createUnplugin\u003C{ version: string }>(options => ({\n  name: 'toolkit:version',\n  enforce: 'pre',\n  \u002F\u002F cheap filter: transform() is never called for files that fail this\n  transformInclude: id => \u002F\\.(vue|[cm]?[jt]s)$\u002F.test(id) && !id.includes('\u002Fnode_modules\u002F'),\n  transform(code, id) {\n    if (!code.includes(TOKEN)) return\n    const s = new MagicString(code)\n    let index = code.indexOf(TOKEN)\n    while (index !== -1) {\n      s.overwrite(index, index + TOKEN.length, JSON.stringify(options.version))\n      index = code.indexOf(TOKEN, index + TOKEN.length)\n    }\n    return { code: s.toString(), map: s.generateMap({ source: id, hires: true }) }\n  },\n}))\n\n\u002F\u002F src\u002Fmodule.ts\nimport { defineNuxtModule, addBuildPlugin } from '@nuxt\u002Fkit'\nimport { readPackageJSON } from 'pkg-types'\n\nexport default defineNuxtModule({\n  meta: { name: '@acme\u002Ftoolkit' },\n  async setup() {\n    const { version = '0.0.0' } = await readPackageJSON(import.meta.url)\n    addBuildPlugin(versionPlugin({ version }))\n  },\n})\n","Three things are graded. **Portability**: `createUnplugin` plus `addBuildPlugin` means one implementation serves whichever builder the consumer chose — a plugin registered with `addVitePlugin` simply never runs for someone on `@nuxt\u002Frspack-builder`. **Cost**: `transform` runs for every module in the graph on every dev request and HMR update, so the extension\u002Fpath filter in `transformInclude` and the `code.includes` guard before allocating a `MagicString` are what keep the toolkit from being blamed for a slow dev server. **Correctness**: returning `code.replace(...)` is two lines shorter and shifts every line number, so consumers' breakpoints and stack traces point at the wrong place with no warning from the bundler.\n\nSay out loud that `enforce: 'pre'` matters when the token can appear in an SFC, because you want to see the source before the Vue compiler rewrites it.\n",{"id":1381,"type":324,"difficulty":347,"subsection":1373,"tags":1382,"prompt":1383,"answer":1384,"section":1226,"sectionTitle":134},"under-the-hood-020",[965,370,835],"Since a team installed your module, their dev server keeps printing \"new dependencies optimized… reloading\" and doing full page reloads. What is happening and how do you fix it in the module?\n","That is Vite's dependency pre-bundling. Vite scans the entry graph at startup and pre-bundles CommonJS and deeply-nested dependencies into ESM; anything it discovers **later**, when a route or component is first imported, forces a re-optimisation and a full page reload. A module is a common trigger because its `runtime\u002F` code is only pulled in once a consumer actually uses a component, and if that code imports a CJS SDK, Vite meets it late.\n\nThe fix belongs in the module, not in the consumer's config: add the offending dependencies to `nuxt.options.vite.optimizeDeps.include` in `setup` so they are pre-bundled once at startup, and put large dependencies that are already ESM and that you do not want pre-bundled into `exclude`. If two modules keep discovering each other's dependencies you can get a reload loop, which is the same problem twice. Worth mentioning that this is a dev-only symptom with no production equivalent, which is why it gets reported as \"your module broke HMR\".\n",{"id":1386,"type":338,"difficulty":347,"subsection":1373,"tags":1387,"prompt":1388,"options":1389,"correct":1394,"answer":1395,"section":1226,"sectionTitle":134},"under-the-hood-021",[1375,1024],"Your module ships a code transform. A consumer who builds with `builder: '@nuxt\u002Frspack-builder'` reports that the transform never runs. What is the most likely cause?\n",[1390,1391,1392,1393],"You registered it with `addVitePlugin`, which only applies to the Vite builder; `addBuildPlugin` with an unplugin factory covers Vite, webpack and Rspack","You forgot `enforce: 'pre'`, so it ran after the SFC compiler had already removed the token","You returned `{ code, map }` from `transform`, and Rspack expects a plain string","You registered it in `modules:done`, which runs after the builder has been created",[378],"`addVitePlugin` pushes into `nuxt.options.vite.plugins`, which the Rspack builder never reads — so the plugin is not \"broken\", it simply does not exist in that build. The portable form is `addBuildPlugin` with a factory created by unplugin's `createUnplugin`, which produces a Vite, webpack and Rspack plugin from one implementation. The `enforce` distractor is a real bug in other situations, but it would affect Vite equally; and returning `{ code, map }` is correct everywhere, not a mistake.\n",{"id":1397,"type":324,"difficulty":476,"subsection":1373,"tags":1398,"prompt":1400,"answer":1401,"docs":1402,"section":1226,"sectionTitle":134},"under-the-hood-022",[965,966,1399],"migration","Your module registers a Vite plugin that should only apply to the client build, and it currently uses `addVitePlugin(plugin, { client: true, server: false })`. What is changing, and how would you write it so it survives the next major?\n","Vite's Environment API replaces the \"two separate configs, one for client and one for server\" model with a single config plus named environments, and Nuxt is adopting it. As a result the `client` and `server` options of `addVitePlugin` are deprecated in favour of the plugin deciding for itself with `applyToEnvironment(env => env.name === 'client')` (and `configEnvironment` for per-environment config), and `extendViteConfig` is deprecated for the same reason. The Nuxt 5 line builds on this.\n\nPractically, I move the environment decision into the plugin object, keep `addVitePlugin(plugin, { dev, build, prepend })` for the *when*, and verify the ordering rather than assume it — when Nuxt wraps a non-isomorphic plugin it can apply its own `enforce`, so a plugin that declared `pre` may end up running later than expected. And if the plugin is really a transform rather than a Vite integration, the better answer is to stop using `addVitePlugin` at all and write it as an unplugin registered with `addBuildPlugin`, which sidesteps the whole question.\n",[1403],{"title":1404,"url":1405},"Nuxt Kit — builder helpers","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fkit\u002Fbuilder",{"id":1407,"type":324,"difficulty":325,"subsection":1408,"tags":1409,"prompt":1411,"answer":1412,"docs":1413,"section":1226,"sectionTitle":134},"under-the-hood-023","rendering-and-data",[640,1410],"rendering","What rendering modes does Nuxt support, and how do you choose between them for a given route?\n","Universal SSR is the default: Nitro renders the page and the browser hydrates it, so you get fast first paint and working SEO at the cost of running your components on the server. SPA is `ssr: false` — an empty shell and client rendering, which is right for an authenticated dashboard where there is nothing crawlable and every request is personalised. Static is `nuxt generate`, prerendering everything the crawler finds plus `nitro.prerender.routes`, which is right when content changes at deploy time.\n\nThe useful answer is that this is not one decision for the whole app. Hybrid rendering lets `routeRules` set it per path: marketing pages `prerender: true`, a blog on `isr: 3600`, pricing on `swr: 600`, and `\u002Fapp\u002F**` with `ssr: false`, all in one deployment. I choose per route by asking who the response is for — if it is identical for everyone, cache or prerender it; if it is personalised, render it per request or client-side.\n",[1414],{"title":1415,"url":1416},"Nuxt — rendering modes","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fconcepts\u002Frendering",{"id":1418,"type":324,"difficulty":347,"subsection":1408,"tags":1419,"prompt":1421,"answer":1422,"docs":1423,"section":1226,"sectionTitle":134},"under-the-hood-024",[1420,640,1007],"data","Why should a component use `useAsyncData` or `useFetch` instead of just calling `$fetch` in setup?\n","Because `useAsyncData` is the bridge between the two renders. It runs the handler during SSR, serialises the result into the payload, and the client's first render reuses that value instead of fetching again — one request, no loading flash, and no window in which the two sides disagree. A raw `$fetch` in setup runs on the server *and* again during hydration: two requests, a slower page, and a hydration mismatch if the responses differ at all.\n\nThe options that matter are `key`, because everything dedupes on it and calls sharing a key must share options; `transform` and `pick`, which run before serialisation and therefore shrink the payload that ships in every SSR response; `getCachedData` with its `ctx.cause` (`initial`, `refresh:manual`, `refresh:hook`, `watch`) to decide when a refresh really means the network; and `dedupe`, which defaults to `cancel`. The returned `data` is `undefined` until resolved, and `status` distinguishes idle from pending, which `pending` alone cannot.\n",[1424],{"title":1425,"url":1426},"Nuxt — useAsyncData","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fcomposables\u002Fuse-async-data",{"id":1428,"type":338,"difficulty":347,"subsection":1408,"tags":1429,"prompt":1430,"options":1431,"correct":1436,"answer":1437,"section":1226,"sectionTitle":134},"under-the-hood-025",[1420,370],"On the client, before a `useAsyncData` call with no `default` has resolved, what are `data` and `status`?\n",[1432,1433,1434,1435],"`data` is `null` and `status` is `'loading'`","`data` is `undefined` and `status` is `'pending'`, or `'idle'` if the call has not been executed yet","`data` is an empty object and `status` stays `'idle'` until `refresh()` is called","Reading `data` before resolution throws, so you must check `pending` first",[325],"`data` is `undefined` — not `null` — unless you supply `default`, which is why templates that check `v-if=\"data\"` behave differently from ones that check `data !== null`. `status` is one of `idle`, `pending`, `success` and `error`; there is no `loading`. The distinction between `idle` and `pending` is the practical reason to prefer `status` over the older `pending` boolean: with `immediate: false` or `lazy`, a boolean cannot tell \"not started\" from \"in flight\", so the spinner shows at the wrong time.\n",{"id":1439,"type":351,"difficulty":347,"subsection":1408,"language":353,"tags":1440,"prompt":1441,"starter":1442,"solution":1443,"answer":1444,"docs":1445,"section":1226,"sectionTitle":134},"under-the-hood-026",[564,640],"Write the plugin your module injects, using the object syntax. It must be named `acme:toolkit`, run before unenforced plugins, be allowed to run in parallel with other parallel plugins, wait for a plugin named `acme:auth`, be skipped while rendering islands, provide a `$toolkit` helper to the app, and mark a performance measurement once the app has mounted.\n","export default defineNuxtPlugin((nuxtApp) => {\n  \u002F\u002F TODO: convert to the object syntax and add the ordering metadata\n  return { provide: { toolkit: createToolkit(nuxtApp) } }\n})\n","import { defineNuxtPlugin } from '#imports'\nimport { createToolkit } from '..\u002Ftoolkit'\n\nexport default defineNuxtPlugin({\n  name: 'acme:toolkit',\n  enforce: 'pre',\n  parallel: true,\n  dependsOn: ['acme:auth'],\n  env: { islands: false },\n  setup(nuxtApp) {\n    return { provide: { toolkit: createToolkit(nuxtApp) } }\n  },\n  hooks: {\n    'app:mounted': () => performance.mark('toolkit:ready'),\n  },\n})\n","The object syntax exists so that ordering is declared rather than implied by filename. `name` is what other plugins reference; `dependsOn: ['acme:auth']` makes the dependency explicit and survives a consumer reordering things, which a numeric filename prefix does not. `enforce: 'pre'` puts it in the first group, `parallel: true` allows it to be awaited alongside other parallel plugins instead of blocking the chain, and `env: { islands: false }` keeps it out of island renders where the app context is deliberately minimal.\n\nTwo things to mention out loud: plugins run on **every SSR request**, so anything expensive here is paid per request and a composable is usually the better home; and `provide` is typed through the `NuxtApp` augmentation, so a module should ship that declaration or consumers get `any`.\n",[1446],{"title":1447,"url":1448},"Nuxt — plugins directory","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fapp\u002Fplugins",{"id":1450,"type":324,"difficulty":325,"subsection":769,"tags":1451,"prompt":1452,"answer":1453,"section":1226,"sectionTitle":134},"under-the-hood-027",[335,525],"Layer or module — how do you decide, and what would you build for an internal design-system toolkit?\n","Layers ship app code and conventions: components, pages, composables, server routes, config presets, `app.config`. Modules ship build-time behaviour and the runtime they inject: generated templates, transforms, registered handlers, types, reactions to the consumer's config. The test is whether anything needs to *execute* while Nuxt starts — a layer is declarative, merged by c12 and defu, and cannot run code.\n\nFor an internal design-system toolkit I would build both: a module for the machinery (registering components with a prefix, validating options, generating a token file, injecting a plugin) and a layer for the scaffold (CSS, `app.config` defaults, shared layouts) which lists the module in its own `modules`. Consumers then get everything from one `extends` entry. That combination is safe because `modules` arrays concatenate and Nuxt deduplicates by `meta.name`, and because module options resolve with the project's config beating the layer's beating my defaults.\n",{"id":1455,"type":338,"difficulty":476,"subsection":769,"tags":1456,"prompt":1457,"options":1458,"correct":1463,"answer":1464,"section":1226,"sectionTitle":134},"under-the-hood-028",[335,525,357],"Your toolkit layer lists `@acme\u002Ftoolkit` in its own `modules`, and the consuming project lists it again in `nuxt.config`. What happens?\n",[1459,1460,1461,1462],"The module's `setup` runs twice, once per declaration, which is why a layer should never list a module","The build fails with a duplicate-module error until one declaration is removed","`modules` arrays concatenate under defu, then Nuxt deduplicates by `meta.name`, so `setup` runs once","The project's declaration replaces the layer's, so options configured inside the layer's entry are discarded",[347],"Layer configs are merged with defu, and defu concatenates arrays — so the merged `modules` really does contain the module twice. Nuxt then deduplicates by the module's `meta.name`, which is exactly why every published module must declare one: without a `meta.name` the deduplication has nothing to match on and the module can genuinely run twice, registering its components and plugins twice over. Option 4 is the plausible-sounding trap: nothing is discarded, because options do not travel in the `modules` entry — they resolve separately as `defu(inline, nuxt.config[configKey], defaults)`, where the project's config beats the layer's.\n",{"id":1466,"type":324,"difficulty":325,"subsection":326,"tags":1467,"prompt":1468,"answer":1469,"docs":1470,"section":1474,"sectionTitle":176},"hydration-001",[328,640],"What is hydration in a Nuxt app, and what exactly is a hydration mismatch?\n","On the server, Nitro's render handler runs the component tree through Vue's `renderToString` and sends HTML plus a serialised payload. In the browser the entry calls `createSSRApp(App).mount('#__nuxt')`: because the app was created with `createSSRApp`, `mount` **hydrates**, meaning it runs the first render and walks the existing DOM in step with the vnodes, claiming each node and attaching listeners instead of creating elements.\n\nA mismatch is when the client's first render produces a different tree from the server HTML: a different text node, a different element type, more or fewer children, a different attribute. Vue warns in development and repairs silently in production. The root cause is always some input that existed on only one side: `Date.now()`, a browser API, data fetched outside the payload, or markup the browser corrected.\n",[1471],{"title":1472,"url":1473},"Vue — SSR guide (hydration mismatch)","https:\u002F\u002Fvuejs.org\u002Fguide\u002Fscaling-up\u002Fssr#hydration-mismatch","hydration",{"id":1476,"type":324,"difficulty":347,"subsection":326,"tags":1477,"prompt":1478,"answer":1479,"section":1474,"sectionTitle":176},"hydration-002",[525,335,370],"Why should the author of a layer or module care about hydration more than an application developer does?\n","Because the blast radius is different. An app developer who ships a mismatch breaks one page in one app and sees the warning in their own console. A layer's layout or a module's component runs on **every page of every consuming app**, and the warning appears in the consumer's console with a trace pointing at *their* page, so their first hours of debugging happen in the wrong repository. They also cannot fix it without forking or wrapping your component in `\u003CClientOnly>`, which deletes the server rendering you promised them.\n\nThat changes the responsibilities: components must be deterministic by construction, composables must document what they return during SSR, browser-only components are registered with `mode: 'client'` or as `.client.vue` so consumers never need to know, head and body attributes go through `useHead`, heavy static output goes into server components, and the playground ships an E2E test that fails on any console message matching `\u002FHydration\u002F`.\n",{"id":1481,"type":338,"difficulty":325,"subsection":1482,"tags":1483,"prompt":1484,"options":1485,"correct":1490,"answer":1491,"section":1474,"sectionTitle":176},"hydration-003","what-hydration-is",[640],"In a production build with no debug flags set, what does Vue 3.5 do when it detects a hydration mismatch?\n",[1486,1487,1488,1489],"Throws an error that the Nuxt error page or `\u003CNuxtErrorBoundary>` must catch","Logs the same warning as in development, just without the component trace","Recovers silently: overwrites mismatched text, re-creates mismatched nodes and carries on","Abandons hydration and performs a full client-side render of the whole application",[347],"Production Vue compiles the hydration warnings out entirely (unless `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` is set) and recovers in place: text content is overwritten, a node of the wrong type is discarded and mounted fresh, surplus server nodes are removed. It never throws and never re-renders the whole app, which is exactly why these bugs survive into production unnoticed while users pay in layout shift, a flash of content and wasted main-thread time. The tempting \"logs without a trace\" option is what many people assume; the reality is total silence.\n",{"id":1493,"type":324,"difficulty":347,"subsection":1482,"tags":1494,"prompt":1495,"answer":1496,"docs":1497,"section":1474,"sectionTitle":176},"hydration-004",[640,1007],"How does Nuxt make sure the client's first render sees the same inputs the server saw? Name the channels.\n","Nuxt ships state rather than hoping both sides recompute it identically. `useState` refs and `useAsyncData`\u002F`useFetch` results are serialised with devalue into the `#__NUXT_DATA__` JSON script and **revived before the app renders**, so the client's first render already holds the server's values. `useCookie` reads the request's `Cookie` header on the server and `document.cookie` in the browser, so a preference the server rendered is the one the client renders. Head state goes through unhead, which renders tags on the server and hydrates them on the client. `useId()` is deterministic by construction, so nothing has to travel.\n\nEverything outside these channels is single-sided: `Date.now()`, `window`, `localStorage`, a raw `$fetch` in setup. The senior point is that the fix for a mismatch is almost always \"move this value into one of the channels\", not \"hide it in `\u003CClientOnly>`\".\n",[1498,1501],{"title":1499,"url":1500},"Nuxt — useState","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fcomposables\u002Fuse-state",{"title":1425,"url":1426},{"id":1503,"type":324,"difficulty":476,"subsection":1482,"tags":1504,"prompt":1506,"answer":1507,"docs":1508,"section":1474,"sectionTitle":176},"hydration-005",[795,1505],"accessibility","What are `useId()` and `data-allow-mismatch`, and when would you reach for each?\n","Both are Vue 3.5. `useId()` (import from `vue`) returns an id that is unique per app instance and **identical on server and client**, because it is derived from the component's position in the tree rather than from a counter or `Math.random()`. It is the correct source for generated `id`, `for` and `aria-*` attributes in form primitives and disclosure widgets; if several Vue apps share a page you separate them with `app.config.idPrefix`, and you never call it inside `computed`. Nuxt 4 no longer documents its own `useId`; use Vue's.\n\n`data-allow-mismatch` is a **declaration**, not a fix: it tells Vue that a mismatch on this element is expected and suppresses the warning, optionally scoped to `text`, `children`, `class`, `style` or `attribute`. The recovery still happens, so a timestamp formatted in the user's locale still flashes from the server's value to the client's. I use it only where the mismatch is inherent and the flash is acceptable; used as a console-cleaner it destroys the signal for real bugs.\n",[1509,1512],{"title":1510,"url":1511},"Vue — useId()","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fcomposition-api-helpers.html#useid",{"title":1513,"url":1514},"Vue — SSR API (data-allow-mismatch)","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fssr.html",{"id":1516,"type":351,"difficulty":325,"subsection":1482,"language":795,"tags":1517,"prompt":1518,"starter":1519,"solution":1520,"answer":1521,"section":1474,"sectionTitle":176},"hydration-006",[640],"This footer produces two hydration warnings. Fix it so it hydrates cleanly. The timestamp must be the time the **server** rendered the page (the client must not recompute it), and the footer must keep a unique `id`.\n","\u003Ctemplate>\n  \u003Cfooter :id=\"`stamp-${Math.random().toString(36).slice(2)}`\">\n    Rendered at {{ Date.now() }}\n  \u003C\u002Ffooter>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\nimport { useId } from 'vue'\n\n\u002F\u002F initializer runs on the server; the value is shipped in the payload and reused by the client\nconst renderedAt = useState('footer:rendered-at', () => Date.now())\nconst id = useId()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cfooter :id=\"id\">Rendered at {{ renderedAt }}\u003C\u002Ffooter>\n\u003C\u002Ftemplate>\n","Two different tools for two different problems. The timestamp is a value that must be computed **once** and shared, so it goes through the payload: a `useState` initializer runs during SSR and the client revives the value before its first render. The id must be **stable by construction**, so `useId()` from Vue 3.5 replaces the random string; nothing needs to travel. Common mistakes: guarding with `import.meta.client` (the server still renders a different value), or storing a `Date` when an ISO string or number is what the template needs. Mention that the `useState` key should be namespaced so a component elsewhere cannot collide with it.\n",{"id":1523,"type":351,"difficulty":347,"subsection":1524,"language":795,"tags":1525,"prompt":1527,"starter":1528,"solution":1529,"answer":1530,"section":1474,"sectionTitle":176},"hydration-007","catalogue-of-causes",[640,1526],"cookies","This theme wrapper hydrates with a `class` mismatch for every user who picked the dark theme. Fix it so the server HTML already has the correct theme class and the choice still persists across visits.\n","\u003Cscript setup lang=\"ts\">\nconst theme = ref(import.meta.client ? localStorage.getItem('theme') ?? 'light' : 'light')\n\nfunction toggle() {\n  theme.value = theme.value === 'light' ? 'dark' : 'light'\n  if (import.meta.client) localStorage.setItem('theme', theme.value)\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv :class=\"`theme-${theme}`\">\n    \u003Cbutton @click=\"toggle\">Toggle theme\u003C\u002Fbutton>\n    \u003Cslot \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\n\u002F\u002F the request carries the cookie, so server and client render the same class\nconst theme = useCookie\u003C'light' | 'dark'>('theme', {\n  default: () => 'light',\n  maxAge: 60 * 60 * 24 * 365,\n})\n\nfunction toggle() {\n  theme.value = theme.value === 'light' ? 'dark' : 'light'\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv :class=\"`theme-${theme}`\">\n    \u003Cbutton @click=\"toggle\">Toggle theme\u003C\u002Fbutton>\n    \u003Cslot \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","`localStorage` exists only in the browser, so the server always renders `theme-light`; the `import.meta.client` guard prevents the crash but not the mismatch. A cookie is the storage the server can read: `useCookie` returns the header value during SSR and `document.cookie` on the client, and writing to the ref persists it. Worth saying out loud: a `class` mismatch is one Vue does **not** repair during hydration, so without this fix the wrapper stays `theme-light` while state says `dark` until the next reactive update. Also mention that `secure: true` on a plain-HTTP dev origin would reintroduce the mismatch, because the browser would not send the cookie back.\n",{"id":1532,"type":338,"difficulty":325,"subsection":1524,"tags":1533,"prompt":1535,"options":1536,"correct":1541,"answer":1542,"section":1474,"sectionTitle":176},"hydration-008",[1534],"html","A component's template renders `\u003Cp>\u003Cdiv>{{ text }}\u003C\u002Fdiv>\u003C\u002Fp>`. Why does this produce a hydration warning even though the server output contains exactly that markup?\n",[1537,1538,1539,1540],"Vue's server renderer strips block elements out of `\u003Cp>`, so the server HTML is missing the `\u003Cdiv>`","The browser's HTML parser closes the `\u003Cp>` before the `\u003Cdiv>`, so the DOM Vue hydrates against differs from the client vnode tree","`\u003Cp>` is a void element and cannot have children, so Vue refuses to hydrate it","It only warns in development because eslint-plugin-vue injects a runtime check; production is unaffected",[325],"The server string is exactly what the template says, but the browser parses it with the HTML spec's error recovery: a `\u003Cdiv>` implicitly closes an open `\u003Cp>`, so the DOM becomes `\u003Cp>\u003C\u002Fp>\u003Cdiv>…\u003C\u002Fdiv>\u003Cp>\u003C\u002Fp>`. Vue then hydrates against that corrected DOM and reports a children or node mismatch. The same happens with `\u003Ca>` inside `\u003Ca>`, `\u003Ctr>` outside `\u003Ctbody>` and `\u003Cli>` outside a list. Vue 3.4 flags invalid nesting in development, but production recovers silently, so the fix is the markup, and a validator over the SSR output in CI keeps it fixed.\n",{"id":1544,"type":351,"difficulty":347,"subsection":1524,"language":795,"tags":1545,"prompt":1546,"starter":1547,"solution":1548,"answer":1549,"section":1474,"sectionTitle":176},"hydration-009",[1534],"This card hydrates with children mismatches on every page. Fix the markup so it hydrates cleanly without changing what the user sees (a title, a lead paragraph and two links).\n","\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ title: string, lead: string, href: string, docsHref: string }>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cp class=\"card\">\n    \u003Cdiv class=\"card__title\">{{ title }}\u003C\u002Fdiv>\n    {{ lead }}\n    \u003Ca :href=\"href\">Read more \u003Ca :href=\"docsHref\">(docs)\u003C\u002Fa>\u003C\u002Fa>\n  \u003C\u002Fp>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ title: string, lead: string, href: string, docsHref: string }>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Carticle class=\"card\">\n    \u003Ch2 class=\"card__title\">{{ title }}\u003C\u002Fh2>\n    \u003Cp>{{ lead }}\u003C\u002Fp>\n    \u003Cp>\n      \u003Ca :href=\"href\">Read more\u003C\u002Fa>\n      \u003Ca :href=\"docsHref\">(docs)\u003C\u002Fa>\n    \u003C\u002Fp>\n  \u003C\u002Farticle>\n\u003C\u002Ftemplate>\n","Two invalid structures: a block `\u003Cdiv>` inside `\u003Cp>` (the parser closes the paragraph early) and an `\u003Ca>` nested in an `\u003Ca>` (the parser splits them). Both make the browser's DOM differ from the vnode tree the client builds, so Vue reports mismatches although the server string was \"right\". The fix is semantic HTML: a block container (`\u003Carticle>`\u002F`\u003Cdiv>`), a heading, paragraphs for phrasing content, sibling anchors. Mention that this class of bug never shows in a Vue unit test with `mount` because jsdom is not hydrating server HTML; only SSR plus a browser (or an HTML validator on the SSR output) catches it.\n",{"id":1551,"type":351,"difficulty":347,"subsection":1524,"language":795,"tags":1552,"prompt":1554,"starter":1555,"solution":1556,"answer":1557,"docs":1558,"section":1474,"sectionTitle":176},"hydration-010",[1553,640],"data-fetching","This list component fetches on the server, then fetches again on the client and sometimes hydrates with a children mismatch. Fix it so the data is fetched once, shipped to the client and rendered identically on both sides.\n","\u003Cscript setup lang=\"ts\">\ninterface Item { id: number, name: string }\n\nconst items = await $fetch\u003CItem[]>('\u002Fapi\u002Fitems')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cul>\n    \u003Cli v-for=\"item in items\" :key=\"item.id\">{{ item.name }}\u003C\u002Fli>\n  \u003C\u002Ful>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\ninterface Item { id: number, name: string }\n\n\u002F\u002F fetched during SSR, serialised into the payload, reused by the client's first render\nconst { data: items } = await useFetch\u003CItem[]>('\u002Fapi\u002Fitems', { default: () => [] })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cul>\n    \u003Cli v-for=\"item in items\" :key=\"item.id\">{{ item.name }}\u003C\u002Fli>\n  \u003C\u002Ful>\n\u003C\u002Ftemplate>\n","A raw `$fetch` in setup runs on the server and again on the client, and nothing is written to the payload. The second request can return fresher data, a different order, or a different result because the server-side call lacked the user's cookies, and any difference is a mismatch; even identical data costs a duplicate request and delays interactivity. `useFetch` (or `useAsyncData` with an explicit key) keys the result, ships it in `#__NUXT_DATA__` and deduplicates. `default: () => []` keeps the template stable while pending. If the data genuinely must be client-only, `server: false` makes both sides render the default first, which is deterministic.\n",[1559],{"title":1425,"url":1426},{"id":1561,"type":338,"difficulty":347,"subsection":1524,"tags":1562,"prompt":1564,"options":1565,"correct":1570,"answer":1571,"docs":1572,"section":1474,"sectionTitle":176},"hydration-011",[1563,640],"teleport","A layer ships a `\u003CTeamModal>` built on `\u003CTeleport to=\"body\">`. On server-rendered pages consumers see the modal content duplicated or a hydration warning. Which statement is correct?\n",[1566,1567,1568,1569],"Nuxt renders every Teleport target during SSR, so the problem must be somewhere else","Nuxt renders only the `#teleports` target during SSR; for any other target the Teleport must be wrapped in `\u003CClientOnly>`","Teleport is not supported in SSR at all; the component has to use `v-if` on a mounted flag instead","Vue automatically defers every Teleport until after hydration, so this is a bug in Nuxt",[325],"Vue's server renderer collects teleported content into `ssrContext.teleports`, and Nuxt writes only the `#teleports` entry into the document (a `\u003Cdiv id=\"teleports\">` it appends to the body). Content teleported to `body` or to a custom selector is therefore absent from the HTML while the client expects it, or ends up duplicated after recovery. The documented rule is: target `#teleports` when the content should be server-rendered, wrap the `\u003CTeleport>` in `\u003CClientOnly>` for any other target. Teleport is supported in SSR, just with this constraint.\n",[1573],{"title":1574,"url":1575},"Nuxt — \u003CTeleport> and SSR","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fcomponents\u002Fteleports",{"id":1577,"type":351,"difficulty":347,"subsection":1524,"language":795,"tags":1578,"prompt":1579,"starter":1580,"solution":1581,"answer":1582,"section":1474,"sectionTitle":176},"hydration-012",[1563,640],"This dialog component hydrates with a mismatch because its Teleport target does not exist in the server HTML. Fix it so the dialog content is server-rendered and hydrates cleanly.\n","\u003Cscript setup lang=\"ts\">\nconst open = defineModel\u003Cboolean>({ default: false })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CTeleport to=\"#modal-root\">\n    \u003Cdialog v-if=\"open\" open class=\"dialog\">\n      \u003Cslot \u002F>\n      \u003Cbutton @click=\"open = false\">Close\u003C\u002Fbutton>\n    \u003C\u002Fdialog>\n  \u003C\u002FTeleport>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\nconst open = defineModel\u003Cboolean>({ default: false })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- #teleports is the one target Nuxt renders during SSR -->\n  \u003CTeleport to=\"#teleports\">\n    \u003Cdialog v-if=\"open\" open class=\"dialog\">\n      \u003Cslot \u002F>\n      \u003Cbutton @click=\"open = false\">Close\u003C\u002Fbutton>\n    \u003C\u002Fdialog>\n  \u003C\u002FTeleport>\n\u003C\u002Ftemplate>\n","`#modal-root` never exists in the SSR output: Nuxt only serialises `ssrContext.teleports['#teleports']` into the document, so the server HTML lacks the dialog while the client's first render expects it. Retargeting to `#teleports` keeps server rendering and makes both trees agree. If the target *must* be arbitrary (a third-party container, for instance), the alternative is `\u003CClientOnly>\u003CTeleport to=\"#modal-root\">…\u003C\u002FTeleport>\u003C\u002FClientOnly>`, at the cost of no server HTML for the dialog. Mention that in a layer this is exactly the kind of detail consumers cannot fix themselves, so the component should be right before it ships.\n",{"id":1584,"type":351,"difficulty":347,"subsection":1585,"language":795,"tags":1586,"prompt":1587,"starter":1588,"solution":1589,"answer":1590,"docs":1591,"section":1474,"sectionTitle":176},"hydration-013","author-responsibilities",[1505,795],"This form field primitive from the team UI layer generates its own ids and produces attribute mismatches on `id`, `for` and `aria-describedby`. Fix it so the ids are stable across server and client and remain unique per instance.\n","\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ label: string, hint?: string }>()\nconst model = defineModel\u003Cstring>()\n\nconst id = `field-${Math.random().toString(36).slice(2, 8)}`\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"field\">\n    \u003Clabel :for=\"id\">{{ label }}\u003C\u002Flabel>\n    \u003Cinput :id=\"id\" v-model=\"model\" :aria-describedby=\"hint ? `${id}-hint` : undefined\">\n    \u003Cp v-if=\"hint\" :id=\"`${id}-hint`\" class=\"field__hint\">{{ hint }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\nimport { useId } from 'vue'\n\ndefineProps\u003C{ label: string, hint?: string }>()\nconst model = defineModel\u003Cstring>()\n\n\u002F\u002F unique per instance, identical on server and client\nconst id = useId()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"field\">\n    \u003Clabel :for=\"id\">{{ label }}\u003C\u002Flabel>\n    \u003Cinput :id=\"id\" v-model=\"model\" :aria-describedby=\"hint ? `${id}-hint` : undefined\">\n    \u003Cp v-if=\"hint\" :id=\"`${id}-hint`\" class=\"field__hint\">{{ hint }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","`Math.random()` (or a module-level counter, which also leaks across requests on the server) gives every render a different id, so the server HTML and the client vnodes disagree on `id`, `for` and `aria-describedby`; attribute mismatches are only warned about, never repaired, so the label association is broken until a re-render. Vue 3.5's `useId()` derives the id from the component's position in the tree, so it is unique per instance and stable across server and client; derived ids like `${id}-hint` inherit that stability. Do not call it inside `computed`, and mention `app.config.idPrefix` for pages that host more than one Vue app.\n",[1592],{"title":1510,"url":1511},{"id":1594,"type":324,"difficulty":476,"subsection":1524,"tags":1595,"prompt":1597,"answer":1598,"section":1474,"sectionTitle":176},"hydration-014",[453,1596],"debugging","A consumer of your layer reports hydration warnings on their dashboard, but only for some users and never on your machine. What do you check, in what order, and why?\n","\"Some users\" means environment, so I stop reading component code and reproduce their environment. First the **browser**: extensions such as Grammarly, translators and ad blockers mutate the DOM before hydration and produce attribute or class mismatches; I ask for a repro in a clean profile or incognito. Second **locale and timezone**: dates and numbers formatted with `toLocaleString()` or `Intl.*` differ between a UTC\u002FEnglish server and a Prague user; I run the server with `TZ=Europe\u002FPrague LANG=cs_CZ.UTF-8` and look at every formatted value. Third **auth state**: anything derived from `localStorage` or in-memory tokens renders logged-out on the server and logged-in on the client, so I compare logged-in versus logged-out. Fourth **caching**: if it started right after a deploy or only hits some regions, a CDN or SWR layer is serving stale HTML against new chunks, or personalised HTML built for another user; I check `age` and cache-status headers before touching a component. Only then do I bisect with `\u003CClientOnly>` and check the payload, and I end by adding their route to the hydration E2E test with a timezone matrix.\n",{"id":1600,"type":324,"difficulty":476,"subsection":1524,"tags":1601,"prompt":1602,"answer":1603,"docs":1604,"section":1474,"sectionTitle":176},"hydration-015",[1282,415,639],"How can a CDN or Nitro cache produce hydration errors, and what is the connection to the Nuxt 4.5.1 payload-cache advisory?\n","Two shapes. **Stale HTML after a deploy**: the cache keeps serving yesterday's document, whose inline payload and chunk names belong to the old build, while the browser loads today's client bundle. The client renders with different code and different chunk URLs, so you get mismatches and chunk-load errors only in the minutes after a release. Mitigations: purge on deploy, version cache keys, and rely on the app manifest with `experimental.emitRouteChunkError: 'automatic'` (the default) to reload on a failed chunk. **Personalised content cached without variation**: a route with `swr`\u002F`isr`\u002F`cache` rules stores one user's rendered HTML or payload under a path-only key, and the next user receives it, so their client render disagrees with the HTML, and worse, they see someone else's data. Fix: never cache personalised routes, or vary the key with `varies`\u002F`getKey` in the Nitro cache options and bypass with `shouldBypassCache` for authenticated requests.\n\nAdvisory GHSA-wm8w-6qjm-cv43 (Nuxt 4.4.0–4.5.0, fixed in 4.5.1) was exactly the second shape at framework level: the renderer stored the SSR payload in the shared `cache:nuxt:payload` storage keyed by path alone, so cached payloads were served across users and to unauthenticated clients. The hydration warning was the visible symptom of a data leak.\n",[1605,1608],{"title":1606,"url":1607},"GHSA-wm8w-6qjm-cv43","https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt\u002Fsecurity\u002Fadvisories\u002FGHSA-wm8w-6qjm-cv43",{"title":1609,"url":1610},"Nitro — cache","https:\u002F\u002Fnitro.build\u002Fdocs\u002Fcache",{"id":1612,"type":324,"difficulty":347,"subsection":1613,"tags":1614,"prompt":1616,"answer":1617,"docs":1618,"section":1474,"sectionTitle":176},"hydration-016","diagnosing",[1596,1615],"production","Walk me through debugging a hydration mismatch that only happens in production.\n","Production is silent by design: Vue compiles the warnings out unless `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` is set. So step one is a staging build with `debug: { hydration: true }` in `nuxt.config` (Nuxt sets the Vite define for you; the manual form is `vite.define`), scoped with `$env.staging` so it never ships, because the flag adds bundle size and logs to every user. With the warning visible I diff the server HTML from `curl` against `document.documentElement.outerHTML` after load, both run through prettier: the diff is exactly what Vue repaired. If the warning still does not reproduce, the difference is environmental: I run the server with the user's `TZ` and `LANG`, compare logged-in and logged-out, try a clean browser profile, and read the cache headers to rule out stale or personalised HTML. Then I bisect with `\u003CClientOnly>` as a probe, and check the payload in Nuxt DevTools to see whether the data the client renders was ever shipped. I finish with an E2E test that fails on `\u002FHydration\u002F` so the regression cannot return.\n",[1619],{"title":1620,"url":1621},"Vue — compile-time flags","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fcompile-time-flags.html",{"id":1623,"type":338,"difficulty":347,"subsection":1613,"tags":1624,"prompt":1625,"options":1626,"correct":1631,"answer":1632,"section":1474,"sectionTitle":176},"hydration-017",[357,1596],"You need a production build of a Nuxt 4.5 app to log hydration mismatch details. Which configuration does that?\n",[1627,1628,1629,1630],"`debug: { hydration: true }` in `nuxt.config.ts`","`vite: { define: { __VUE_PROD_DEVTOOLS__: 'true' } }`","`sourcemap: { client: true }` so the warnings can be mapped back to components","`experimental: { renderJsonPayloads: false }` to disable payload minification",[378],"Nuxt's `debug` option accepts a granular object, and `debug.hydration` is wired into the Vite `define` block as `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` (`debug: true` enables it too, along with every other debug flag). `__VUE_PROD_DEVTOOLS__` enables devtools support in production, not mismatch logging. Sourcemaps do not change what Vue logs. `renderJsonPayloads` controls how the payload is serialised and has nothing to do with warnings. Mention that the equivalent manual form is `vite: { define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true' } }`, and that either belongs in a `$env.staging` override, not in the default build.\n",{"id":1634,"type":324,"difficulty":476,"subsection":1613,"tags":1635,"prompt":1636,"answer":1637,"docs":1638,"section":1474,"sectionTitle":176},"hydration-018",[730,414,513],"Design the CI safety net that catches hydration regressions in a shared layer before any consumer sees them.\n","The core is an E2E test with `@nuxt\u002Ftest-utils`: `setup({ rootDir, browser: true, nuxtConfig: { debug: { hydration: true } } })` builds the fixture as a **production** bundle, which is what consumers run, but with Vue's mismatch details compiled in, because a plain production build is silent and would pass with a broken page. Each test calls `createPage()` **without a path**, attaches `page.on('console')` and `page.on('pageerror')`, then navigates with `page.goto(url(route), { waitUntil: 'hydration' })`, which polls `window.useNuxtApp().isHydrating === false` rather than stopping at `load`. It asserts that no message matches `\u002FHydration\u002F`.\n\nAround that: one fixture per rendering mode the layer supports (SSR, `ssr: false`, prerendered), a route list covering every layout and page the layer ships, and a matrix job with `TZ=Europe\u002FPrague` and a non-English `LANG` to catch locale formatting. An HTML validator over `$fetch`'ed SSR output catches invalid nesting cheaply. The suite runs on every PR and is a release gate; the module template's `test\u002Ffixtures\u002F*` plus `playground\u002F` is where it lives.\n",[1639],{"title":1640,"url":738},"Nuxt — testing",{"id":1642,"type":324,"difficulty":476,"subsection":1585,"tags":1643,"prompt":1644,"answer":1645,"docs":1646,"section":1474,"sectionTitle":176},"hydration-019",[525,513],"How does a module you author avoid causing hydration mismatches in the apps that install it?\n","By designing for SSR by construction rather than reacting to warnings. Components in `runtime\u002F` render identically on both sides: no `Date.now()`, `Math.random()` or locale formatting in render paths, ids from `useId()`, browser APIs only in `onMounted` with an SSR default that is also the client's first render. Composables that need the browser return a stable value during SSR (`null`, `false`), flip after mount, clean up with `onScopeDispose`, and say so in the README. Components that cannot exist in Node (editors, maps, canvas) are registered with `addComponent({ mode: 'client' })` so Nuxt wraps them and consumers never write `\u003CClientOnly>`; they get a sized fallback. Plugins never touch the DOM before mount; `\u003Chtml>`\u002F`\u003Cbody>` attributes go through `useHead` so unhead renders them on both sides. Heavy static output (markdown, highlighted code) lives in a `.server.vue` component that is never hydrated. And the playground ships the hydration E2E test so a regression fails CI before it reaches a consumer. The trade-off I state out loud: `mode: 'client'` moves cost to layout shift and missing SSR content, so it is the exception, not the default.\n",[1647],{"title":1648,"url":1649},"Kit — addComponent","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fkit\u002Fcomponents",{"id":1651,"type":324,"difficulty":347,"subsection":1585,"tags":1652,"prompt":1654,"answer":1655,"docs":1656,"section":1474,"sectionTitle":176},"hydration-020",[1653],"trade-offs","When is `\u003CClientOnly>` the wrong fix for a hydration mismatch?\n","When the content matters or when the mismatch could be made deterministic, which covers most cases. `\u003CClientOnly>` deletes server rendering for the whole subtree: crawlers and the LCP measurement never see it, the default slot is tree-shaken from the server build so its CSS is no longer inlined, and the user gets a hole (or a fallback) that fills after mount, which is itself a layout shift. It also hides the signal: the real bug is still there, wrapped.\n\nThe deterministic fixes usually exist: `useState` for values computed once, `useCookie` for preferences the server should know, `useAsyncData` for data, `useId` for ids, `useHead` for body attributes, `#teleports` for teleports. I reserve `\u003CClientOnly>` and `mode: 'client'` for things that genuinely cannot run in Node, give them a sized `#fallback`, and I never put one in a shared layout casually, because there it affects every page in every consumer.\n",[1657],{"title":1658,"url":1659},"Nuxt — \u003CClientOnly>","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fcomponents\u002Fclient-only",{"id":1661,"type":338,"difficulty":325,"subsection":1585,"tags":1662,"prompt":1663,"options":1664,"correct":1669,"answer":1670,"docs":1671,"section":1474,"sectionTitle":176},"hydration-021",[525,387],"Your module ships a `\u003CTeamRichEditor>` that instantiates a browser-only library in `setup()`. What is the idiomatic way to make it safe without forcing consumers to wrap it in `\u003CClientOnly>`?\n",[1665,1666,1667,1668],"Register it with `addComponent({ name, filePath, mode: 'client' })` (or name the file `.client.vue` in a layer)","Wrap its template in `\u003CDevOnly>` so the server never renders it","Set `ssr: false` from the module's `setup()` so consumers' apps become SPAs","Rename it to `TeamRichEditor.server.vue` so it becomes an island",[378],"`mode: 'client'` (the `.client.vue` suffix in a layer) tells Nuxt to render the component only in the browser and wrap it automatically, so the consumer writes `\u003CTeamRichEditor \u002F>` and nothing else. `\u003CDevOnly>` removes content from production builds, not from the server. Flipping `ssr: false` for the whole consumer app is a wildly disproportionate side effect a module must never have. A `.server.vue` island is the opposite of what is needed: it would run the browser-only library in Node. Mention the trade-off: no server HTML for the editor, so document it and offer a fallback.\n",[1672],{"title":1648,"url":1649},{"id":1674,"type":324,"difficulty":476,"subsection":1585,"tags":1675,"prompt":1677,"answer":1678,"docs":1679,"section":1474,"sectionTitle":176},"hydration-022",[513,835,1676],"islands","Your layer needs to ship a heavy component: a rendered markdown body with syntax-highlighted code and one interactive \"copy\" button per block. Compare `\u003CClientOnly>`, `mode: 'client'`, a server component\u002Fisland and lazy hydration for it, and pick one.\n","`\u003CClientOnly>` or `mode: 'client'` would ship the markdown renderer and highlighter to the browser and render nothing on the server, so no SEO, a layout shift, and the largest possible bundle: wrong for content. Lazy hydration (`\u003CLazyMarkdownBody hydrate-on-visible \u002F>`) keeps the server HTML and delays hydration, but the client still downloads the renderer and highlighter to hydrate later, and the HTML must match when it does; it is a performance lever for interactive components, not a way to avoid shipping code. A server component (`MarkdownBody.server.vue`, backed by `\u003CNuxtIsland>`) renders on the server only, sends finished HTML, ships zero client JS for that subtree and is never hydrated, so it cannot mismatch; its constraints are serialisable props and a server round-trip per island on client-side navigation.\n\nI would pick the island, and hydrate only the copy buttons with the `nuxt-client` attribute (`experimental.componentIslands: { selectiveClient: true }`), so the interactive part is a few hundred bytes and the content stays static. I would document that the body is non-interactive by design and add it to the hydration E2E fixture.\n",[1680],{"title":1681,"url":1682},"Nuxt — \u003CNuxtIsland>","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fcomponents\u002Fnuxt-island",{"id":1684,"type":324,"difficulty":325,"subsection":326,"tags":1685,"prompt":1686,"answer":1687,"docs":1688,"section":1691,"sectionTitle":203},"memory-leaks-001",[640,328],"Why do server-rendered Nuxt apps leak memory more easily than single-page apps?\n","Because the process outlives the request. In an SPA the browser tab owns every object and takes them all away on reload, so a sloppy module-scope array is at worst a session-long annoyance. In Nuxt, **plugins and component `setup()` run again for every SSR request** inside one Node process that stays up for days. Anything those runs create at module scope, or attach to something global — an array, a `Map`, a `process.on` listener, a `setInterval` — is still reachable when the next request arrives, so it accumulates a few thousand times an hour.\n\nThe general definition is worth saying out loud: a leak is a **longer-lived owner holding references to shorter-lived objects**, plus the variant where a genuinely long-lived structure only ever grows. That is why the fix is almost always \"give the value a home with the right lifetime\" rather than \"make the object smaller\".\n",[1689],{"title":1690,"url":1448},"Nuxt — plugins","memory-leaks",{"id":1693,"type":324,"difficulty":347,"subsection":326,"tags":1694,"prompt":1695,"answer":1696,"section":1691,"sectionTitle":203},"memory-leaks-002",[415,640],"What is cross-request state pollution, and why do you treat it as a security bug rather than a memory bug?\n","It is request-lifetime data stored at process lifetime and read back by a later request. A module-scope `let currentUser`, a `ref()` created at import time, a `createPinia()` or i18n instance built once in a module rather than per `nuxtApp`, a `const cache = new Map()` keyed by something that does not identify the user: request one writes into it and request two renders it. The memory symptom is a heap that grows; the security symptom is one user's name, order list or permissions appearing in another user's HTML, which is a disclosure incident rather than a performance regression.\n\nThe same shape shows up in caching: an SSR response cached without varying on identity is the pattern behind Nuxt's own advisories in this area. So on the server I treat \"does this leak?\" and \"can this leak *data*?\" as one question, and the fixes are the same — `event.context` for the request, `useState` or `nuxtApp` for the app instance, per-app instances of anything stateful, and a deliberate cache key.\n",{"id":1698,"type":324,"difficulty":325,"subsection":1699,"tags":1700,"prompt":1701,"answer":1702,"section":1691,"sectionTitle":203},"memory-leaks-003","three-lifetimes",[640,328],"Walk me through the three lifetimes in a server-rendered Nuxt app and say what lives in each.\n","**Process** — the Node server, hours to days. Module-scope variables in the server bundle, Nitro plugins, global caches, database pools, and anything registered on a process-lifetime emitter: `process.on`, timers, client library listeners. Owner: the process. Nothing here is collected while the server runs.\n\n**Request** — milliseconds. The h3 `event` and `event.context`, and a completely fresh `nuxtApp` per SSR request: a new Vue app, a new run of every universal plugin, a new `setup()` for every component, the `useState` store and payload that will be serialised into this response, unhead, the per-request Pinia instance. Owner: the request; all of it should be unreachable once the response is flushed.\n\n**Client session** — a browser tab, minutes to hours. Exactly one `nuxtApp`, its payload and async-data cache, its plugin state and router; components mount and unmount underneath it on every navigation. Owner: the tab.\n\nThe catch is that a universal plugin runs in two of those at once with identical source, which is why `setInterval` there is a leak and in `.client.ts` is fine.\n",{"id":1704,"type":338,"difficulty":347,"subsection":1699,"tags":1705,"prompt":1706,"options":1707,"correct":1712,"answer":1713,"section":1691,"sectionTitle":203},"memory-leaks-004",[640,639],"A layer ships `app\u002Fplugins\u002Ferror-report.server.ts` whose body calls `process.on('unhandledRejection', report)`. After the server has handled 500 SSR requests, how many listeners are attached to that event?\n",[1708,1709,1710,1711],"One — a `.server.ts` plugin body runs once when the Nitro server boots","One — Node de-duplicates identical listener functions registered on the same event","500 — the plugin body runs for every SSR request, so each request adds a listener","Zero — `process` is stubbed in the Nitro runtime, so the call is a no-op",[347],"The `.server` suffix means \"not shipped to the browser\", not \"runs once\". Every Nuxt plugin, server-only or universal, runs inside the fresh `nuxtApp` that Nuxt builds for each SSR request, so the body executes 500 times and adds 500 closures to a process-lifetime emitter. Node starts warning at eleven with `MaxListenersExceededWarning`, which is the free early signal. Process-lifetime setup belongs in `server\u002Fplugins\u002F*` with `defineNitroPlugin`, which genuinely runs once when Nitro starts.\n",{"id":1715,"type":324,"difficulty":476,"subsection":1699,"tags":1716,"prompt":1718,"answer":1719,"docs":1720,"section":1691,"sectionTitle":203},"memory-leaks-005",[640,1717],"state","Why is `useState('user')` safe on the server when a module-scope `const user = ref(null)` is not? And how do you decide where any given value belongs?\n","`useState` is not a global. It is a key into `nuxtApp.payload.state`, and there is **one `nuxtApp` per SSR request**, so the ref it returns is created fresh for every request and serialised into that request's payload only. A module-scope `ref()` is created once when the server bundle is imported, so every request on that process shares the same object: request two sees request one's user, and the ref pins whatever it holds for the life of the process. Same reactive primitive, completely different lifetime, purely because of where it was created.\n\nThe decision procedure is one question: **who should still hold this after the response is sent?** Nobody → a local, or `event.context` if it must cross handlers in the same request. This app instance → `useState`, `nuxtApp.provide`, or a value returned from the plugin. The whole process, deliberately → a cache with an explicit key, a bound and a TTL, documented in the README. There is no fourth answer, and \"module scope\" is not one of the three.\n\nThe mirror-image trap is the client, where one `nuxtApp` lives for the whole tab, so a `useState` array that a route pushes into on every visit grows all session.\n",[1721],{"title":1499,"url":1500},{"id":1723,"type":324,"difficulty":347,"subsection":1724,"tags":1725,"prompt":1726,"answer":1727,"section":1691,"sectionTitle":203},"memory-leaks-006","server-side-leaks",[640,639],"Name three Nuxt-specific server-side leak patterns and the fix for each.\n","**Module-scope mutable state in runtime code** — `const seen = []` or `let currentUser` at the top of a plugin, composable or server util. The plugin body runs per SSR request, so the array retains every request's objects and the `let` hands request one's data to request two. It moves to `useState`, `nuxtApp.provide` or `event.context`.\n\n**Timers started during SSR** — `setInterval` in a universal plugin creates one interval per request that nothing ever clears, and each one pins its closure. Rename the file to `.client.ts` so it never enters the server bundle, or run a single interval from a Nitro plugin with a `close` handler.\n\n**Unbounded cache keys** — a hand-rolled `Map` keyed by full request URL, or a `defineCachedEventHandler` whose default key includes arbitrary query params, so a crawler adding `?t=…` makes every request its own cache entry. Fix with a `getKey` that normalises to a small key space, a `maxAge` and `staleMaxAge`, and external storage in production.\n\nA fourth worth naming: listeners registered per request on process-lifetime emitters (`nitroApp.hooks.hook` inside a handler, `process.on`, a db client's `on`), whose tell is `MaxListenersExceededWarning`.\n",{"id":1729,"type":351,"difficulty":325,"subsection":1724,"language":353,"tags":1730,"prompt":1731,"starter":1732,"solution":1733,"answer":1734,"section":1691,"sectionTitle":203},"memory-leaks-007",[640,415],"This plugin ships in a layer used by ten internal apps. It leaks on every SSR request and also renders one user's data into another user's HTML. Rewrite it so each value lives in the lifetime that should own it.\n","\u002F\u002F app\u002Fplugins\u002Faudit.ts\nconst seen: unknown[] = []\nlet lastUser: { id: string, name: string } | null = null\n\nexport default defineNuxtPlugin(async (nuxtApp) => {\n  seen.push(nuxtApp)\n  lastUser = await $fetch\u003C{ id: string, name: string }>('\u002Fapi\u002Fme')\n  nuxtApp.provide('lastUser', lastUser)\n})\n","\u002F\u002F app\u002Fplugins\u002Faudit.ts\nexport default defineNuxtPlugin(async (nuxtApp) => {\n  \u002F\u002F request lifetime: created fresh per SSR request, serialised into THIS response's payload\n  const user = useState\u003C{ id: string, name: string } | null>('audit:user', () => null)\n  user.value = await $fetch\u003C{ id: string, name: string }>('\u002Fapi\u002Fme')\n\n  if (import.meta.server) {\n    \u002F\u002F per-request bookkeeping belongs on the event, which dies with the response\n    const event = useRequestEvent()\n    if (event) event.context.audit = { renderedAt: Date.now() }\n  }\n\n  \u002F\u002F nothing at module scope, nothing retained after the response\n  return { provide: { user } }\n})\n","Two separate bugs with one cause. `seen` is a process-lifetime array holding a reference to every request's `nuxtApp`, so every Vue app, component tree and payload ever rendered stays reachable — the heap grows linearly with traffic. `lastUser` is worse: it is written by one request and read by the next, so under any concurrency a user sees someone else's name. `useState` fixes both because its store lives on the per-request `nuxtApp`; `event.context` is the equivalent home for server-only data.\n\nWhat to say out loud: the general rule for a layer is **no module-scope mutable state in runtime code at all**, because a consumer cannot see or patch it, and `let` plus SSR is a data-disclosure bug, not just a memory one.\n",{"id":1736,"type":351,"difficulty":347,"subsection":1724,"language":353,"tags":1737,"prompt":1738,"starter":1739,"solution":1740,"answer":1741,"section":1691,"sectionTitle":203},"memory-leaks-008",[640,639],"This feature-flag plugin adds one timer per SSR request on the server. Fix it so the flags are still seeded during SSR and still refresh every 30 seconds in the browser, but no timer is ever created on the server.\n","\u002F\u002F app\u002Fplugins\u002Fflags.ts\nexport default defineNuxtPlugin(async () => {\n  const flags = useState\u003CRecord\u003Cstring, boolean>>('team:flags', () => ({}))\n  flags.value = await $fetch('\u002Fapi\u002Fflags')\n\n  \u002F\u002F keep them fresh\n  setInterval(async () => {\n    flags.value = await $fetch('\u002Fapi\u002Fflags')\n  }, 30_000)\n})\n","\u002F\u002F app\u002Fplugins\u002Fflags.ts — seeding only. Runs per SSR request and once per tab, creates nothing.\nexport default defineNuxtPlugin(async () => {\n  const flags = useState\u003CRecord\u003Cstring, boolean>>('team:flags', () => ({}))\n  flags.value = await $fetch('\u002Fapi\u002Fflags')\n})\n\n\u002F\u002F app\u002Fplugins\u002Fflags.client.ts — the timer never enters the server bundle at all.\nimport { effectScope } from 'vue'\n\nexport default defineNuxtPlugin(() => {\n  const flags = useState\u003CRecord\u003Cstring, boolean>>('team:flags')\n\n  const scope = effectScope(true)\n  scope.run(() => {\n    \u002F\u002F VueUse registers its cleanup on the active scope, so stopping the scope clears the timer\n    useIntervalFn(async () => { flags.value = await $fetch('\u002Fapi\u002Fflags') }, 30_000)\n  })\n\n  \u002F\u002F one teardown for everything the plugin started (HMR would otherwise stack timers in dev)\n  if (import.meta.hot) import.meta.hot.dispose(() => scope.stop())\n})\n","The plugin body runs once per SSR request, so `setInterval` creates one interval per request and nothing ever clears it: the timers accumulate, each pinning its closure and the `useState` ref it captured, and `process.getActiveResourcesInfo()` fills with `Timeout` entries. Splitting the file is the honest fix — the `.client.ts` half is tree-shaken out of the server build entirely, so there is no timer to leak, while the universal half still seeds the payload so the first render has flags.\n\nAn `if (import.meta.client)` guard inside one plugin also works (Nuxt replaces the flag with `false` and tree-shakes the branch), but the two-file split states the intent in the filename, which matters more in a layer. Mention the `effectScope` and `import.meta.hot.dispose`: without them, `nuxt dev` stacks a new interval on every HMR update.\n",{"id":1743,"type":351,"difficulty":476,"subsection":1724,"language":353,"tags":1744,"prompt":1745,"starter":1746,"solution":1747,"answer":1748,"section":1691,"sectionTitle":203},"memory-leaks-009",[639,640],"Under load this endpoint prints `MaxListenersExceededWarning` and the heap never comes back down. Explain what is happening by fixing it — keep the same timing metric.\n","\u002F\u002F server\u002Fapi\u002Forders.get.ts\nexport default defineEventHandler(async (event) => {\n  const start = Date.now()\n  const nitroApp = useNitroApp()\n\n  nitroApp.hooks.hook('afterResponse', () => {\n    metrics.timing('orders', Date.now() - start, { path: event.path })\n  })\n\n  return await listOrders(event)\n})\n","\u002F\u002F server\u002Fplugins\u002Forder-metrics.ts — process lifetime: this body runs once, when Nitro starts\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('request', (event) => {\n    \u002F\u002F per-request data goes on the event, not in a closure and not in a Map\n    event.context.startedAt = Date.now()\n  })\n\n  nitroApp.hooks.hook('afterResponse', (event) => {\n    if (!event.path.startsWith('\u002Fapi\u002Forders')) return\n    const startedAt = event.context.startedAt as number | undefined\n    if (startedAt) metrics.timing('orders', Date.now() - startedAt, { path: event.path })\n  })\n})\n\n\u002F\u002F server\u002Fapi\u002Forders.get.ts — back to doing one job\nexport default defineEventHandler(event => listOrders(event))\n","`nitroApp` is a process-lifetime object, so `hooks.hook()` inside a request handler registers a new listener on **every request**, and each closure captures that request's `event` and `start`. Listener count and retained heap both grow linearly with traffic, and Node warns once the emitter passes ten listeners — that warning is the diagnosis, not noise. Registering once in a Nitro plugin and carrying the per-request value on `event.context` keeps the metric identical with a constant number of listeners.\n\nWorth adding: `hook()` returns an unsubscribe function, so if you genuinely must register inside a request path, capture it and call it in a `finally`. And do not silence the warning with `setMaxListeners(0)` — that removes the only free leak detector you have.\n",{"id":1750,"type":351,"difficulty":347,"subsection":1724,"language":353,"tags":1751,"prompt":1752,"starter":1753,"solution":1754,"answer":1755,"section":1691,"sectionTitle":203},"memory-leaks-010",[639,1282],"This server util caches geo lookups in a `Map` keyed by the full request URL. Memory grows with traffic rather than with the number of distinct IPs, and a restart loses the whole cache. Rewrite it with a bounded, deliberately keyed cache.\n","\u002F\u002F server\u002Futils\u002Fgeo.ts\nconst cache = new Map\u003Cstring, GeoResult>()\n\nexport async function lookup(event: H3Event): Promise\u003CGeoResult> {\n  const key = getRequestURL(event).toString()\n  if (!cache.has(key)) {\n    cache.set(key, await $fetch\u003CGeoResult>('https:\u002F\u002Fgeo.internal\u002Flookup', { query: getQuery(event) }))\n  }\n  return cache.get(key)!\n}\n","\u002F\u002F server\u002Futils\u002Fgeo.ts\nexport const lookupIp = defineCachedFunction(\n  \u002F\u002F only a primitive crosses the boundary: never `event`, `nuxtApp` or anything request-shaped\n  async (ip: string) => $fetch\u003CGeoResult>('https:\u002F\u002Fgeo.internal\u002Flookup', { query: { ip } }),\n  {\n    name: 'geo',\n    group: 'team-layer',\n    maxAge: 60 * 60,        \u002F\u002F fresh for an hour\n    staleMaxAge: 60 * 60 * 24, \u002F\u002F served stale for a day while revalidating\n    \u002F\u002F the key space is exactly \"one entry per IP\", and I can say that in one sentence\n    getKey: (ip: string) => ip,\n  },\n)\n\nexport function lookup(event: H3Event): Promise\u003CGeoResult> {\n  return lookupIp(getRequestIP(event, { xForwardedFor: true }) ?? 'unknown')\n}\n","Two problems. The key is the full URL, so every distinct query string — a cache-buster, a tracking parameter, a crawler's pagination — creates a new entry for the same underlying answer, and the `Map` has no `max` and no TTL, so nothing is ever evicted. Moving to `defineCachedFunction` gives a TTL, stale-while-revalidate, and a storage layer you can point at Redis in production so the memory is not in your process and survives a restart.\n\nThe senior points: the cache key is the only thing that can grow, so normalise it to a small, describable space; pass primitives into a cached function so no entry retains a whole request; and if an in-process cache really is right, use `lru-cache` with an explicit `max` and `ttl` rather than a bare `Map`.\n",{"id":1757,"type":338,"difficulty":347,"subsection":1724,"tags":1758,"prompt":1760,"options":1761,"correct":1766,"answer":1767,"section":1691,"sectionTitle":203},"memory-leaks-011",[639,1759],"diagnosis","Your Nitro server logs `MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 afterResponse listeners added`. What is the correct first move?\n",[1762,1763,1764,1765],"Nitro's default limit is too low for production traffic; raise it with `setMaxListeners(0)` in a Nitro plugin","Find the `hooks.hook()` call that runs inside a request path and move it into a Nitro plugin so it registers once","The event loop is saturated and hooks are queueing; scale horizontally and add a circuit breaker","A consumer installed the same module twice at build time; deduplicate it in the `modules` array",[325],"Listener counts are static in a healthy Nitro app — hooks are registered at boot, in Nitro plugins, not per request. A count that crosses ten under traffic means registration is happening inside a request path, and every one of those closures retains its `event`, so the warning is an early, free leak detector. Raising the limit deletes the signal and keeps the leak. The build-time duplicate-module answer is tempting because it also produces duplicate hooks, but it would produce a fixed small number at boot, not a count that climbs with requests.\n",{"id":1769,"type":351,"difficulty":325,"subsection":1770,"language":795,"tags":1771,"prompt":1772,"starter":1773,"solution":1774,"answer":1775,"docs":1776,"section":1691,"sectionTitle":203},"memory-leaks-012","client-side-leaks",[795,370],"Navigating away from and back to this component 20 times leaves 20 extra `resize` listeners on `window` and 20 detached DOM subtrees. Fix it. The component ships in a layer, so the fix must not depend on a consumer remembering anything.\n","\u003Cscript setup lang=\"ts\">\nconst width = ref(0)\n\nonMounted(() => {\n  width.value = window.innerWidth\n  window.addEventListener('resize', () => {\n    width.value = window.innerWidth\n  })\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"viewport-badge\">{{ width }}px\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\nconst width = ref(0)\n\n\u002F\u002F useEventListener registers its own teardown on the active scope,\n\u002F\u002F so the listener is removed when the component (or any owning scope) stops\nuseEventListener(window, 'resize', () => {\n  width.value = window.innerWidth\n})\n\nonMounted(() => {\n  width.value = window.innerWidth\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"viewport-badge\">{{ width }}px\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","`window` outlives the component, so the listener keeps the closure alive, the closure keeps `width` alive, and through the effect the component instance and its detached DOM stay reachable. Twenty navigations, twenty retained component trees — visible as ratcheting \"Event listeners\" and \"DOM nodes\" in the Performance Monitor and as `Detached` entries in a heap snapshot.\n\n`useEventListener` is the right fix for a layer because the cleanup is inside the composable: a consumer cannot forget it. Without VueUse, `window.addEventListener('resize', fn, { signal })` with an `AbortController` aborted in `onScopeDispose` does the same job and retires several listeners at once. Prefer `onScopeDispose` over `onUnmounted` in shipped code, because `onUnmounted` never fires for a component that errored before mounting or for a composable called from a plugin.\n",[1777],{"title":1778,"url":1779},"VueUse — useEventListener","https:\u002F\u002Fvueuse.org",{"id":1781,"type":351,"difficulty":476,"subsection":1770,"language":353,"tags":1782,"prompt":1783,"starter":1784,"solution":1785,"answer":1786,"docs":1787,"section":1691,"sectionTitle":203},"memory-leaks-013",[795,640],"This component creates one watcher per mount that is never stopped, even though it looks like ordinary setup code. Explain the mechanism by fixing it, and keep the `await` where the data is genuinely needed before render.\n","\u002F\u002F app\u002Fcomponents\u002FPriceTicker.vue — options API, plain async setup()\nexport default defineComponent({\n  async setup() {\n    const symbol = useState\u003Cstring>('ticker:symbol', () => 'EURUSD')\n    const { data } = await useAsyncData('ticker', () => $fetch('\u002Fapi\u002Fticker'))\n\n    watch(symbol, s => track('symbol', s))\n\n    return { data }\n  },\n})\n","\u002F\u002F app\u002Fcomponents\u002FPriceTicker.vue\nexport default defineComponent({\n  async setup() {\n    const symbol = useState\u003Cstring>('ticker:symbol', () => 'EURUSD')\n\n    \u002F\u002F create every effect synchronously, BEFORE the first await, while the\n    \u002F\u002F component's scope is still the active one — now it stops on unmount\n    watch(symbol, s => track('symbol', s))\n\n    const { data } = await useAsyncData('ticker', () => $fetch('\u002Fapi\u002Fticker'))\n    return { data }\n  },\n})\n","`watch`, `watchEffect` and `computed` register themselves on the **active effect scope**, which Vue sets while `setup()` runs synchronously. In a plain `async setup()` the active instance and scope are gone after the first `await`, so the watcher is created with no owner: it subscribes to `symbol`, which is a `useState` ref living on the tab's `nuxtApp`, and that subscription keeps the callback — and the component instance it closes over — alive forever. One more per mount.\n\nTwo things to add out loud. First, `\u003Cscript setup>` does not have this problem for top-level `await`, because the compiler wraps it in `withAsyncContext`, which restores the instance and its scope afterwards; the trap is plain `setup()` and any inner `async` helper you call without awaiting. Second, when you genuinely must create an effect after an await, own it yourself with `effectScope()` plus `onScopeDispose(() => scope.stop())`.\n",[1788],{"title":1789,"url":1790},"Vue — watchers","https:\u002F\u002Fvuejs.org\u002Fguide\u002Fessentials\u002Fwatchers.html",{"id":1792,"type":338,"difficulty":347,"subsection":1770,"tags":1793,"prompt":1794,"options":1795,"correct":1800,"answer":1801,"docs":1802,"section":1691,"sectionTitle":203},"memory-leaks-014",[1007,1282],"In Nuxt 4.5, what happens to the data cached under a `useAsyncData` key once no mounted component is using that key any more?\n",[1796,1797,1798,1799],"Nothing — payload data is kept for the life of the tab so a later call with the same key resolves instantly","It is purged, because `experimental.purgeCachedData` is enabled by default — unless a custom `getCachedData` keeps handing back the stale value","It is purged only if you call `clearNuxtData()` yourself; there is no automatic eviction","It is moved into `sessionStorage` so it survives a reload and is dropped when the tab closes",[325],"Nuxt 4 purges cached async data when the last component using the key goes away; `experimental.purgeCachedData` is on by default, which is what stops `nuxtApp.payload.data` growing across a long session. The trap is a hand-written `getCachedData` that unconditionally returns the previous value — that reinstates the old behaviour and defeats the purge, so give it an expiry check. `clearNuxtData(keys)` is still the explicit eviction call for data whose lifecycle you manage yourself, for example a large per-report payload dropped in `onScopeDispose`.\n",[1803],{"title":1804,"url":1805},"Nuxt — experimental features","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fgoing-further\u002Fexperimental-features",{"id":1807,"type":324,"difficulty":347,"subsection":1770,"tags":1808,"prompt":1809,"answer":1810,"section":1691,"sectionTitle":203},"memory-leaks-015",[795,370],"An internal dashboard gets sluggish after a few hours without a reload. What are the usual suspects on the client, and how do you confirm each?\n","The suspects, roughly in order of how often they turn up: `window`\u002F`document` listeners added in `onMounted` without teardown; `setInterval` or `requestAnimationFrame` loops never cleared; `IntersectionObserver`, `ResizeObserver` or `MutationObserver` never disconnected; third-party chart, map or editor instances never `destroy()`ed, which keep whole detached DOM trees alive; effects created where there is no active scope, such as a `watch` inside `router.afterEach` or a `setTimeout`; `nuxtApp.hook` or event-bus subscriptions registered in a component without calling the returned unsubscribe; router guards registered in a component; object URLs never revoked and Workers, `BroadcastChannel`s or WebSockets never closed. Then the slow class: `useState` or Pinia arrays that only grow, and `\u003CNuxtPage keepalive>` or `\u003CKeepAlive>` without a `max`.\n\nConfirmation is one workflow, not nine. Open the Performance Monitor and navigate away and back twenty times: JS heap, **DOM nodes** and **event listeners** should all return to baseline after a forced GC. If they ratchet, take snapshots before and after, filter by `Detached` for orphaned DOM, compare constructor counts for your component names and `ReactiveEffect`, and read the Retainers to see which long-lived owner is holding them.\n",{"id":1812,"type":338,"difficulty":325,"subsection":1770,"tags":1813,"prompt":1814,"options":1815,"correct":1820,"answer":1821,"section":1691,"sectionTitle":203},"memory-leaks-016",[795,835],"A dashboard app renders `\u003CNuxtPage keepalive \u002F>` with no further options. What is the memory consequence, and what is the minimal fix?\n",[1816,1817,1818,1819],"None — `\u003CKeepAlive>` caches vnodes, which are released on the next navigation","Every page the user visits stays mounted for the life of the tab; give the prop a bound such as `max` of 6, or exclude the heavy pages","Only the previously visited page is kept, so the fix is to disable keepalive on heavy pages","Pages are kept but their reactive effects are stopped, so only DOM memory grows; the fix is `v-memo` on the heavy lists",[325],"`\u003CKeepAlive>` is a deliberate leak: it keeps component instances alive — with their DOM, their reactive effects and whatever data they fetched — so returning to a page is instant. Without `max` the cache is unbounded, so an all-day session accumulates every page the user ever visited. `\u003CNuxtPage>` passes the prop straight through to `\u003CKeepAlive>`, so `max`, `include` and `exclude` all work. Vue does not stop the effects of a deactivated component, which is why the \"only DOM memory grows\" option is wrong; deactivated components keep watching their sources.\n",{"id":1823,"type":324,"difficulty":347,"subsection":1824,"tags":1825,"prompt":1826,"answer":1827,"docs":1828,"section":1691,"sectionTitle":203},"memory-leaks-017","detecting-and-measuring",[1759,639],"A consumer reports that their pods restart with OOM a few hours after they adopted your layer. Walk me through what you check.\n","First I separate growth from leakage, because heaps legitimately grow at boot as JIT tiers up and caches fill. The signal I want is a heap that does not come back down **after a forced collection**. I get a baseline from `process.memoryUsage()` on an internal route: `heapUsed` climbing points at JS objects, while `external` and `arrayBuffers` climbing with a flat `heapUsed` points at Buffers, streams and sockets — a different hunt. I also check `process.getActiveResourcesInfo()`, because piles of `Timeout` entries diagnose the interval leak in one call, and I grep their logs for `MaxListenersExceededWarning`.\n\nThen I reproduce locally: run the built server with `node --inspect .output\u002Fserver\u002Findex.mjs`, warm it up, take snapshot A, load it with `autocannon -c 20 -d 30`, click \"Collect garbage\", take snapshot B, and read the Comparison view sorted by size delta. I look for `Array`, `Map`, `Object`, `(closure)`, `ReactiveEffect`, `ComputedRefImpl`, `EffectScope` and my own component names, then open **Retainers**, because the answer is always *who holds it*, not *what it is*. In production I use `--heapsnapshot-signal=SIGUSR2` with `kill -USR2` so no inspector port is needed. Once it is found, the fix ships with a memory watermark test so it cannot come back quietly.\n",[1829],{"title":1830,"url":1831},"Chrome DevTools — fix memory problems","https:\u002F\u002Fdeveloper.chrome.com\u002Fdocs\u002Fdevtools\u002Fmemory-problems",{"id":1833,"type":338,"difficulty":325,"subsection":1824,"tags":1834,"prompt":1835,"options":1836,"correct":1841,"answer":1842,"docs":1843,"section":1691,"sectionTitle":203},"memory-leaks-018",[1759],"Your health route shows `heapUsed` flat under sustained load while RSS climbs steadily. Which fields of `process.memoryUsage()` do you look at next, and what do they point at?\n",[1837,1838,1839,1840],"`heapTotal` — V8 has reserved more heap than it needs, so the fix is `--max-old-space-size`","`external` and `arrayBuffers` — memory held by C++ objects bound to JS, so Buffers, streams and sockets rather than plain JS objects","`rss` alone is enough: a climbing RSS with flat `heapUsed` always means a leaking native addon","None of them — `heapUsed` is the only reliable field and the others are estimates",[325],"`external` is memory used by C++ objects bound to JavaScript, and `arrayBuffers` is the subset held by `ArrayBuffer`s and `Buffer`s. Flat `heapUsed` with rising `external`\u002F`arrayBuffers` is the signature of unconsumed response bodies, unclosed sockets and streams that were never drained — none of which a JS-object heap snapshot will show you clearly. `heapTotal` is what V8 has reserved, not what is live, and `--max-old-space-size` is a mitigation that delays the OOM rather than a diagnosis.\n",[1844],{"title":1845,"url":1846},"Node — process.memoryUsage()","https:\u002F\u002Fnodejs.org\u002Fapi\u002Fprocess.html",{"id":1848,"type":324,"difficulty":476,"subsection":1824,"tags":1849,"prompt":1850,"answer":1851,"section":1691,"sectionTitle":203},"memory-leaks-019",[1759,640],"How do you tell a healthy growing heap from a leaking one, and what specifically do you look at in a heap snapshot comparison?\n","The rule is that **heaps plateau**. A freshly booted Nitro server legitimately gains tens of megabytes as V8 tiers up, lazy chunks load and caches fill, and V8 only collects when it feels pressure, so a rising line proves nothing on its own. A leak is a plateau that never arrives: after a warm-up and a forced collection, the floor is higher than it was before the load and keeps rising with each round. That is why every measurement needs two controls — warm up before the first sample, force GC before the last one.\n\nIn the snapshot comparison I sort by size delta, not by total size, because I am looking for what *grew*. The constructors that carry the signal are `Array`, `Map`, `Object`, `(closure)` — a closure count that matches the request count is the per-request-listener leak — plus `ReactiveEffect`, `ComputedRefImpl` and `EffectScope` for orphaned Vue effects, and my own component names. Then I open **Retainers** on one instance and follow the chain to its root: the retainer path is the fix, because it names the long-lived owner. On the client the same workflow runs with the `Detached` filter to find DOM that is unreachable from the document but still referenced by JS. For continuous evidence rather than snapshots, `--heap-prof` shows where the bytes are allocated and `--trace-gc` shows whether collections reclaim anything at all.\n",{"id":1853,"type":351,"difficulty":476,"subsection":1824,"language":353,"tags":1854,"prompt":1855,"starter":1856,"solution":1857,"answer":1858,"docs":1859,"section":1691,"sectionTitle":203},"memory-leaks-020",[730,639],"Write the memory watermark test a layer ships in CI: boot the built fixture server with a forced-GC probe available, load it for 30 seconds, and fail if `heapUsed` grew beyond a threshold. Assume CI has already run the fixture build and that `autocannon` and `vitest` are dev dependencies.\n","\u002F\u002F test\u002Fmemory-watermark.test.ts\nimport { spawn } from 'node:child_process'\nimport { setTimeout as sleep } from 'node:timers\u002Fpromises'\nimport autocannon from 'autocannon'\nimport { afterAll, beforeAll, expect, it } from 'vitest'\n\n\u002F\u002F TODO: start the fixture server so that a GC can be forced\n\u002F\u002F TODO: warm up, sample, load for 30 s, force GC, sample again\n\u002F\u002F TODO: assert the growth is below a calibrated threshold\n","\u002F\u002F test\u002Ffixtures\u002Fbasic\u002Fserver\u002Froutes\u002F__mem.get.ts\nexport default defineEventHandler(() => {\n  \u002F\u002F the probe exists only when the harness asks for it — never in a real deployment\n  if (process.env.NUXT_MEMORY_PROBE !== '1') throw createError({ statusCode: 404 })\n\n  globalThis.gc?.() \u002F\u002F requires the process to run with --expose-gc\n  const { heapUsed, rss, external } = process.memoryUsage()\n  return { heapUsed, rss, external, resources: process.getActiveResourcesInfo() }\n})\n\n\u002F\u002F test\u002Fmemory-watermark.test.ts\nimport { spawn } from 'node:child_process'\nimport { setTimeout as sleep } from 'node:timers\u002Fpromises'\nimport autocannon from 'autocannon'\nimport { afterAll, beforeAll, expect, it } from 'vitest'\n\nconst PORT = 3123\nconst base = `http:\u002F\u002Flocalhost:${PORT}`\nlet server: ReturnType\u003Ctypeof spawn>\n\n\u002F\u002F hitting the probe forces a collection first, so we compare floors, not peaks\nconst probe = () => fetch(`${base}\u002F__mem`).then(r => r.json() as Promise\u003C{ heapUsed: number }>)\n\nbeforeAll(async () => {\n  server = spawn('node', ['--expose-gc', 'test\u002Ffixtures\u002Fbasic\u002F.output\u002Fserver\u002Findex.mjs'], {\n    env: { ...process.env, PORT: String(PORT), NUXT_MEMORY_PROBE: '1' },\n    stdio: 'inherit',\n  })\n  for (let i = 0; i \u003C 60; i++) {\n    try { await fetch(base); break }\n    catch { await sleep(500) }\n  }\n}, 180_000)\n\nafterAll(() => { server?.kill() })\n\nit('heapUsed stays below the watermark under sustained load', async () => {\n  await autocannon({ url: base, connections: 5, duration: 5 }) \u002F\u002F warm up JIT and caches\n  const before = await probe()\n\n  await autocannon({ url: base, connections: 20, duration: 30 })\n  await sleep(1_000)\n  const after = await probe()\n\n  const grewMb = (after.heapUsed - before.heapUsed) \u002F 1024 \u002F 1024\n  console.log(`heapUsed grew ${grewMb.toFixed(1)} MB over 30 s of load`)\n  expect(grewMb).toBeLessThan(20)\n}, 180_000)\n","The three details that make this a real test rather than theatre. **`--expose-gc` plus a probe route** means both samples are taken after a forced collection, so you compare floors rather than whatever V8 had not got round to collecting. **A warm-up run before the first sample** keeps JIT tiering and lazy chunk loading out of the delta, which is otherwise tens of megabytes of false positive. And the **threshold is calibrated on a green build** and generous — this catches \"grows two megabytes per thousand requests\", not micro-regressions, so it does not flake.\n\nWhat to mention: the probe is gated on an env var so it cannot ship; a leak usually shows as growth proportional to the request count, so doubling the duration should roughly double the delta, which is a good confirmation step; and the one rule for the team is that you never fix a red watermark test by raising the threshold.\n",[1860],{"title":1861,"url":1862},"autocannon","https:\u002F\u002Fgithub.com\u002Fmcollina\u002Fautocannon",{"id":1864,"type":324,"difficulty":476,"subsection":1865,"tags":1866,"prompt":1867,"answer":1868,"section":1691,"sectionTitle":203},"memory-leaks-021","patterns-to-adopt",[370,513],"You are designing a layer that ten internal apps will consume. What rules do you put in place so it never leaks in someone else's production?\n","Six rules, all derived from one fact: my plugins run on every SSR request in an app I do not operate, and consumers cannot patch them.\n\nRequest-lifetime data lives in `event.context` on the server and `useState` or `nuxtApp` in universal code, and there is **no module-scope mutable state in runtime code at all** — no `let`, no `Map`, no top-level `ref()` — which is a grep I run in CI. Anything with a global side effect is either a `.client.ts` plugin so it never reaches the server bundle, or it is lazy and wrapped in an `effectScope()` I hold and can `stop()`. Every cache is bounded with a `maxAge` and a key I can describe in one sentence, and it uses Nitro's cache layer or unstorage rather than a hand-rolled `Map`, so production can point it at Redis. Composables register cleanup with `onScopeDispose` rather than `onUnmounted` and also return a `stop` function. Anything I subscribe to hands back an unsubscribe, and my public API passes it on. Module `setup()` that spawns a watcher, child process or server registers `nuxt.hook('close', …)`.\n\nThen two artefacts make it stick: a **memory behaviour section in the README** — for a layer that is public API — and a **PR checklist** plus the memory watermark test, with the rule that a red test is never fixed by raising the threshold.\n",{"id":1870,"type":324,"difficulty":347,"subsection":1865,"tags":1871,"prompt":1872,"answer":1873,"docs":1874,"section":1691,"sectionTitle":203},"memory-leaks-022",[795,370],"How do you make a composable leak-proof when you cannot control where consumers call it from?\n","Four habits. **Create every effect synchronously**, so the caller's active scope adopts it — a `watch` created after an `await` inside the composable has no owner even if the caller looks fine. **Register teardown with `onScopeDispose`**, not `onUnmounted`: consumers call composables from plugins, stores and detached `effectScope`s, and from components that can error before mounting, none of which fire `onUnmounted`. VueUse's `tryOnScopeDispose` is the version that degrades to a no-op instead of warning when there is no scope. **Return the stop function anyway**, because a caller with no scope still needs an escape hatch; I guard with `getCurrentScope()` and warn in dev if there is neither. And **return the unsubscribe from anything you subscribe to** — `nuxtApp.hook`, an event bus, a router guard all hand one back, and swallowing it designs a leak into the API.\n\nOn top of that: an early return under `import.meta.server` that yields the same shape while creating no socket, timer or observer, and `markRaw` plus `shallowRef` around any third-party instance so Vue never proxies its internals.\n",[1875],{"title":1876,"url":1877},"Vue — reactivity API (effectScope, onScopeDispose)","https:\u002F\u002Fvuejs.org\u002Fapi\u002Freactivity-advanced.html",{"id":1879,"type":324,"difficulty":325,"subsection":1880,"tags":1881,"prompt":1883,"answer":1884,"docs":1885,"section":835,"sectionTitle":235},"performance-001","how-to-talk-about-performance",[1882],"method","A stakeholder says \"the app feels slow\". How do you structure your investigation?\n","By naming the layer before naming a lever. \"Slow\" is at least four different problems: slow to first byte, slow to finish loading, slow to become interactive, or janky afterwards. Each has its own instrument — `curl -w` timings and `autocannon` for the server, the Network panel for the wire, Lighthouse and the Performance panel for the client, `nuxt analyze` for the bundle.\n\nSo the shape is **measure → name the layer → apply the matching lever → verify with the same measurement**, and every number comes from a built server (`nuxt build` plus `node .output\u002Fserver\u002Findex.mjs`), warmed up, repeated. Leading with a lever (\"add caching\") is the failure mode: caching a personalised route is a data-disclosure bug, and caching a slow query hides it until the cache is cold.\n",[1886],{"title":1887,"url":1888},"Core Web Vitals","https:\u002F\u002Fweb.dev\u002Farticles\u002Fvitals",{"id":1890,"type":338,"difficulty":325,"subsection":1880,"tags":1891,"prompt":1892,"options":1893,"correct":1898,"answer":1899,"section":835,"sectionTitle":235},"performance-002",[1882],"You want to compare build time before and after adding your module. Which measurement do you trust?\n",[1894,1895,1896,1897],"One `nuxt build` run of each, in dev mode, comparing the printed duration\n","`hyperfine --warmup 1 --runs 5` over both builds, comparing the distributions\n","The time shown in the Nuxt DevTools timeline\n","`time pnpm dev` until the page first renders\n",[325],"Single runs are dominated by cold caches, JIT warm-up and background load, so they routinely produce \"improvements\" that are noise. `hyperfine` warms up, repeats and reports a mean with a spread, which is what makes a delta believable. Dev-mode numbers measure a different thing entirely (unbundled, instrumented), and are only useful when dev startup itself is what you are optimising.\n",{"id":1901,"type":324,"difficulty":347,"subsection":1902,"tags":1903,"prompt":1904,"answer":1905,"docs":1906,"section":835,"sectionTitle":235},"performance-003","build-and-dev",[525,370],"Why does the dev server sometimes print \"new dependencies optimized… reloading\", and how do you stop it?\n","Vite pre-bundles dependencies so that CommonJS packages and deep import trees become a single ESM module. It discovers them by crawling from the entry, so a dependency imported only from a lazily reached file is found *late*: Vite then optimises it mid-session and reloads the page, throwing away whatever you were doing.\n\nThe fix is to declare it: `vite.optimizeDeps.include` for CommonJS dependencies your runtime imports, `exclude` for large ESM ones you do not want pre-bundled. A module should push its own dependencies into `include` from `setup`, so consumers never see the loop. The trade-off is a slightly slower first start in exchange for a stable session.\n",[1907],{"title":1908,"url":1909},"Vite dependency pre-bundling","https:\u002F\u002Fvite.dev\u002Fguide\u002Fdep-pre-bundling",{"id":1911,"type":351,"difficulty":347,"subsection":1902,"language":353,"tags":1912,"prompt":1913,"starter":1914,"solution":1915,"answer":1916,"section":835,"sectionTitle":235},"performance-004",[525],"This module's setup runs on every dev start and every build in every consuming app. Make it cheap: remove the network call from the critical path and cache the derived registry on disk under the build directory, while keeping the behaviour identical.\n","import { defineNuxtModule, addTemplate } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  async setup(options, nuxt) {\n    const schema = await $fetch('https:\u002F\u002Finternal.example.com\u002Fschema.json')\n    const registry = buildRegistry(schema)\n    addTemplate({\n      filename: 'toolkit\u002Fregistry.mjs',\n      getContents: () => `export const registry = ${JSON.stringify(registry)}`,\n    })\n  },\n})\n","import { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs\u002Fpromises'\nimport { dirname, join } from 'node:path'\nimport { addTemplate, defineNuxtModule, useLogger } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  async setup(options, nuxt) {\n    const logger = useLogger('toolkit')\n    const cacheFile = join(nuxt.options.buildDir, 'toolkit\u002Fregistry.json')\n\n    let registry: Record\u003Cstring, unknown>\n    if (existsSync(cacheFile)) {\n      registry = JSON.parse(await readFile(cacheFile, 'utf8'))\n    }\n    else {\n      logger.info('fetching toolkit schema (first build on this machine)')\n      const schema = await $fetch('https:\u002F\u002Finternal.example.com\u002Fschema.json')\n      registry = buildRegistry(schema)\n      await mkdir(dirname(cacheFile), { recursive: true })\n      await writeFile(cacheFile, JSON.stringify(registry))\n    }\n\n    addTemplate({\n      filename: 'toolkit\u002Fregistry.mjs',\n      getContents: () => `export const registry = ${JSON.stringify(registry)}`,\n    })\n  },\n})\n","The point to say out loud is that `setup` is on the critical path of every developer's every build, so network latency there is multiplied by the whole team, and it breaks builds on machines without access to that host. Caching under `nuxt.options.buildDir` keeps the artefact next to the rest of the generated output, so `nuxt cleanup` or a fresh clone re-fetches it.\n\nTwo refinements worth mentioning: give the cache a version or a TTL so a schema change is picked up, and put anything that needs the *final* `nuxt.options` in a `modules:done` hook rather than blocking setup.\n",{"id":1918,"type":324,"difficulty":347,"subsection":1919,"tags":1920,"prompt":1921,"answer":1922,"docs":1923,"section":835,"sectionTitle":235},"performance-005","server-side",[640,1420],"A page awaits three `useAsyncData` calls in setup and TTFB is 900 ms. What do you change, and what do you measure afterwards?\n","Sequential awaits serialise three round trips, so the page waits for their sum. I start all three without awaiting and `await Promise.all([...])`, which reduces the wait to the slowest one; anything not needed for the first paint gets `lazy: true` so it does not block navigation at all.\n\nThen I re-measure the same way — `curl -s -o \u002Fdev\u002Fnull -w '%{time_starttransfer}'` against a warm built server, a dozen times — and quote the delta. If the remaining time is not in data, `--cpu-prof` under `autocannon` tells me whether it is render or serialisation. The trade-off with `lazy` is that the client fetches after hydration, so the data is not in the server HTML: fine for a sidebar, wrong for the page's main content or anything that needs to be indexed.\n",[1924],{"title":1925,"url":1426},"useAsyncData",{"id":1927,"type":351,"difficulty":347,"subsection":1919,"language":795,"tags":1928,"prompt":1929,"starter":1930,"solution":1931,"answer":1932,"section":835,"sectionTitle":235},"performance-006",[640,1420,1007],"This page blocks on three sequential requests and serialises the full API response into the payload, although the template only renders each product's id, name and price. Fix both problems.\n","\u003Cscript setup lang=\"ts\">\nconst { data: user } = await useAsyncData('user', () => $fetch('\u002Fapi\u002Fuser'))\nconst { data: products } = await useAsyncData('products', () => $fetch('\u002Fapi\u002Fproducts'))\nconst { data: banner } = await useAsyncData('banner', () => $fetch('\u002Fapi\u002Fbanner'))\n\u003C\u002Fscript>\n","\u003Cscript setup lang=\"ts\">\n\u002F\u002F Critical data: start together, await once - the slowest request sets TTFB.\nconst userReq = useAsyncData('user', () => $fetch('\u002Fapi\u002Fuser'))\nconst productsReq = useAsyncData('products', () => $fetch('\u002Fapi\u002Fproducts'), {\n  \u002F\u002F transform runs on the server, so only these three fields are serialised into the payload\n  transform: list => list.map(p => ({ id: p.id, name: p.name, price: p.price })),\n})\nconst [{ data: user }, { data: products }] = await Promise.all([userReq, productsReq])\n\n\u002F\u002F Non-critical: does not block navigation or the server render.\nconst { data: banner } = await useAsyncData('banner', () => $fetch('\u002Fapi\u002Fbanner'), { lazy: true })\n\u003C\u002Fscript>\n","Two independent wins. Parallelising turns a sum of latencies into a maximum. `transform` (or `pick`) shrinks what crosses the wire: it runs once on the server, so the client pays nothing, and the payload is embedded in every SSR response, which makes it a network cost on every single request.\n\nMention the trade-offs: `lazy` data is absent from the server HTML, and `transform` means the full response is no longer available to other components sharing that key. Calls sharing a key must use consistent options, so if another component needs the untransformed data, use a different key or move the fetch into a shared composable.\n",{"id":1934,"type":338,"difficulty":347,"subsection":1919,"tags":1935,"prompt":1936,"options":1937,"correct":1942,"answer":1943,"section":835,"sectionTitle":235},"performance-007",[1282,415],"Which of these is safe to cache with a route rule of `swr: 60`?\n",[1938,1939,1940,1941],"`\u002Faccount\u002Forders`, which renders the signed-in user's order list\n","`\u002Fpricing`, which renders the same content for everyone\n","`\u002Fsearch`, which renders results from an arbitrary `?q=` query string\n","`\u002Fapi\u002Fsession`, which returns the current session from a cookie\n",[325],"`swr` caches the rendered response by route and serves it to everyone, so anything personalised (the orders page, the session endpoint) leaks one user's data to the next. `\u002Fsearch` is not a leak but is unbounded: every distinct query string becomes a cache entry, so the store grows with traffic. A page whose content is identical for all visitors is exactly the case route-level caching was designed for.\n",{"id":1945,"type":351,"difficulty":476,"subsection":1919,"language":353,"tags":1946,"prompt":1947,"solution":1948,"answer":1949,"docs":1950,"section":835,"sectionTitle":235},"performance-008",[1282,639],"Write a cached Nitro handler for an expensive report. Cache for a minute, allow stale responses for five minutes while revalidating, derive the key from only the `region` and `period` query parameters so it cannot grow unbounded, vary on the language header, and let a `preview` cookie bypass the cache entirely.\n","export default defineCachedEventHandler(\n  async (event) => {\n    const { region = 'eu', period = 'month' } = getQuery(event)\n    return await buildExpensiveReport(String(region), String(period))\n  },\n  {\n    maxAge: 60,\n    staleMaxAge: 300,\n    swr: true,\n    \u002F\u002F Only these two inputs may enter the key. Using the full URL would let any\n    \u002F\u002F query string create a new entry and grow the cache with traffic.\n    getKey: (event) => {\n      const { region = 'eu', period = 'month' } = getQuery(event)\n      return `report:${region}:${period}`\n    },\n    varies: ['accept-language'],\n    shouldBypassCache: event => !!getCookie(event, 'preview'),\n  },\n)\n","The interesting part is `getKey`. The default key includes the request path and query, so an endpoint reachable with arbitrary parameters has an unbounded key space and the cache becomes a slow memory leak. Naming the inputs explicitly bounds it.\n\nSay the rest out loud too: `varies` adds headers that change the response so the cache is not shared across languages; `shouldBypassCache` gives editors a way out; and the store behind this is unstorage, which defaults to in-process memory, so with several instances you get several caches. A shared driver (Redis, KV) is what makes a hit-rate number meaningful.\n",[1951],{"title":1952,"url":648},"server directory (cached handlers)",{"id":1954,"type":324,"difficulty":347,"subsection":1919,"tags":1955,"prompt":1956,"answer":1957,"section":835,"sectionTitle":235},"performance-009",[1007],"What exactly is \"the payload\", and why does its size matter on every request?\n","The payload is the serialised state Nuxt embeds in the SSR response so the client can hydrate from the same data the server rendered: everything in `useState`, every `useAsyncData`\u002F`useFetch` result, and `runtimeConfig.public`. It is serialised with devalue, which supports Date, Map, Set, RegExp and BigInt, and revived before the app renders.\n\nIt matters because it is *per response*: a 200 KB payload is 200 KB added to every page view, ahead of the JavaScript, on a connection you do not control. The levers are `pick`\u002F`transform` to shrink what is stored, keeping `runtimeConfig.public` tiny, and moving large static or derived data into a build-time template that is tree-shaken and cacheable as a separate file. I measure it in the Nuxt DevTools payload tab or with `curl -so \u002Fdev\u002Fnull -w '%{size_download}'`.\n",{"id":1959,"type":324,"difficulty":476,"subsection":1919,"tags":1960,"prompt":1962,"answer":1963,"section":835,"sectionTitle":235},"performance-010",[1282,1961],"ops","Your team deploys three instances behind a load balancer, and the cache hit rate reported by your `defineCachedEventHandler` looks much worse than in staging. Why?\n","Because the default cache store is in-process memory. Three instances mean three independent caches, so a given key is populated up to three times and each instance's hit rate is roughly a third of what one instance would see. In staging with a single instance the number looked fine.\n\nThe fix is a shared unstorage driver mounted in `nitro.storage` (Redis, Cloudflare KV, whatever the platform offers) so all instances read and write the same entries. Two things to add: the cache now has network latency and a failure mode, so it needs a timeout and a fallback path, and cache keys must be versioned or purged on deploy, otherwise a released change keeps serving HTML and payloads built against the previous bundle.\n",{"id":1965,"type":351,"difficulty":347,"subsection":1919,"language":353,"tags":1966,"prompt":1967,"solution":1968,"answer":1969,"docs":1970,"section":835,"sectionTitle":235},"performance-011",[639,1290],"Write a Nitro server plugin that exposes a `Server-Timing` header with the total server duration for every request, so the split between server and network is visible in the browser's Network panel.\n","export default defineNitroPlugin((nitroApp) => {\n  \u002F\u002F Registered once at server start; the hooks then fire per request.\n  nitroApp.hooks.hook('request', (event) => {\n    event.context.startedAt = performance.now()\n  })\n\n  nitroApp.hooks.hook('beforeResponse', (event) => {\n    const started = event.context.startedAt as number | undefined\n    if (started === undefined) return\n    const total = (performance.now() - started).toFixed(1)\n    setResponseHeader(event, 'Server-Timing', `total;dur=${total}`)\n  })\n})\n","The mechanism worth naming: a Nitro plugin runs **once** when the server starts, and the hooks it registers run per request. That is why the timestamp goes on `event.context` (request lifetime) and not in a module-scope variable, which would be shared by every concurrent request and would also leak.\n\nIn a real layer this goes behind an option, because the header exposes internal timing, and you would add finer marks (`data`, `render`) around the phases you care about rather than one total.\n",[1971],{"title":647,"url":648},{"id":1973,"type":324,"difficulty":347,"subsection":1974,"tags":1975,"prompt":1977,"answer":1978,"docs":1979,"section":835,"sectionTitle":235},"performance-012","client-side",[1474,1976],"inp","What do lazy hydration strategies buy you, and what do they not change?\n","They move hydration CPU off the critical path. `\u003CLazyTeamTestimonials hydrate-on-visible \u002F>` still renders on the server, so the HTML, the CSS and the data are all in the response; what is deferred is Vue attaching to that subtree and executing its setup. For below-the-fold or rarely used components that is a direct reduction in total blocking time and therefore in INP.\n\nWhat it does **not** change: the markup is still there (good for SEO and LCP, so this is not a way to slim the HTML), the data was still fetched, and the component's JavaScript is still downloaded as a chunk. If the goal is to stop shipping the JavaScript at all, the tool is a server component or island; if the goal is to stop fetching, that is a conditional fetch. Strategies available: visible, idle, interaction, media-query, after, when, never.\n",[1980],{"title":1981,"url":804},"components directory (lazy hydration)",{"id":1983,"type":338,"difficulty":347,"subsection":1974,"tags":1984,"prompt":1985,"options":1986,"correct":1991,"answer":1992,"section":835,"sectionTitle":235},"performance-013",[1336],"Which of these puts code in the entry chunk of *every* page?\n",[1987,1988,1989,1990],"A component in `app\u002Fcomponents\u002F` used by one page\n","A universal plugin with a top-level `import Chart from 'chart.js'`\n","A page in `app\u002Fpages\u002Freports.vue` importing a heavy library\n","A component registered with `addComponent({ mode: 'client' })`\n",[325],"Plugins are part of the app entry, so anything imported at their top level is in the entry chunk of every page, even pages that never use it. Components and pages are split per route by Nuxt, and a client-mode component is still a separate chunk. The fix for the plugin is to import inside a function (`await import('chart.js\u002Fauto')`), make the plugin `.client`, or drop the plugin in favour of a tree-shakeable composable.\n",{"id":1994,"type":351,"difficulty":347,"subsection":1974,"language":795,"tags":1995,"prompt":1996,"starter":1997,"solution":1998,"answer":1999,"section":835,"sectionTitle":235},"performance-014",[1336,1474],"This page eagerly hydrates a heavy testimonials carousel that sits far below the fold, and renders a static changelog that never needs interactivity. Rewrite the template so the carousel hydrates only when it scrolls into view and the changelog ships no client JavaScript at all. Note in a comment what you would rename on disk.\n","\u003Ctemplate>\n  \u003CTeamHero \u002F>\n  \u003CTeamTestimonials :items=\"testimonials\" \u002F>\n  \u003CTeamChangelog :entries=\"entries\" \u002F>\n\u003C\u002Ftemplate>\n","\u003Ctemplate>\n  \u003C!-- Above the fold: hydrate normally, it is interactive immediately. -->\n  \u003CTeamHero \u002F>\n\n  \u003C!-- Server-rendered now, hydrated when it scrolls into view: no JS on the critical path. -->\n  \u003CLazyTeamTestimonials :items=\"testimonials\" hydrate-on-visible \u002F>\n\n  \u003C!-- Static, never interactive: rename the file to\n       app\u002Fcomponents\u002FTeam\u002FChangelog.server.vue so it becomes a server component\n       and its JavaScript never reaches the client. -->\n  \u003CTeamChangelog :entries=\"entries\" \u002F>\n\u003C\u002Ftemplate>\n","Two different tools for two different problems. `Lazy` plus a hydration strategy keeps the component fully interactive but defers the cost; the markup is still server-rendered, so nothing is lost for SEO or LCP. A server component removes the JavaScript entirely, which is the bigger win but only legal when the component truly has no interactivity, since it cannot have client-side state or event handlers (children marked `nuxt-client` are the escape hatch).\n\nMeasure the difference with `nuxt analyze` for the chunk and Lighthouse TBT\u002FINP for the effect.\n",{"id":2001,"type":324,"difficulty":347,"subsection":1974,"tags":2002,"prompt":2005,"answer":2006,"docs":2007,"section":835,"sectionTitle":235},"performance-015",[2003,2004],"vitals","images","Which levers move LCP and which move CLS, and why is the LCP image treated differently from every other image?\n","LCP is dominated by how quickly the largest element's resource arrives: server response time, then the image itself. So the levers are TTFB (caching, prerendering), and for the image, `preload` plus `fetchpriority=\"high\"` with a correctly sized modern format through `@nuxt\u002Fimage`. Every *other* image gets `loading=\"lazy\"` precisely so it does not compete for bandwidth with the LCP one — which is why applying lazy loading blindly to all images usually makes LCP worse.\n\nCLS is caused by content moving after paint: images and embeds without dimensions, web fonts swapping with different metrics, content injected above existing content, and hydration mismatches that make Vue patch the DOM. The fixes are explicit `width`\u002F`height` or aspect ratios, `@nuxt\u002Ffonts` with fallback metric overrides, reserving space for anything injected, and eliminating mismatches ([hydration](\u002Fstudy\u002Fhydration)).\n",[2008],{"title":1887,"url":1888},{"id":2010,"type":351,"difficulty":476,"subsection":1974,"language":795,"tags":2011,"prompt":2014,"starter":2015,"solution":2016,"answer":2017,"section":835,"sectionTitle":235},"performance-016",[2012,2013],"reactivity","third-party","This component makes Vue proxy a ten-thousand-row dataset and a third-party chart instance, and it never releases the instance. Fix the reactivity cost and the cleanup.\n","\u003Cscript setup lang=\"ts\">\nimport Chart from 'chart.js\u002Fauto'\n\nconst rows = ref\u003C{ x: number, y: number }[]>([])\nconst chart = ref\u003CChart>()\nconst el = ref\u003CHTMLCanvasElement>()\n\nonMounted(async () => {\n  rows.value = await $fetch('\u002Fapi\u002Fseries')\n  chart.value = new Chart(el.value!, { type: 'line', data: { datasets: [{ data: rows.value }] } })\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Ccanvas ref=\"el\" \u002F>\n\u003C\u002Ftemplate>\n","\u003Cscript setup lang=\"ts\">\n\u002F\u002F Import the library lazily so it is not in this page's initial chunk.\nconst rows = shallowRef\u003C{ x: number, y: number }[]>([])          \u002F\u002F no deep proxying of 10k rows\nconst chart = shallowRef\u003Cimport('chart.js').Chart>()\nconst el = useTemplateRef\u003CHTMLCanvasElement>('canvas')\n\nonMounted(async () => {\n  const Chart = (await import('chart.js\u002Fauto')).default\n  rows.value = await $fetch('\u002Fapi\u002Fseries')\n  \u002F\u002F markRaw: Vue must never proxy a third-party instance that holds DOM and internal caches.\n  chart.value = markRaw(new Chart(el.value!, {\n    type: 'line',\n    data: { datasets: [{ data: rows.value }] },\n  }))\n})\n\nonBeforeUnmount(() => {\n  chart.value?.destroy()   \u002F\u002F release canvas, listeners and internal timers\n  chart.value = undefined\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Ccanvas ref=\"canvas\" \u002F>\n\u003C\u002Ftemplate>\n","`ref` on a large array creates a reactive proxy for every object in it, which costs memory and CPU on every access; `shallowRef` tracks only the reference, which is all a chart needs since you replace the whole array. `markRaw` stops Vue from proxying the chart instance, which otherwise wraps its internal state and its DOM references.\n\nThe `destroy()` in `onBeforeUnmount` is the leak fix: without it the instance keeps listeners and detached DOM alive for the whole session, and navigating back and forth accumulates them — exactly the pattern in [memory leaks](\u002Fstudy\u002Fmemory-leaks). Also worth saying: the dynamic import keeps the library out of the page's initial chunk.\n",{"id":2019,"type":324,"difficulty":347,"subsection":1974,"tags":2020,"prompt":2022,"answer":2023,"docs":2024,"section":835,"sectionTitle":235},"performance-017",[2021],"navigation","How does `NuxtLink` prefetching work, and when would you turn it off?\n","`NuxtLink` prefetches the route's JavaScript chunk when the link enters the viewport (on browsers and connections where that is reasonable), so a click navigates without waiting for a download. `prefetchOn` chooses the trigger (viewport or interaction) and `prefetch: false` disables it.\n\nI turn it off when the target is heavy and rarely visited: prefetching spends the user's bandwidth and the device's CPU on a guess, and a page full of links to large routes can download more than the page itself. On mobile data or a link-dense page (a search results list, a table of every product), switching to `prefetchOn: 'interaction'` is usually the better trade.\n",[2025],{"title":2026,"url":2027},"NuxtLink","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fapi\u002Fcomponents\u002Fnuxt-link",{"id":2029,"type":324,"difficulty":476,"subsection":2030,"tags":2031,"prompt":2033,"answer":2034,"docs":2035,"section":835,"sectionTitle":235},"performance-018","author-checklist",[335,525,2032],"budgets","How do you stop a shared layer from slowly making twenty apps heavier?\n","By turning its cost into a number and putting that number in CI. Two fixtures — an empty Nuxt app and the same app extending the layer — give the client-bundle delta (`size-limit` or a `nuxt analyze` diff) and the build-time delta (`hyperfine`). Both become budgets that fail a pull request when they regress past a threshold I chose deliberately.\n\nThe design rules follow from the measurement: tree-shakeable composables instead of plugins, because a plugin runs on every request and page load whether or not it is used; `.client` and `.server` splits so nothing lands in a bundle where it cannot run; no `global: true` components and no unconditional global CSS; a tiny `runtimeConfig.public`, since it is serialised into every SSR response of every app; and opt-in features that register nothing when the option is off. Without budgets those rules erode one convenient exception at a time.\n",[2036],{"title":2037,"url":2038},"size-limit","https:\u002F\u002Fgithub.com\u002Fai\u002Fsize-limit",{"id":2040,"type":338,"difficulty":347,"subsection":2030,"tags":2041,"prompt":2042,"options":2043,"correct":2048,"answer":2049,"section":835,"sectionTitle":235},"performance-019",[525],"Your module exposes a helper that most consumers use on a few pages. Which delivery mechanism has the lowest cost for apps that do not use it?\n",[2044,2045,2046,2047],"A universal plugin providing `$teamHelper` through `provide`\n","An auto-imported composable registered with `addImports`\n","A global component registered with `global: true`\n","A Nitro plugin that attaches it to `event.context`\n",[325],"An auto-import is rewritten into an explicit import at build time, so a page that never calls it never pulls it into a chunk — the cost for non-users is zero. A plugin runs on every SSR request and every page load and its imports live in the entry chunk; a global component is bundled into the entry chunk of every page; a Nitro plugin runs on every server request. Plugins are the right answer only when you genuinely need app-wide setup, such as installing a Vue plugin or wiring hooks.\n",{"id":2051,"type":324,"difficulty":476,"subsection":326,"tags":2052,"prompt":2053,"answer":2054,"section":835,"sectionTitle":235},"performance-020",[1882,640],"A page is fast in production for most users but consistently slow for a subset. How do you investigate?\n","\"A subset\" points away from the levers that affect everyone and towards inputs that differ per user: cache misses (their route or locale is not the cached variant), personalised rendering that cannot be cached at all, geography and therefore origin distance, device class for anything CPU-bound like hydration, and third-party scripts that only some cohorts load.\n\nI would look at field data first, split by dimension — CrUX or a real-user-monitoring plugin reporting LCP and INP with country, device and connection — because a lab Lighthouse run on a fast laptop cannot see any of this. Then reproduce: throttle CPU and network in DevTools, request a personalised route while watching cache headers, and compare a cached versus uncached response with `curl -w`. The fix depends on which dimension lit up: cache key design, edge rendering, less hydration on low-end devices, or deferring the third-party script.\n",{"id":2056,"type":324,"difficulty":325,"subsection":326,"tags":2057,"prompt":2058,"answer":2059,"section":835,"sectionTitle":235},"performance-021",[1882],"Why should performance numbers never be taken from `nuxt dev`?\n","Because dev mode measures a different program. Modules are unbundled and served individually, source maps and HMR instrumentation are attached, nothing is minified, caching is disabled and the server re-renders more aggressively. A number from dev tells you almost nothing about production, and improvements measured there routinely disappear or reverse after a build.\n\nThe exception is the thing dev mode *is*: startup time and HMR latency, which are real costs your teammates pay all day and worth optimising on their own terms. Everything else comes from `nuxt build` plus the built server, warmed up, repeated.\n",{"id":2061,"type":351,"difficulty":347,"subsection":2030,"language":441,"tags":2062,"prompt":2063,"solution":2064,"answer":2065,"section":835,"sectionTitle":235},"performance-022",[414,2032],"Write the commands a CI job runs to report what your layer costs a consuming app: the client bundle size of an empty fixture versus one that extends the layer, and the build-time difference between them.\n","# Two fixtures: test\u002Ffixtures\u002Fempty (bare Nuxt app) and test\u002Ffixtures\u002Fbasic (extends: ['..\u002F..\u002F..'])\npnpm nuxt build test\u002Ffixtures\u002Fempty\npnpm nuxt build test\u002Ffixtures\u002Fbasic\n\n# Client bundle delta\ndu -sh test\u002Ffixtures\u002Fempty\u002F.output\u002Fpublic\u002F_nuxt\ndu -sh test\u002Ffixtures\u002Fbasic\u002F.output\u002Fpublic\u002F_nuxt\n\n# Enforce the budget (thresholds declared in package.json under \"size-limit\")\nnpx size-limit\n\n# Build-time delta, warmed up and repeated so the numbers are comparable\nnpx hyperfine --warmup 1 --runs 3 \\\n  'pnpm nuxt build test\u002Ffixtures\u002Fempty' \\\n  'pnpm nuxt build test\u002Ffixtures\u002Fbasic'\n","The habit being tested is comparing against a baseline rather than quoting an absolute. An app that extends the layer will always be bigger; what matters is by how much, and whether that number moved this week.\n\nWorth adding out loud: `size-limit` turns the bundle number into a failing check rather than a report nobody reads, and both fixtures should also be built with the layer's features switched *off*, to prove that an opt-in feature really registers nothing when it is disabled.\n",{"id":2067,"type":324,"difficulty":476,"subsection":2068,"tags":2069,"prompt":2070,"answer":2071,"docs":2072,"section":2074,"sectionTitle":267},"senior-craft-001","designing-the-api",[513,525],"Design the options API for an internal analytics module that every app in the company will install. Walk me through your choices.\n","I start from the surface, not the implementation. Options flat and typed in `ModuleOptions`, grouped into objects rather than parallel booleans (`analytics: { enabled, sampleRate, endpoint }` instead of `analyticsEnabled` plus `analyticsSampleRate`), each one defaulted in `defaults` and documented in a README table. An `enabled` flag so an app can switch the module off without removing it from the preset.\n\nSecrets never go in options: the write key lives in private `runtimeConfig` so each deployment supplies `NUXT_ANALYTICS_KEY`, while the public endpoint goes in `runtimeConfig.public`. Validation runs in `setup` and fails with a message naming the config key and the fix. The client script loads through `useScript` with an idle trigger so the module does not cost INP on every page.\n\nThen the escape hatches, because I will not anticipate everything: a build hook so other modules can register event schemas, a runtime hook the app can use to enrich events, and an alias to the runtime directory. Finally, SSR semantics documented — what the composable returns during SSR — and a `debug` option that logs setup timings.\n",[2073],{"title":530,"url":531},"senior-craft",{"id":2076,"type":324,"difficulty":476,"subsection":2068,"tags":2077,"prompt":2078,"answer":2079,"section":2074,"sectionTitle":267},"senior-craft-002",[513,335],"What is the public surface of a design-system layer, and how do you keep it small?\n","It is everything a consumer can name and depend on: component names, composable and util names, `app.config` keys, `runtimeConfig` keys, routes and layouts, and the modules in the preset. That is the API, even though there is no options object anywhere.\n\nKeeping it small is mostly discipline plus a few mechanisms. Everything is prefixed (`TeamButton`, `useTeamAuth`, `team.*`, `\u002Fapi\u002F_team\u002F*`), which prevents collisions and makes adoption greppable. Internal components are either not registered at all or registered from a directory whose names make their status obvious. Theme customisation goes through typed `app.config` keys rather than props on every component, so there is one documented way to restyle. And the README states which parts are public, because anything undocumented that consumers discover becomes de facto public the moment someone depends on it.\n\nWhat breaks if you skip this: every rename becomes a migration, and you cannot refactor internals without breaking apps that reached into them.\n",{"id":2081,"type":338,"difficulty":347,"subsection":2068,"tags":2082,"prompt":2083,"options":2084,"correct":2089,"answer":2090,"section":2074,"sectionTitle":267},"senior-craft-003",[513],"Your layer has `compact: boolean` and product teams now want a third density. What is the best change?\n",[2085,2086,2087,2088],"Add `ultraCompact: boolean` alongside `compact`\n","Replace both with `density: 'compact' | 'comfortable' | 'spacious'`, keeping `compact` working with a deprecation warning\n","Keep `compact` and let apps override the component to get the third density\n","Add a `spacing` number so any value is possible\n",[325],"Two booleans can contradict each other (`compact: true, ultraCompact: true`) and every new variant adds another. An enum documents that the choice is exclusive and extends without breaking. Doing it *with* a deprecation shim is what makes it non-breaking today: `compact: true` keeps mapping to `density: 'compact'` for one major.\n\nForcing apps to override the component pushes your problem into twenty repositories, and an open-ended number turns a design-system decision into an infinite surface you cannot test or theme consistently.\n",{"id":2092,"type":351,"difficulty":347,"subsection":2068,"language":353,"tags":2093,"prompt":2094,"starter":2095,"solution":2096,"answer":2097,"section":2074,"sectionTitle":267},"senior-craft-004",[525,370],"Add validation to this module so a misconfigured app fails immediately with a message that names the config key and the fix, rather than failing later at runtime. Validate that `apiBase` is an absolute URL and that `sampleRate` is between 0 and 1.\n","import { defineNuxtModule } from '@nuxt\u002Fkit'\n\nexport interface ModuleOptions {\n  apiBase: string\n  analytics: { enabled: boolean, sampleRate: number }\n}\n\nexport default defineNuxtModule\u003CModuleOptions>({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  defaults: {\n    apiBase: 'https:\u002F\u002Fapi.internal',\n    analytics: { enabled: false, sampleRate: 0.1 },\n  },\n  setup(options, nuxt) {\n    \u002F\u002F TODO\n  },\n})\n","import { defineNuxtModule, useLogger } from '@nuxt\u002Fkit'\n\nexport interface ModuleOptions {\n  apiBase: string\n  analytics: { enabled: boolean, sampleRate: number }\n}\n\nexport default defineNuxtModule\u003CModuleOptions>({\n  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },\n  defaults: {\n    apiBase: 'https:\u002F\u002Fapi.internal',\n    analytics: { enabled: false, sampleRate: 0.1 },\n  },\n  setup(options, nuxt) {\n    const logger = useLogger('toolkit')\n\n    if (!\u002F^https?:\\\u002F\\\u002F\u002F.test(options.apiBase)) {\n      throw new Error(\n        `[nuxt-team-toolkit] \\`toolkit.apiBase\\` must be an absolute http(s) URL, received \"${options.apiBase}\". `\n        + 'Set it in nuxt.config.ts under `toolkit.apiBase`, or remove it to use the default.',\n      )\n    }\n\n    const { sampleRate } = options.analytics\n    if (typeof sampleRate !== 'number' || sampleRate \u003C 0 || sampleRate > 1) {\n      throw new Error(\n        `[nuxt-team-toolkit] \\`toolkit.analytics.sampleRate\\` must be a number between 0 and 1, received ${JSON.stringify(sampleRate)}.`,\n      )\n    }\n\n    if (options.analytics.enabled && sampleRate === 0) {\n      logger.warn('toolkit.analytics is enabled but sampleRate is 0, so no events will be sent.')\n    }\n  },\n})\n","The mechanism is that `setup` runs at build time, so throwing there fails the build with a message the developer sees in their terminal, instead of producing a broken runtime somewhere in production. Three things make the message good: the package name as a prefix, the exact config path (`toolkit.analytics.sampleRate`), and the received value.\n\nThe warning case matters too: a configuration that is valid but useless deserves a warning rather than an exception, because throwing on it would be a breaking change for someone who deliberately set it that way.\n",{"id":2099,"type":324,"difficulty":347,"subsection":2068,"tags":2100,"prompt":2102,"answer":2103,"section":2074,"sectionTitle":267},"senior-craft-005",[513,2101],"escape-hatch","Why should a module expose hooks and an alias even if nothing needs them yet?\n","Because the alternative when a consumer needs something you did not anticipate is a fork or a copy-paste, and both are permanent. A build hook lets other modules contribute to whatever registry you build; a runtime hook lets an app react to your lifecycle; an alias to your runtime directory lets an advanced consumer import a piece directly instead of reaching into `dist\u002F`. Each costs a few lines and removes a category of support request.\n\nThe trade-off is that an exposed hook is part of the public surface: it needs a typed signature, documentation of when it fires, and the same deprecation discipline as an option. So expose a small number deliberately rather than everything internal, and do not confuse an escape hatch with a substitute for good defaults — if everyone uses the hatch, the default is wrong.\n",{"id":2105,"type":324,"difficulty":476,"subsection":2106,"tags":2107,"prompt":2109,"answer":2110,"section":2074,"sectionTitle":267},"senior-craft-006","rollout-and-breaking-changes",[2108],"rollout","How do you roll out a breaking change to twenty apps?\n","I turn it into a migration rather than a release. First, adoption data: a script reading each app's lockfile tells me who is actually on the affected path, and if it is one or two teams this is a conversation instead of a process.\n\nThen a minor where the old path still works, delegating to the new one and warning **in development only** with a message that names the replacement and links the migration note. The note is written from the consumer's side: what to change, before and after, what breaks if they do nothing, and the deadline. If the rename touches many files I ship a codemod so each team reviews a pull request instead of doing the edit by hand. I canary it in our own app, then one unrelated app, announce the support window, and remove it in the next major only once adoption shows the old path is empty.\n\nWhat breaks if you skip the middle: removing on a date rather than on evidence converts one migration into several simultaneous incidents, usually in the apps with the least capacity to respond.\n",{"id":2112,"type":351,"difficulty":347,"subsection":2106,"language":795,"tags":2113,"prompt":2114,"solution":2115,"answer":2116,"section":2074,"sectionTitle":267},"senior-craft-007",[2108,335],"Your layer renames `\u003CTeamPrimaryButton>` to `\u003CTeamButton variant=\"primary\">`. Write the shim that keeps the old name working for one major, warns the developer without polluting production logs, and still renders the new component.\n","\u003Cscript setup lang=\"ts\">\n\u002F\u002F Deprecated in 2.4, removed in 3.0. Use \u003CTeamButton variant=\"primary\">.\nimport TeamButton from '.\u002FButton.vue'\n\n\u002F\u002F Dev-only: a console.warn here would otherwise print on every SSR request\n\u002F\u002F in every consuming app's production logs, where nobody can act on it.\nif (import.meta.dev) {\n  console.warn(\n    '[nuxt-layer-base] \u003CTeamPrimaryButton> is deprecated and will be removed in 3.0. '\n    + 'Use \u003CTeamButton variant=\"primary\">. See MIGRATION.md#2-4.',\n  )\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CTeamButton variant=\"primary\" v-bind=\"$attrs\">\n    \u003Cslot \u002F>\n  \u003C\u002FTeamButton>\n\u003C\u002Ftemplate>\n","The shim is a real component at the old path, so the consumer's build keeps resolving the old name; it forwards attributes and slots so behaviour is unchanged. The `import.meta.dev` guard is the part interviewers listen for: warnings must reach the person who can act, not the production log of an app whose team has not read the changelog yet.\n\nMention the alternative for module options — warn at build time through `useLogger`, which lands in the terminal — and that the shim is deleted in the major, together with the migration note's deadline.\n",{"id":2118,"type":338,"difficulty":347,"subsection":2106,"tags":2119,"prompt":2120,"options":2121,"correct":2126,"answer":2127,"section":2074,"sectionTitle":267},"senior-craft-008",[1207,335],"Which of these changes to a layer is **not** breaking for consumers?\n",[2122,2123,2124,2125],"Changing the default of `team.density` in the layer's `app.config`\n","Adding a new prefixed component `\u003CTeamDataTable>`\n","Removing `@nuxt\u002Fimage` from the layer's `modules` preset\n","Renaming the `runtimeConfig` key `teamApiBase` to `teamApiUrl`\n",[325],"Adding a new, prefixed component is additive: nothing an app already depends on changes, and the prefix makes a collision with an app's own component unlikely. Everything else forces consumers to edit: a changed `app.config` default silently alters rendering across their pages, removing a module from the preset breaks apps that relied on `\u003CNuxtImg>` without declaring it, and a renamed `runtimeConfig` key breaks every deployment's environment variables.\n\nThe last one is worth extra care because it fails at runtime in production, not at build time.\n",{"id":2129,"type":324,"difficulty":347,"subsection":2130,"tags":2131,"prompt":2133,"answer":2134,"docs":2135,"section":2074,"sectionTitle":267},"senior-craft-009","team-process-and-security",[677,2132],"process","Monorepo or separate repositories for a team's layers and modules, and how do consumers get updates?\n","I start in a pnpm workspace: `packages\u002Flayers\u002F*`, `packages\u002Fmodules\u002F*`, `apps\u002F*`, with apps depending on `workspace:*`. Changes to a layer hot-reload in every app, one lockfile pins everything, and a change that breaks an app is caught in the same pull request. That feedback loop is worth more than versioning while the tooling is young.\n\nThe moment consumers live in other repositories, the workspace stops helping and I publish to the internal registry. Apps then use semver ranges and Renovate opens the upgrade pull requests, which also gives me adoption data per version. Git tags via giget are the fallback when there is no registry, at the cost of tokens in CI, `install: true` for dependencies and a download cache to reason about.\n\nThe trade-off to state: the workspace makes upgrades implicit and simultaneous, the registry makes them explicit and staggered. Staggered is what you want once teams need to choose their own timing.\n",[2136],{"title":682,"url":683},{"id":2138,"type":324,"difficulty":476,"subsection":2130,"tags":2139,"prompt":2140,"answer":2141,"docs":2142,"section":2074,"sectionTitle":267},"senior-craft-010",[415],"How do you keep shared Nuxt tooling secure when it is deployed inside twenty production apps?\n","Two halves: staying current, and not being the hole myself.\n\nStaying current means upgrading on a cadence with `nuxt upgrade --dedupe`, subscribing to the GitHub security advisories for Nuxt, and keeping a CI matrix so upstream releases arrive as pull requests that pass or fail visibly. Nuxt shipped security releases during 2026, so this is a live concern rather than hygiene theatre. After upgrading I purge CDN and edge caches if we use `cache`, `swr` or `isr`, because stale HTML and payloads reference the previous bundle.\n\nNot being the hole means remembering that everything my layer ships runs in every app: server routes validate input with `readValidatedBody`\u002F`getValidatedQuery` and are namespaced, no debug endpoints or `process.memoryUsage()` output outside development, server-island props are treated as untrusted because they arrive from the client, secrets live only in private runtime config, and no personalised route is ever cached without varying on identity. Dependencies get a review at release time, since a vulnerable transitive dependency in my package is deployed everywhere at once.\n",[2143],{"title":2144,"url":2145},"Nuxt security advisories","https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt\u002Fsecurity\u002Fadvisories",{"id":2147,"type":338,"difficulty":347,"subsection":2130,"tags":2148,"prompt":2149,"options":2150,"correct":2155,"answer":2156,"section":2074,"sectionTitle":267},"senior-craft-011",[415,1282],"A layer ships `\u002Fapi\u002F_team\u002Fme`, which returns the current user from a session cookie. A consumer adds `routeRules: { '\u002Fapi\u002F_team\u002F**': { swr: 60 } }`. What is the consequence?\n",[2151,2152,2153,2154],"Nothing: Nitro automatically varies cached responses on cookies\n","One user's profile is served to other users for up to 60 seconds\n","The route stops working because cached routes cannot read cookies\n","Only the first request is slow and the rest are correctly personalised\n",[325],"Route-level caching stores the rendered response by route, not by identity. Unless the cache key or `varies` includes the identifying cookie, the first response is served to everyone who asks within the window — a cross-user data disclosure, which is exactly the class of bug the 2026 Nuxt security releases addressed in other forms.\n\nAs the layer author this is partly your responsibility: routes that must never be cached should say so in their own `routeRules` (`cache: false`) and in the README, because a consumer applying a broad glob will not read your handler.\n",{"id":2158,"type":324,"difficulty":347,"subsection":2130,"tags":2159,"prompt":2161,"answer":2162,"section":2074,"sectionTitle":267},"senior-craft-012",[2160],"support","A consumer reports \"it works in my app but not in the playground\" (or the reverse). How do you handle it?\n","First I ask for a reproduction, because that is the intake rule that keeps maintenance affordable: the fixture they send becomes a test in my repository, so the bug cannot return.\n\nThen I check the usual asymmetries between an app and a playground. Path and alias resolution from `node_modules` versus a local folder. Layer order, since their app may extend other layers or have a `layers\u002F` folder that outranks mine. `imports: { autoImport: false }` or a different `components` configuration. A different Nuxt version, and duplicate installs of Nuxt or Vue, which `npx nuxt upgrade --dedupe` resolves. Different `app.config` or `runtimeConfig` values. I ask for their resolved layer order and the output of `nuxt info` before I start guessing.\n\nThe systemic fix is a fixture that lives outside the package folder and is built in CI, because that is the environment the playground cannot simulate.\n",{"id":2164,"type":324,"difficulty":347,"subsection":2165,"tags":2166,"prompt":2168,"answer":2169,"section":2074,"sectionTitle":267},"senior-craft-013","code-review-checklist",[2167],"review","Name the items you check first when reviewing a change to a layer, and why those first.\n","Paths, names and side effects, because they are the three that the playground cannot catch and that consumers hit immediately.\n\nPaths: every path in the layer's config resolved from `import.meta.url`, no `~` or `@` in layer config or layer CSS, because those resolve against the consuming project and work only inside the layer's own repository. Names: components, composables, `app.config` keys, `runtimeConfig` keys and server routes all prefixed, because a collision silently overrides or is overridden depending on priority. Side effects: no plugin that initialises something globally without a switch, because plugins from every layer run and an app cannot remove one.\n\nAfter those: typed `app.config`, namespaced and validated server routes, a fixture proving a consumer override wins, and the cost the change adds to every app's bundle and build.\n",{"id":2171,"type":338,"difficulty":347,"subsection":2165,"tags":2172,"prompt":2173,"options":2174,"correct":2179,"answer":2180,"section":2074,"sectionTitle":267},"senior-craft-014",[2167,525],"Which line in a module pull request should fail review immediately?\n",[2175,2176,2177,2178],"`import { defineNuxtModule } from '@nuxt\u002Fkit'` at the top of `src\u002Fmodule.ts`\n","`import { createResolver } from '@nuxt\u002Fkit'` inside `src\u002Fruntime\u002Fplugin.ts`\n","`import { useState } from '#imports'` inside `src\u002Fruntime\u002Fcomposables\u002FuseToolkit.ts`\n","`nuxt.options.vite.optimizeDeps.include.push('some-cjs-lib')` in `setup`\n",[325],"`src\u002Fruntime\u002F**` is code that ships into the consumer's app and server bundles. `@nuxt\u002Fkit` is build-time only: it needs the Nuxt instance from unctx, which does not exist at runtime, so the import either fails to resolve or pulls build tooling into the bundle. The other three are correct usage: kit in the module entry, explicit `#imports` in runtime code, and declaring a CommonJS dependency for pre-bundling.\n",{"id":2182,"type":324,"difficulty":476,"subsection":326,"tags":2183,"prompt":2184,"answer":2185,"docs":2186,"section":2074,"sectionTitle":267},"senior-craft-015",[513,335,525],"A team asks you to build \"a design system\" for eight Nuxt apps. Layer, module, component library, or several of those?\n","Mostly a layer, probably with a module inside it, and not a plain component library.\n\nThe layer carries what the apps consume: prefixed components, composables, layouts, the CSS entry, typed theme tokens in `app.config`, and a `nuxt.config` preset that installs the UI, icon, font and lint modules with our settings. That gives auto-registration, override-by-path and merged config for free — none of which a plain npm component library provides, which is the argument against that option unless we also have non-Nuxt consumers.\n\nA module joins it when something must *happen* at build time: generating a token file from design tokens, a transform, or validating that an app's theme override is complete. The layer lists it, so consumers still write one `extends` line.\n\nThen the parts that decide whether it succeeds: a small documented surface with override recipes, a bundle and build-time budget in CI because the layer taxes all eight apps, a fixture app outside the package, and a governance story — one owning team, an RFC for surface changes, deprecation windows with adoption data.\n",[2187],{"title":395,"url":365},{"id":2189,"type":324,"difficulty":347,"subsection":326,"tags":2190,"prompt":2192,"answer":2193,"section":2074,"sectionTitle":267},"senior-craft-016",[2132,2191],"metrics","How would you know whether the shared tooling you own is actually working for the team?\n","By measuring adoption, cost and friction rather than asking for opinions.\n\nAdoption: which apps are on which version, from their lockfiles in CI. A package stuck at an old major in half the apps is telling you upgrades are painful. Cost: the bundle and build-time delta a fixture pays for installing it, tracked over time, because a shared package that quietly doubles everyone's entry chunk is a net loss. Friction: the support queue — what people ask, how often the same question returns, how many bugs arrive with a reproduction — plus how often an app forks or copy-pastes something instead of consuming it, which is the clearest signal that the surface or the escape hatches are wrong.\n\nIn the first six months of a role I would set up exactly those three, because they also tell me what to build next.\n",{"id":2195,"type":324,"difficulty":325,"subsection":326,"tags":2196,"prompt":2198,"answer":2199,"docs":2200,"section":2204,"sectionTitle":2205},"landscape-2026-001",[2197],"versions","What is the current state of Nuxt, and what happened to Nuxt 3?\n","Nuxt 4.5 is the current stable line, and Nuxt 3 reached end of life on 31 July 2026 — so an app still on 3.x needs migrating rather than patching, including for security fixes. Nuxt 5 is in development and can be previewed on a 4.x project through `future.compatibilityVersion: 5`.\n\nThe 4.5 line itself was deliberately unexciting: Vite 8 running on Rolldown, Rspack 2 as an alternative builder, experimental SSR streaming, and security releases on the 4.4 and 4.5 lines. Its job was to move the foundations so that the step to v5 is small. The version numbers move, so I would confirm the exact current patch on the releases page before quoting it.\n",[2201],{"title":2202,"url":2203},"Nuxt releases","https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt\u002Freleases","landscape-2026","2026 landscape",{"id":2207,"type":324,"difficulty":347,"subsection":2208,"tags":2209,"prompt":2210,"answer":2211,"section":2204,"sectionTitle":2205},"landscape-2026-002","nuxt-4-5",[1024,525],"Nuxt 4.5 moved Vite onto Rolldown. Why does that matter to you as a module author more than to an app developer?\n","Because a bundler swap is a plugin-behaviour change. An app developer mostly notices faster builds; a module author ships Vite plugins, and those depend on ordering (`enforce: 'pre' | 'post'`), on how `transform` hooks interleave, and sometimes on Rollup-specific plugin APIs that Rolldown implements differently.\n\nSo the practical response is to run the module's fixtures against the new version and assert the *output* of the transform rather than trusting that it ran — a build-level test that reads the generated chunk or the emitted file. The same release added the Rspack builder, which is a second reason to write build plugins with unplugin through `addBuildPlugin`: one implementation that survives a consumer switching builders.\n",{"id":2213,"type":338,"difficulty":347,"subsection":2214,"tags":2215,"prompt":2217,"options":2218,"correct":2223,"answer":2224,"section":2204,"sectionTitle":2205},"landscape-2026-003","nuxt-5-changes",[2216,965],"v5","Under Nuxt 5, what replaces `addVitePlugin(plugin, { client: true, server: false })`?\n",[2219,2220,2221,2222],"Two separate calls, one per Vite config\n","A plugin declaring `applyToEnvironment: env => env.name === 'client'`, because Nuxt 5 uses Vite's Environment API with one shared config\n","`extendViteConfig` with an `isClient` check\n","Nothing: the `client` and `server` options are unchanged in Nuxt 5\n",[325],"Nuxt 5 adopts Vite's Environment API, which replaces the old \"two configs, one per side\" model with one config whose plugins declare which environments they apply to. The `client` and `server` options on `addVitePlugin` are deprecated for that reason, and `extendViteConfig` is deprecated in favour of plugins with `config` hooks. This is the single most likely thing to break in a module that touches the bundler.\n",{"id":2226,"type":324,"difficulty":476,"subsection":2214,"tags":2227,"prompt":2229,"answer":2230,"docs":2231,"section":2204,"sectionTitle":2205},"landscape-2026-004",[2216,2228],"audit","Walk me through the Nuxt 5 changes that would affect a layer you maintain.\n","I would group them by where they bite. **Build**: the Vite Environment API, so any Vite plugin the layer registers moves off the `client`\u002F`server` options to `applyToEnvironment`, and Rolldown means re-testing plugin ordering. **Server**: Nitro v3 with web-standard Request and Response, and server utilities moving to `nuxt\u002Fserver`, so any deep `nitropack` import in the layer's `server\u002F` code has to go.\n\n**Runtime behaviour**: `process.*` type augmentation is removed, so everything becomes `import.meta.*`; `callHook` may return `void`, so no chaining `.then()`; `clearNuxtState` now resets to defaults rather than `undefined`; `\u003CClientOnly>` renders an HTML comment placeholder instead of a `\u003Cdiv>`, which breaks CSS that targeted it. **Routing**: case-sensitive matching and normalised page component names, so links, redirects and anything keyed on component names need a grep. **Tooling**: typed pages on by default, stricter TypeScript, and jiti no longer bundled so Node 22.19 or newer is required.\n\nThe way I would find out which of those actually matter is empirical: run the playground and the outside-the-package fixture with `future.compatibilityVersion: 5`, and add a nightly job against `nuxt-nightly`.\n",[2232],{"title":2233,"url":2234},"Nuxt 5 upgrade guide","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F5.x\u002Fgetting-started\u002Fupgrade",{"id":2236,"type":338,"difficulty":347,"subsection":2214,"tags":2237,"prompt":2238,"options":2239,"correct":2244,"answer":2245,"section":2204,"sectionTitle":2205},"landscape-2026-005",[2216,1050],"Why is `nuxt.callHook('toolkit:extend', registry).then(done)` a problem under Nuxt 5?\n",[2240,2241,2242,2243],"Build hooks are removed in favour of runtime hooks\n","`callHook` may return `void` rather than always returning a promise, so `.then` can throw\n","Hook names must be namespaced with a colon, which this one already is\n","Hooks now run in parallel, so the result is non-deterministic\n",[325],"Nuxt 5 allows `callHook` to be non-async and return `void` when nothing asynchronous is registered, which is a performance optimisation. Code that assumes a promise and chains `.then` throws a \"cannot read properties of undefined\" error. `await` works in both worlds, which is why the fix is to await rather than to chain.\n",{"id":2247,"type":324,"difficulty":347,"subsection":2248,"tags":2249,"prompt":2250,"answer":2251,"docs":2252,"section":2204,"sectionTitle":2205},"landscape-2026-006","v5-readiness-plan",[2216,414],"How would you make a module or layer Nuxt 5 ready without waiting for Nuxt 5?\n","Two switches plus a list. The switches make breakage visible today: a fixture with `future: { compatibilityVersion: 5 }`, which backports most of the behavioural changes into 4.x, and a nightly CI job installing `nuxt@npm:nuxt-nightly@5x`, allowed to fail but routed somewhere a human reads, because a `continue-on-error` job nobody looks at is a checkbox rather than a warning system.\n\nThe list is the upgrade guide turned into greps: `process.*` to `import.meta.*`, Vite plugins onto `applyToEnvironment`, no deep `nitropack` imports, no `.then()` on `callHook`, and checks on case-sensitive routes, page component names and the `\u003CClientOnly>` placeholder. Each finding is fixed in the current major, which is the point: by the time v5 ships, the upgrade is a version bump instead of a project. The flag is a smoke test, not a guarantee — it cannot give you Nitro v3 or the new bundler internals, which is what the nightly job covers.\n",[2253],{"title":2233,"url":2234},{"id":2255,"type":351,"difficulty":347,"subsection":2248,"language":2256,"tags":2257,"prompt":2258,"solution":2259,"answer":2260,"section":2204,"sectionTitle":2205},"landscape-2026-007","diff",[2216,2228],"Write the diff that fixes the three most common Nuxt 5 incompatibilities in a module: a `process.client` guard in runtime code, an `addVitePlugin` call using the deprecated per-side options, and a chained `callHook`.\n","--- a\u002Fsrc\u002Fruntime\u002Fplugin.ts\n+++ b\u002Fsrc\u002Fruntime\u002Fplugin.ts\n@@\n-if (process.client) {\n+if (import.meta.client) {\n   initBrowserOnlyThing()\n }\n\n--- a\u002Fsrc\u002Fmodule.ts\n+++ b\u002Fsrc\u002Fmodule.ts\n@@\n-    addVitePlugin(toolkitTransform(), { client: true, server: false })\n+    \u002F\u002F The client\u002Fserver options are deprecated for Vite's Environment API:\n+    \u002F\u002F one shared config, plugins declare where they apply.\n+    addVitePlugin({\n+      ...toolkitTransform(),\n+      applyToEnvironment: environment => environment.name === 'client',\n+    })\n\n--- a\u002Fsrc\u002Fmodule.ts\n+++ b\u002Fsrc\u002Fmodule.ts\n@@\n-    nuxt.callHook('toolkit:extend', registry).then(() => generateTemplates(registry))\n+    \u002F\u002F callHook may return void in Nuxt 5, so awaiting is the only safe form.\n+    await nuxt.callHook('toolkit:extend', registry)\n+    generateTemplates(registry)\n","These three cover most packages. The `process.*` one is mechanical — the type augmentation is removed in v5 and the `nuxt\u002Fprefer-import-meta` ESLint rule finds every occurrence — but it matters because `process.client` silently becomes `undefined` rather than failing loudly, so a browser-only branch quietly stops running.\n\nThe Vite one is the substantive change: with the Environment API there is one config, so \"which side does this plugin apply to?\" moves from the registration call into the plugin itself. The `callHook` one is a one-character class of bug that only appears when nothing async is registered on that hook, which makes it easy to miss in your own tests and easy for a consumer to hit.\n",{"id":2262,"type":324,"difficulty":347,"subsection":2263,"tags":2264,"prompt":2265,"answer":2266,"docs":2267,"section":2204,"sectionTitle":2205},"landscape-2026-008","vue-and-ecosystem",[795,640],"Which Vue 3.5 features matter most for server-rendered apps, and what problem does each solve?\n","`useId()` produces identifiers that are identical on server and client, which fixes the hydration mismatches that came from generated `id`, `for` and `aria-*` attributes — a particular problem for shipped form components, where the consumer cannot easily work around it.\n\nLazy hydration strategies, surfaced in Nuxt as `hydrate-on-visible`, `-idle`, `-interaction`, `-media-query`, `hydrate-after`, `hydrate-when` and `hydrate-never` on `Lazy*` components, keep server rendering while moving hydration CPU off the critical path, which is the main lever on INP. `data-allow-mismatch` whitelists an expected mismatch on one element with `text`, `children`, `class`, `style` or `attribute`, so a deliberately client-formatted timestamp does not need `\u003CClientOnly>` and the server render is kept. `useTemplateRef()` makes template refs explicit, and reactive props destructure removes a subtle reactivity trap in library components.\n\nVue 3.6 with Vapor mode was in release candidates when I last checked, which is how I would phrase it rather than claiming it has shipped.\n",[2268],{"title":2269,"url":1514},"Vue SSR API (data-allow-mismatch)",{"id":2271,"type":338,"difficulty":325,"subsection":2263,"tags":2272,"prompt":2274,"options":2275,"correct":2280,"answer":2281,"section":2204,"sectionTitle":2205},"landscape-2026-009",[2273],"ecosystem","Which module would you reach for to fix layout shift caused by web fonts?\n",[2276,2277,2278,2279],"`@nuxt\u002Fimage`\n","`@nuxt\u002Ffonts`\n","`@nuxt\u002Fscripts`\n","`nuxt-security`\n",[325],"`@nuxt\u002Ffonts` self-hosts fonts and generates fallback metric overrides so the fallback font occupies nearly the same space as the web font, which is the standard fix for font-driven CLS. `@nuxt\u002Fimage` addresses image-driven shift and LCP, `@nuxt\u002Fscripts` defers third-party scripts, and `nuxt-security` handles headers and CSP.\n",{"id":2283,"type":324,"difficulty":347,"subsection":326,"tags":2284,"prompt":2286,"answer":2287,"section":2204,"sectionTitle":2205},"landscape-2026-010",[1882,2285],"currency","How do you stay current with a framework that ships every few weeks, without reading every changelog?\n","By automating the part that matters and skimming the rest. The automation is a CI matrix: the lowest version we support, latest, and nightly. Upstream breakage then arrives as a failing job with a stack trace, which is far more informative than a changelog entry, and Renovate turns upgrades into pull requests I can merge on a cadence rather than remember to do.\n\nThe reading is narrow: release notes for minors and the upgrade guide for the next major, both skimmed for things that touch what we ship — bundler, Nitro, hooks, types — plus the security advisories, which I subscribe to rather than poll. Everything else I learn when I need it. For an interview I would also verify the fast-moving specifics the week before, because option names and defaults move between minors and confidently quoting a stale one is worse than saying \"I would check\".\n",{"id":2289,"type":338,"difficulty":347,"subsection":2214,"tags":2290,"prompt":2292,"options":2293,"correct":2298,"answer":2299,"section":2204,"sectionTitle":2205},"landscape-2026-011",[2216,2291],"routing","Your layer ships `pages\u002Fabout.vue` and a consuming app links to `\u002FAbout`. What happens under Nuxt 5?\n",[2294,2295,2296,2297],"It resolves, because Nuxt normalises the case of incoming URLs\n","It 404s, because routing became case-sensitive\n","It redirects to `\u002Fabout` automatically\n","It resolves but logs a deprecation warning\n",[325],"Nuxt 5 makes route matching case-sensitive, consistent with Nitro, so `\u002FAbout` no longer matches `pages\u002Fabout.vue`. For a layer author this is a grep task rather than a design problem: links, redirects, canonical URLs, sitemap entries and tests that relied on the loose matching all need checking, and anything externally published should get an explicit redirect rather than relying on a behaviour that has gone away.\n",{"id":2301,"type":324,"difficulty":476,"subsection":326,"tags":2302,"prompt":2304,"answer":2305,"section":2204,"sectionTitle":2205},"landscape-2026-012",[2303,335],"strategy","The company has eight apps: three on Nuxt 3, five on Nuxt 4.3. You are hired to build shared layers. What is your sequencing?\n","The three on Nuxt 3 are the urgent item, because 3.x went end of life in July 2026 and they are receiving no security fixes. That is a migration project with its own timeline, and it is separate from the layer work — building a shared layer that must support both 3 and 4 would poison the design with compatibility shims for code that is being retired anyway.\n\nSo: the layer targets Nuxt 4, with `peerDependencies.nuxt` set to the lowest version actually in use (4.3) and CI running that version alongside latest and nightly, so I cannot accidentally use a 4.5-only API and break five apps. The Nuxt 3 apps become consumers after they migrate, and in the meantime the layer's existence is an incentive: migrating gets you the design system.\n\nIn parallel I would set up the things that make the rest cheap — a fixture outside the package, bundle and build budgets, an adoption report from the apps' lockfiles, and the v5 readiness switches — because doing those at the start costs days and doing them after twenty apps depend on you costs quarters.\n",{"id":2307,"title":210,"body":2308,"description":213,"docs":3462,"extension":3469,"icon":3470,"meta":3471,"navigation":2620,"path":211,"sayings":3472,"seo":3475,"stem":212,"__hash__":3476},"study\u002Fstudy\u002F5.memory-leaks\u002F01.three-lifetimes.md",{"type":2309,"value":2310,"toc":3455},"minimark",[2311,2315,2320,2438,2442,2533,2537,2548,2756,2863,2866,3021,3024,3300,3338,3367,3371,3414,3418,3451],[2312,2313,2314],"p",{},"Almost every memory question an interviewer asks is really \"do you know which lifetime this value is in?\". A Nuxt app has three, they are nested, and code that looks identical behaves completely differently depending on which one it runs in. Get this table into muscle memory and the leak catalogues on the next two pages stop being lists to memorise and become consequences you can derive.",[2316,2317,2319],"h2",{"id":2318},"know","Know",[2321,2322,2323,2348,2378,2398,2412,2422],"ul",{},[2324,2325,2326,2330,2331,2334,2335,2339,2340,2343,2344,2347],"li",{},[2327,2328,2329],"strong",{},"Process lifetime — the Node server, hours to days."," Module-scope variables in the server bundle (everything at the top level of a ",[351,2332,2333],{},"server\u002F"," file, a Nitro plugin, or the server half of a universal plugin's ",[2336,2337,2338],"em",{},"module","), Nitro plugins themselves, global caches, and listeners on process-lifetime emitters: ",[351,2341,2342],{},"process.on",", timers, a database client, a message-bus connection. Owner: ",[2327,2345,2346],{},"the Node process",". Nothing here is ever collected while the server runs.",[2324,2349,2350,2353,2354,2357,2358,2361,2362,2369,2370,2373,2374,2377],{},[2327,2351,2352],{},"Request lifetime — milliseconds."," The h3 ",[351,2355,2356],{},"event",", ",[351,2359,2360],{},"event.context",", a ",[2327,2363,2364,2365,2368],{},"fresh ",[351,2366,2367],{},"nuxtApp"," for every SSR request"," (a fresh Vue app, a fresh set of plugin runs, a fresh component tree), the ",[351,2371,2372],{},"useState"," store and the payload that will be serialised into the HTML, the unhead instance, the per-request Pinia instance. Owner: ",[2327,2375,2376],{},"the request",". All of it should be unreachable the moment the response is flushed.",[2324,2379,2380,2383,2384,2387,2388,2390,2391,2393,2394,2397],{},[2327,2381,2382],{},"Client session — a browser tab, minutes to hours."," Exactly ",[2327,2385,2386],{},"one"," ",[351,2389,2367],{},", created once by the client entry and alive until reload or close; its payload\u002F",[351,2392,1925],{}," cache, its plugin state, its router. Components mount and unmount below it on every route change. Owner: ",[2327,2395,2396],{},"the tab",".",[2324,2399,2400,2403,2404,2407,2408,2411],{},[2327,2401,2402],{},"Universal plugins live in two lifetimes at once."," The same file runs once per tab in the browser and once per request on the server. Every line has to be correct in both readings — this is why ",[351,2405,2406],{},"setInterval"," in a universal plugin is a leak and in a ",[351,2409,2410],{},".client.ts"," plugin is fine.",[2324,2413,2414,2417,2418,2421],{},[2327,2415,2416],{},"Module scope is not a lifetime, it is a trap door into the longest one."," Top-level code in a server bundle runs once per process; top-level code in a client bundle runs once per tab. A ",[351,2419,2420],{},"const cache = new Map()"," in a file imported by both is two completely different objects with two completely different risk profiles.",[2324,2423,2424,2427,2428,2430,2431,2433,2434,2437],{},[2327,2425,2426],{},"The deciding question: \"who should still hold this after the response is sent?\""," Nobody → ",[351,2429,2360],{}," or a local. This app instance → ",[351,2432,2372],{}," \u002F ",[351,2435,2436],{},"nuxtApp.provide",". The whole process, deliberately → a bounded cache with a key and a TTL, documented. There is no fourth answer.",[2316,2439,2441],{"id":2440},"the-table","The table",[2443,2444,2445,2464],"table",{},[2446,2447,2448],"thead",{},[2449,2450,2451,2455,2458,2461],"tr",{},[2452,2453,2454],"th",{},"Lifetime",[2452,2456,2457],{},"What lives there",[2452,2459,2460],{},"Owner",[2452,2462,2463],{},"Ends when",[2465,2466,2467,2487,2513],"tbody",{},[2449,2468,2469,2476,2482,2484],{},[2470,2471,2472,2475],"td",{},[2327,2473,2474],{},"Process"," (hours\u002Fdays)",[2470,2477,2478,2479,2481],{},"module-scope variables in the server bundle, Nitro plugins, global caches, ",[351,2480,2132],{},"\u002Ftimer\u002Femitter listeners, DB pools",[2470,2483,2346],{},[2470,2485,2486],{},"the process restarts",[2449,2488,2489,2495,2508,2510],{},[2470,2490,2491,2494],{},[2327,2492,2493],{},"Request"," (milliseconds)",[2470,2496,2497,2357,2499,2501,2502,2504,2505,2507],{},[351,2498,2356],{},[351,2500,2360],{},", a fresh ",[351,2503,2367],{}," per SSR request, ",[351,2506,2372],{},"\u002Fpayload, unhead, Pinia",[2470,2509,2376],{},[2470,2511,2512],{},"the response is sent",[2449,2514,2515,2521,2527,2530],{},[2470,2516,2517,2520],{},[2327,2518,2519],{},"Client session"," (minutes\u002Fhours)",[2470,2522,2523,2524,2526],{},"the single ",[351,2525,2367],{},", payload cache, plugin state; components mount\u002Funmount per route",[2470,2528,2529],{},"the browser tab",[2470,2531,2532],{},"reload or close",[2316,2534,2536],{"id":2535},"how-it-works","How it works",[2312,2538,2539,2540,2543,2544,2547],{},"Two files that look similar and are not. The Nitro plugin body runs ",[2327,2541,2542],{},"once","; the event handler body runs ",[2327,2545,2546],{},"per request",":",[2549,2550,2555],"pre",{"className":2551,"code":2552,"filename":2553,"language":353,"meta":2554,"style":2554},"language-ts shiki shiki-themes material-theme-lighter github-light github-dark","\u002F\u002F process lifetime: this body runs once, when Nitro starts\nexport default defineNitroPlugin((nitroApp) => {\n  let requests = 0\n\n  \u002F\u002F registered once, for the life of the process — correct\n  nitroApp.hooks.hook('afterResponse', () => { requests++ })\n\n  \u002F\u002F anything long-lived must also be tearable-down\n  nitroApp.hooks.hookOnce('close', () => { console.log('served', requests) })\n})\n","server\u002Fplugins\u002Fmetrics.ts","",[351,2556,2557,2565,2599,2615,2622,2628,2678,2683,2689,2749],{"__ignoreMap":2554},[2558,2559,2561],"span",{"class":2560,"line":325},"line",[2558,2562,2564],{"class":2563},"sutJx","\u002F\u002F process lifetime: this body runs once, when Nitro starts\n",[2558,2566,2567,2571,2574,2578,2582,2585,2589,2592,2596],{"class":2560,"line":347},[2558,2568,2570],{"class":2569},"sVHd0","export",[2558,2572,2573],{"class":2569}," default",[2558,2575,2577],{"class":2576},"sGLFI"," defineNitroPlugin",[2558,2579,2581],{"class":2580},"su5hD","(",[2558,2583,2581],{"class":2584},"sP7_E",[2558,2586,2588],{"class":2587},"s99_P","nitroApp",[2558,2590,2591],{"class":2584},")",[2558,2593,2595],{"class":2594},"sbsja"," =>",[2558,2597,2598],{"class":2584}," {\n",[2558,2600,2601,2604,2607,2611],{"class":2560,"line":476},[2558,2602,2603],{"class":2594},"  let",[2558,2605,2606],{"class":2580}," requests",[2558,2608,2610],{"class":2609},"smGrS"," =",[2558,2612,2614],{"class":2613},"srdBf"," 0\n",[2558,2616,2618],{"class":2560,"line":2617},4,[2558,2619,2621],{"emptyLinePlaceholder":2620},true,"\n",[2558,2623,2625],{"class":2560,"line":2624},5,[2558,2626,2627],{"class":2563},"  \u002F\u002F registered once, for the life of the process — correct\n",[2558,2629,2631,2634,2636,2638,2640,2643,2646,2650,2654,2656,2659,2662,2664,2667,2669,2672,2675],{"class":2560,"line":2630},6,[2558,2632,2633],{"class":2580},"  nitroApp",[2558,2635,2397],{"class":2584},[2558,2637,1050],{"class":2580},[2558,2639,2397],{"class":2584},[2558,2641,2642],{"class":2576},"hook",[2558,2644,2581],{"class":2645},"skxfh",[2558,2647,2649],{"class":2648},"sjJ54","'",[2558,2651,2653],{"class":2652},"s_sjI","afterResponse",[2558,2655,2649],{"class":2648},[2558,2657,2658],{"class":2584},",",[2558,2660,2661],{"class":2584}," ()",[2558,2663,2595],{"class":2594},[2558,2665,2666],{"class":2584}," {",[2558,2668,2606],{"class":2580},[2558,2670,2671],{"class":2609},"++",[2558,2673,2674],{"class":2584}," }",[2558,2676,2677],{"class":2645},")\n",[2558,2679,2681],{"class":2560,"line":2680},7,[2558,2682,2621],{"emptyLinePlaceholder":2620},[2558,2684,2686],{"class":2560,"line":2685},8,[2558,2687,2688],{"class":2563},"  \u002F\u002F anything long-lived must also be tearable-down\n",[2558,2690,2692,2694,2696,2698,2700,2703,2705,2707,2710,2712,2714,2716,2718,2720,2723,2725,2728,2730,2732,2735,2737,2739,2741,2744,2747],{"class":2560,"line":2691},9,[2558,2693,2633],{"class":2580},[2558,2695,2397],{"class":2584},[2558,2697,1050],{"class":2580},[2558,2699,2397],{"class":2584},[2558,2701,2702],{"class":2576},"hookOnce",[2558,2704,2581],{"class":2645},[2558,2706,2649],{"class":2648},[2558,2708,2709],{"class":2652},"close",[2558,2711,2649],{"class":2648},[2558,2713,2658],{"class":2584},[2558,2715,2661],{"class":2584},[2558,2717,2595],{"class":2594},[2558,2719,2666],{"class":2584},[2558,2721,2722],{"class":2580}," console",[2558,2724,2397],{"class":2584},[2558,2726,2727],{"class":2576},"log",[2558,2729,2581],{"class":2645},[2558,2731,2649],{"class":2648},[2558,2733,2734],{"class":2652},"served",[2558,2736,2649],{"class":2648},[2558,2738,2658],{"class":2584},[2558,2740,2606],{"class":2580},[2558,2742,2743],{"class":2645},") ",[2558,2745,2746],{"class":2584},"}",[2558,2748,2677],{"class":2645},[2558,2750,2752,2754],{"class":2560,"line":2751},10,[2558,2753,2746],{"class":2584},[2558,2755,2677],{"class":2580},[2549,2757,2760],{"className":2551,"code":2758,"filename":2759,"language":353,"meta":2554,"style":2554},"\u002F\u002F request lifetime: this body runs for every request\nexport default defineEventHandler(async (event) => {\n  \u002F\u002F correct home for per-request data: dies with the event\n  event.context.user = await resolveUser(event)\n  return { name: event.context.user.name }\n})\n","server\u002Fapi\u002Fprofile.get.ts",[351,2761,2762,2767,2792,2797,2826,2857],{"__ignoreMap":2554},[2558,2763,2764],{"class":2560,"line":325},[2558,2765,2766],{"class":2563},"\u002F\u002F request lifetime: this body runs for every request\n",[2558,2768,2769,2771,2773,2776,2778,2781,2784,2786,2788,2790],{"class":2560,"line":347},[2558,2770,2570],{"class":2569},[2558,2772,2573],{"class":2569},[2558,2774,2775],{"class":2576}," defineEventHandler",[2558,2777,2581],{"class":2580},[2558,2779,2780],{"class":2594},"async",[2558,2782,2783],{"class":2584}," (",[2558,2785,2356],{"class":2587},[2558,2787,2591],{"class":2584},[2558,2789,2595],{"class":2594},[2558,2791,2598],{"class":2584},[2558,2793,2794],{"class":2560,"line":476},[2558,2795,2796],{"class":2563},"  \u002F\u002F correct home for per-request data: dies with the event\n",[2558,2798,2799,2802,2804,2807,2809,2812,2814,2817,2820,2822,2824],{"class":2560,"line":2617},[2558,2800,2801],{"class":2580},"  event",[2558,2803,2397],{"class":2584},[2558,2805,2806],{"class":2580},"context",[2558,2808,2397],{"class":2584},[2558,2810,2811],{"class":2580},"user",[2558,2813,2610],{"class":2609},[2558,2815,2816],{"class":2569}," await",[2558,2818,2819],{"class":2576}," resolveUser",[2558,2821,2581],{"class":2645},[2558,2823,2356],{"class":2580},[2558,2825,2677],{"class":2645},[2558,2827,2828,2831,2833,2836,2838,2841,2843,2845,2847,2849,2851,2854],{"class":2560,"line":2624},[2558,2829,2830],{"class":2569},"  return",[2558,2832,2666],{"class":2584},[2558,2834,2835],{"class":2645}," name",[2558,2837,2547],{"class":2584},[2558,2839,2840],{"class":2580}," event",[2558,2842,2397],{"class":2584},[2558,2844,2806],{"class":2580},[2558,2846,2397],{"class":2584},[2558,2848,2811],{"class":2580},[2558,2850,2397],{"class":2584},[2558,2852,2853],{"class":2580},"name",[2558,2855,2856],{"class":2584}," }\n",[2558,2858,2859,2861],{"class":2560,"line":2630},[2558,2860,2746],{"class":2584},[2558,2862,2677],{"class":2580},[2312,2864,2865],{},"The universal plugin is the one to reason about out loud, because it is both lifetimes at once:",[2549,2867,2870],{"className":2551,"code":2868,"filename":2869,"language":353,"meta":2554,"style":2554},"\u002F\u002F ⚠ module scope. On the server this is ONE array for the whole process.\nconst buffer: unknown[] = []\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  \u002F\u002F this callback runs once per tab in the browser, and once per SSR request in Node\n  buffer.push(nuxtApp) \u002F\u002F leaks every request's Vue app, forever\n\n  if (import.meta.client) {\n    \u002F\u002F safe: the tab owns this, and it goes away with the tab\n    setInterval(() => flush(), 10_000)\n  }\n})\n","app\u002Fplugins\u002Ftelemetry.ts",[351,2871,2872,2877,2901,2905,2926,2931,2950,2954,2979,2984,3008,3014],{"__ignoreMap":2554},[2558,2873,2874],{"class":2560,"line":325},[2558,2875,2876],{"class":2563},"\u002F\u002F ⚠ module scope. On the server this is ONE array for the whole process.\n",[2558,2878,2879,2882,2886,2888,2892,2895,2898],{"class":2560,"line":347},[2558,2880,2881],{"class":2594},"const",[2558,2883,2885],{"class":2884},"s_hVV"," buffer",[2558,2887,2547],{"class":2609},[2558,2889,2891],{"class":2890},"sZMiF"," unknown",[2558,2893,2894],{"class":2580},"[] ",[2558,2896,2897],{"class":2609},"=",[2558,2899,2900],{"class":2580}," []\n",[2558,2902,2903],{"class":2560,"line":476},[2558,2904,2621],{"emptyLinePlaceholder":2620},[2558,2906,2907,2909,2911,2914,2916,2918,2920,2922,2924],{"class":2560,"line":2617},[2558,2908,2570],{"class":2569},[2558,2910,2573],{"class":2569},[2558,2912,2913],{"class":2576}," defineNuxtPlugin",[2558,2915,2581],{"class":2580},[2558,2917,2581],{"class":2584},[2558,2919,2367],{"class":2587},[2558,2921,2591],{"class":2584},[2558,2923,2595],{"class":2594},[2558,2925,2598],{"class":2584},[2558,2927,2928],{"class":2560,"line":2624},[2558,2929,2930],{"class":2563},"  \u002F\u002F this callback runs once per tab in the browser, and once per SSR request in Node\n",[2558,2932,2933,2936,2938,2941,2943,2945,2947],{"class":2560,"line":2630},[2558,2934,2935],{"class":2580},"  buffer",[2558,2937,2397],{"class":2584},[2558,2939,2940],{"class":2576},"push",[2558,2942,2581],{"class":2645},[2558,2944,2367],{"class":2580},[2558,2946,2743],{"class":2645},[2558,2948,2949],{"class":2563},"\u002F\u002F leaks every request's Vue app, forever\n",[2558,2951,2952],{"class":2560,"line":2680},[2558,2953,2621],{"emptyLinePlaceholder":2620},[2558,2955,2956,2959,2961,2964,2966,2969,2971,2974,2976],{"class":2560,"line":2685},[2558,2957,2958],{"class":2569},"  if",[2558,2960,2783],{"class":2645},[2558,2962,2963],{"class":2569},"import",[2558,2965,2397],{"class":2584},[2558,2967,2968],{"class":2884},"meta",[2558,2970,2397],{"class":2584},[2558,2972,2973],{"class":2580},"client",[2558,2975,2743],{"class":2645},[2558,2977,2978],{"class":2584},"{\n",[2558,2980,2981],{"class":2560,"line":2691},[2558,2982,2983],{"class":2563},"    \u002F\u002F safe: the tab owns this, and it goes away with the tab\n",[2558,2985,2986,2989,2991,2994,2996,2999,3001,3003,3006],{"class":2560,"line":2751},[2558,2987,2988],{"class":2576},"    setInterval",[2558,2990,2581],{"class":2645},[2558,2992,2993],{"class":2584},"()",[2558,2995,2595],{"class":2594},[2558,2997,2998],{"class":2576}," flush",[2558,3000,2993],{"class":2645},[2558,3002,2658],{"class":2584},[2558,3004,3005],{"class":2613}," 10_000",[2558,3007,2677],{"class":2645},[2558,3009,3011],{"class":2560,"line":3010},11,[2558,3012,3013],{"class":2584},"  }\n",[2558,3015,3017,3019],{"class":2560,"line":3016},12,[2558,3018,2746],{"class":2584},[2558,3020,2677],{"class":2580},[2312,3022,3023],{},"The same intent, written so each value sits in the lifetime that should own it:",[2549,3025,3028],{"className":2551,"code":3026,"filename":3027,"language":353,"meta":2554,"style":2554},"import { effectScope, shallowRef } from 'vue'\n\nexport default defineNuxtPlugin(() => {\n  \u002F\u002F client session: one buffer per tab, never one per request\n  const buffer = shallowRef\u003Cunknown[]>([])\n\n  \u002F\u002F detached scope: no component owns this, so I own it and I can stop it\n  const scope = effectScope(true)\n  scope.run(() => {\n    \u002F\u002F VueUse registers its own cleanup on the active scope, so stopping it clears the timer\n    useIntervalFn(() => flush(buffer.value), 10_000)\n  })\n\n  \u002F\u002F one teardown for everything the plugin started (HMR would otherwise stack timers in dev)\n  if (import.meta.hot) import.meta.hot.dispose(() => scope.stop())\n\n  return { provide: { telemetry: { buffer } } }\n})\n","app\u002Fplugins\u002Ftelemetry.client.ts",[351,3029,3030,3057,3061,3077,3082,3108,3112,3117,3136,3154,3159,3190,3197,3202,3208,3259,3264,3293],{"__ignoreMap":2554},[2558,3031,3032,3034,3036,3039,3041,3044,3046,3049,3052,3054],{"class":2560,"line":325},[2558,3033,2963],{"class":2569},[2558,3035,2666],{"class":2584},[2558,3037,3038],{"class":2580}," effectScope",[2558,3040,2658],{"class":2584},[2558,3042,3043],{"class":2580}," shallowRef",[2558,3045,2674],{"class":2584},[2558,3047,3048],{"class":2569}," from",[2558,3050,3051],{"class":2648}," '",[2558,3053,795],{"class":2652},[2558,3055,3056],{"class":2648},"'\n",[2558,3058,3059],{"class":2560,"line":347},[2558,3060,2621],{"emptyLinePlaceholder":2620},[2558,3062,3063,3065,3067,3069,3071,3073,3075],{"class":2560,"line":476},[2558,3064,2570],{"class":2569},[2558,3066,2573],{"class":2569},[2558,3068,2913],{"class":2576},[2558,3070,2581],{"class":2580},[2558,3072,2993],{"class":2584},[2558,3074,2595],{"class":2594},[2558,3076,2598],{"class":2584},[2558,3078,3079],{"class":2560,"line":2617},[2558,3080,3081],{"class":2563},"  \u002F\u002F client session: one buffer per tab, never one per request\n",[2558,3083,3084,3087,3089,3091,3093,3096,3099,3102,3105],{"class":2560,"line":2624},[2558,3085,3086],{"class":2594},"  const",[2558,3088,2885],{"class":2884},[2558,3090,2610],{"class":2609},[2558,3092,3043],{"class":2576},[2558,3094,3095],{"class":2584},"\u003C",[2558,3097,3098],{"class":2890},"unknown",[2558,3100,3101],{"class":2645},"[]",[2558,3103,3104],{"class":2584},">",[2558,3106,3107],{"class":2645},"([])\n",[2558,3109,3110],{"class":2560,"line":2630},[2558,3111,2621],{"emptyLinePlaceholder":2620},[2558,3113,3114],{"class":2560,"line":2680},[2558,3115,3116],{"class":2563},"  \u002F\u002F detached scope: no component owns this, so I own it and I can stop it\n",[2558,3118,3119,3121,3124,3126,3128,3130,3134],{"class":2560,"line":2685},[2558,3120,3086],{"class":2594},[2558,3122,3123],{"class":2884}," scope",[2558,3125,2610],{"class":2609},[2558,3127,3038],{"class":2576},[2558,3129,2581],{"class":2645},[2558,3131,3133],{"class":3132},"syTEX","true",[2558,3135,2677],{"class":2645},[2558,3137,3138,3141,3143,3146,3148,3150,3152],{"class":2560,"line":2691},[2558,3139,3140],{"class":2580},"  scope",[2558,3142,2397],{"class":2584},[2558,3144,3145],{"class":2576},"run",[2558,3147,2581],{"class":2645},[2558,3149,2993],{"class":2584},[2558,3151,2595],{"class":2594},[2558,3153,2598],{"class":2584},[2558,3155,3156],{"class":2560,"line":2751},[2558,3157,3158],{"class":2563},"    \u002F\u002F VueUse registers its own cleanup on the active scope, so stopping it clears the timer\n",[2558,3160,3161,3164,3166,3168,3170,3172,3174,3177,3179,3182,3184,3186,3188],{"class":2560,"line":3010},[2558,3162,3163],{"class":2576},"    useIntervalFn",[2558,3165,2581],{"class":2645},[2558,3167,2993],{"class":2584},[2558,3169,2595],{"class":2594},[2558,3171,2998],{"class":2576},[2558,3173,2581],{"class":2645},[2558,3175,3176],{"class":2580},"buffer",[2558,3178,2397],{"class":2584},[2558,3180,3181],{"class":2580},"value",[2558,3183,2591],{"class":2645},[2558,3185,2658],{"class":2584},[2558,3187,3005],{"class":2613},[2558,3189,2677],{"class":2645},[2558,3191,3192,3195],{"class":2560,"line":3016},[2558,3193,3194],{"class":2584},"  }",[2558,3196,2677],{"class":2645},[2558,3198,3200],{"class":2560,"line":3199},13,[2558,3201,2621],{"emptyLinePlaceholder":2620},[2558,3203,3205],{"class":2560,"line":3204},14,[2558,3206,3207],{"class":2563},"  \u002F\u002F one teardown for everything the plugin started (HMR would otherwise stack timers in dev)\n",[2558,3209,3211,3213,3215,3217,3219,3221,3223,3226,3228,3230,3232,3234,3236,3238,3240,3243,3245,3247,3249,3251,3253,3256],{"class":2560,"line":3210},15,[2558,3212,2958],{"class":2569},[2558,3214,2783],{"class":2645},[2558,3216,2963],{"class":2569},[2558,3218,2397],{"class":2584},[2558,3220,2968],{"class":2884},[2558,3222,2397],{"class":2584},[2558,3224,3225],{"class":2580},"hot",[2558,3227,2743],{"class":2645},[2558,3229,2963],{"class":2569},[2558,3231,2397],{"class":2584},[2558,3233,2968],{"class":2884},[2558,3235,2397],{"class":2584},[2558,3237,3225],{"class":2580},[2558,3239,2397],{"class":2584},[2558,3241,3242],{"class":2576},"dispose",[2558,3244,2581],{"class":2645},[2558,3246,2993],{"class":2584},[2558,3248,2595],{"class":2594},[2558,3250,3123],{"class":2580},[2558,3252,2397],{"class":2584},[2558,3254,3255],{"class":2576},"stop",[2558,3257,3258],{"class":2645},"())\n",[2558,3260,3262],{"class":2560,"line":3261},16,[2558,3263,2621],{"emptyLinePlaceholder":2620},[2558,3265,3267,3269,3271,3274,3276,3278,3281,3283,3285,3287,3289,3291],{"class":2560,"line":3266},17,[2558,3268,2830],{"class":2569},[2558,3270,2666],{"class":2584},[2558,3272,3273],{"class":2645}," provide",[2558,3275,2547],{"class":2584},[2558,3277,2666],{"class":2584},[2558,3279,3280],{"class":2645}," telemetry",[2558,3282,2547],{"class":2584},[2558,3284,2666],{"class":2584},[2558,3286,2885],{"class":2580},[2558,3288,2674],{"class":2584},[2558,3290,2674],{"class":2584},[2558,3292,2856],{"class":2584},[2558,3294,3296,3298],{"class":2560,"line":3295},18,[2558,3297,2746],{"class":2584},[2558,3299,2677],{"class":2580},[3301,3302,3304],"gotcha",{"title":3303},"`useState` is not global state, it is per-instance state",[2312,3305,3306,3307,3309,3310,3313,3314,3320,3321,3324,3325,3328,3329,3331,3332,3334,3335,3337],{},"People read \"shared, SSR-friendly state\" and conclude ",[351,3308,2372],{}," is a singleton. It is a key into ",[351,3311,3312],{},"nuxtApp.payload.state",", and there is ",[2327,3315,3316,3317,3319],{},"one ",[351,3318,2367],{}," per SSR request",". That is precisely why it is safe on the server and why a module-scope ",[351,3322,3323],{},"ref()"," is not: the ",[351,3326,3327],{},"ref"," is created once per process and shared by every user, the ",[351,3330,2372],{}," is created once per request. The corollary bites in the other direction on the client, where there is one ",[351,3333,2367],{}," for the whole tab — a ",[351,3336,2372],{}," array that a route pushes into on every visit grows for the life of the session.",[3301,3339,3341],{"title":3340},"A `.server.ts` plugin is still per request",[2312,3342,3343,3344,3347,3348,3351,3352,3355,3356,3359,3360,2783,3363,3366],{},"The ",[351,3345,3346],{},".server"," suffix means \"does not ship to the browser\", not \"runs once\". A ",[351,3349,3350],{},".server.ts"," plugin body executes for every SSR request, so registering a ",[351,3353,3354],{},"process.on('unhandledRejection', …)"," there adds one listener per request and you will see ",[351,3357,3358],{},"MaxListenersExceededWarning"," within minutes of real traffic. Process-lifetime setup belongs in ",[351,3361,3362],{},"server\u002Fplugins\u002F*",[351,3364,3365],{},"defineNitroPlugin","), which genuinely runs once.",[2316,3368,3370],{"id":3369},"exercise","Exercise",[3369,3372,3374],{"id":3373},"memory-leaks-three-lifetimes-1",[2321,3375,3378,3398,3408],{"className":3376},[3377],"contains-task-list",[2324,3379,3382,3386,3387,3390,3391,3393,3394,3397],{"className":3380},[3381],"task-list-item",[3383,3384],"input",{"disabled":2620,"type":3385},"checkbox"," Add ",[351,3388,3389],{},"console.log('plugin ran')"," to a universal plugin, a ",[351,3392,2410],{}," plugin and a Nitro plugin. Build, start the server, and reload a page five times. Count the lines from each in the terminal and in the browser console — the numbers ",[2336,3395,3396],{},"are"," the three lifetimes.",[2324,3399,3401,3403,3404,3407],{"className":3400},[3381],[3383,3402],{"disabled":2620,"type":3385}," In the universal plugin, add a module-scope ",[351,3405,3406],{},"let runs = 0",", increment it in the plugin body, and expose it from a server route. Reload ten times and watch it climb; note that the browser's copy stays at 1.",[2324,3409,3411,3413],{"className":3410},[3381],[3383,3412],{"disabled":2620,"type":3385}," Take one value from a real layer you maintain and write down which lifetime owns it and where it is stored today. If the answer is \"module scope in runtime code\", you have found a bug.",[2316,3415,3417],{"id":3416},"be-able-to-say","Be able to say",[3416,3419,3421],{"q":3420},"Walk me through the lifetimes in a server-rendered Nuxt app and how you decide where a value belongs.",[2312,3422,3423,3424,3426,3427,2357,3429,3431,3432,3434,3435,3438,3439,3441,3442,3444,3445,3447,3448,3450],{},"\"There are three. The process lives for hours or days and owns module-scope variables in the server bundle, Nitro plugins, global caches and anything registered on ",[351,3425,2132],{}," or a long-lived client. The request lives for milliseconds and owns the h3 ",[351,3428,2356],{},[351,3430,2360],{},", and a completely fresh ",[351,3433,2367],{}," — Nuxt builds a new Vue app and re-runs every universal plugin and every ",[351,3436,3437],{},"setup()"," for each SSR request. The client session is a browser tab: one ",[351,3440,2367],{}," for its whole life, with components mounting and unmounting underneath it. The trap is that a universal plugin runs in two of those lifetimes with the same source code. So the question I ask about any value is who should still hold it after the response is sent: nobody means ",[351,3443,2360],{}," or a local, this app instance means ",[351,3446,2372],{}," or ",[351,3449,2436],{},", and the whole process only when I meant it, in which case it gets a bounded key and a TTL.\"",[3452,3453,3454],"style",{},"html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":2554,"searchDepth":476,"depth":476,"links":3456},[3457,3458,3459,3460,3461],{"id":2318,"depth":347,"text":2319},{"id":2440,"depth":347,"text":2441},{"id":2535,"depth":347,"text":2536},{"id":3369,"depth":347,"text":3370},{"id":3416,"depth":347,"text":3417},[3463,3464,3466,3467],{"title":1690,"url":1448},{"title":3465,"url":648},"Nuxt — server directory (Nitro plugins, event handlers)",{"title":1499,"url":1500},{"title":3468,"url":1877},"Vue — reactivity API (effectScope)","md",null,{},[3473],{"q":3420,"a":3474},"\"There are three. The process lives for hours or days and owns module-scope variables in the server bundle, Nitro plugins, global caches and anything registered on process or a long-lived client. The request lives for milliseconds and owns the h3 event, event.context, and a completely fresh nuxtApp — Nuxt builds a new Vue app and re-runs every universal plugin and every setup() for each SSR request. The client session is a browser tab: one nuxtApp for its whole life, with components mounting and unmounting underneath it. The trap is that a universal plugin runs in two of those lifetimes with the same source code. So the question I ask about any value is who should still hold it after the response is sent: nobody means event.context or a local, this app instance means useState or nuxtApp.provide, and the whole process only when I meant it, in which case it gets a bounded key and a TTL.\"",{"title":210,"description":213},"WqtJiK5IuAwlSAE8Gu7JysbJ_ue0P7KM4cG0GCO3V-c",[3478,3479],{"title":203,"path":204,"stem":205,"description":208,"children":-1},{"title":215,"path":216,"stem":217,"description":218,"children":-1},1789930285730]