This is the page interviewers use to separate people who have used layers from people who have shipped them. The rules are short, but two of them are counter-intuitive (alphabetical order runs Z before A, and the first extends entry beats the second), and the merge semantics for arrays surprise almost everyone the first time.
~~/layers/*, in alphabetical order with later letters winning (Z beats A), so layers/2.features beats layers/1.base. Prefix folders with numbers to make the order explicit.extends entries, in array order: the first entry has higher priority than the second.nuxt.config (with its $development / $production / $env overrides applied first), then merges with defu: objects deep-merge, arrays concatenate, scalars take the higher-priority value, and undefined never overrides a defined default.modules, css, plugins, vite.plugins, imports.dirs: everything you and the layer both declare ends up in the final list. That is what you want for modules (Nuxt then deduplicates by module name), and rarely what you want for css when the app "overrides" a stylesheet and gets both._layers mirrors the priority. nuxt.options._layers[0] is the project; getLayerDirectories() returns the same order, and "earlier layers override later layers". Modules iterate it in that order to let the project win.nuxt.options. Inside a module, nuxt.options is the merged result; to see what an individual layer contributed, read layer.config from _layers.project/
├─ nuxt.config.ts extends: ['./vendor/theme', '@team/nuxt-layer-base']
├─ layers/
│ ├─ 1.base/ (auto) ┐ alphabetical, Z > A:
│ └─ 2.features/ (auto) ┘ 2.features beats 1.base
└─ vendor/theme/ (extends[0])
Resolved order, highest first:
project > layers/2.features > layers/1.base > ./vendor/theme > @team/nuxt-layer-base
The merge, illustrated with two configs:
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@nuxt/eslint'],
css: [join(currentDir, 'app/assets/css/base.css')],
runtimeConfig: { public: { apiBase: 'https://api.internal', featureFlags: { beta: false } } },
routeRules: { '/admin/**': { ssr: false } },
})
export default defineNuxtConfig({
modules: ['@nuxt/content'],
css: ['~/assets/css/app.css'],
runtimeConfig: { public: { featureFlags: { beta: true } } },
routeRules: { '/admin/**': { ssr: true } },
})
Result: modules is ['@nuxt/content', '@nuxt/ui', '@nuxt/eslint'] (project items first, then the layer's), css contains both files, featureFlags is { beta: true } with apiBase inherited, and /admin/** is { ssr: true } because the project's scalar wins inside the deep merge.
People assume layers/base is "first" because it sounds foundational, then put an override in layers/theme and wonder why theme loses to nothing and base wins over… nothing either. In practice theme > base alphabetically, so it works by accident until someone adds layers/ui. Use numeric prefixes (1.base, 2.theme, 3.ui) or list the folders in extends to state the order you mean.
Web developers expect "last wins" (CSS cascade, Object.assign). extends is the opposite: ['./a', './b'] means a overrides b. Read it as "a extends b": the first entry is the most specific.
The exact wording of the priority order and whether anything changed for auto-scanned layers in the current minor. It is documented on the getting-started page under "priority order"; re-read it the week before.
docs ↗layers/1.base and layers/2.theme, each with app/components/AppHeader.vue rendering its own name. Confirm 2.theme renders. Rename them to a-theme and b-base and confirm b-base now wins.extends: ['./layers/a-theme', './layers/b-base'] and confirm the explicit order overrides the alphabetical one.css entry and count the stylesheets in the rendered <head>. Then give both a runtimeConfig.public.flags object with different keys and print useRuntimeConfig().public.flags to see the deep merge."The project always wins. Below it, folders auto-scanned from layers/ are ordered alphabetically with later names winning, and below those the extends entries in array order, first entry highest. Configs are not replaced but merged with defu: objects deep-merge, scalars take the higher-priority value, and arrays are concatenated, which is why a layer's modules and css are added to the app's rather than overridden. I never trust my memory on the order; I put a component in two layers and check which renders, and I prefix layers/ folders with numbers so the order is explicit."