A layer is only useful if consumers can bend it. The override model in Nuxt is deliberately dumb: the same resolved name at a higher priority wins. There is no override: keyword, no registry, no merge of two components. That simplicity is what makes it predictable, and it also produces the sharp edges this page catalogues: what "same name" means for each file type, what cannot be overridden by placing a file, and how to extend instead of replace.
app/components/Team/Button.vue replaces the layer's app/components/Team/Button.vue because both resolve to TeamButton. A project file with a different path but the same final name also wins, with a warning about the duplicate.app/pages/settings.vue replaces the layer's settings route; pages the project does not redefine stay. To remove an inherited page use the pages:extend hook and filter the routes.default.vue). Middleware by file name for named middleware; global middleware (*.global.ts) from every layer runs.01.team-setup.ts runs before the project's 02.tracking.ts; use dependsOn in the object syntax when order matters rather than trusting names.server/api and server/routes are matched by path; server/middleware from every layer runs on every request. Namespace layer routes (/api/_team/...) so an app cannot collide by accident.app.vue, error.vue, app/router.options.ts come from the project if present, otherwise from the highest-priority layer that has one. A layer app.vue is a legitimate way to ship a default shell with <NuxtLayout> and <NuxtPage>.#layers/<name> alias: the project component imports the layer's original by path and renders it inside, adding props, classes or slots. This keeps the layer's future fixes flowing into the app.Three ways to change a layer component, in order of preference:
// 1. Configure: the layer exposes knobs in app.config; the project sets them. No override at all.
export default defineAppConfig({
team: { button: { rounded: 'full', size: 'lg' } },
})
<script setup lang="ts">
// 2. Wrap: replace the registration but keep rendering the original.
import LayerButton from '#layers/base/app/components/Team/Button.vue'
</script>
<template>
<LayerButton v-bind="$attrs" data-analytics="cta">
<slot />
</LayerButton>
</template>
<!-- 3. Replace: same path, brand-new implementation. The layer's version is no longer registered. -->
<template>
<button class="btn"><slot /></button>
</template>
Removing an inherited route, which no file placement can do:
export default defineNuxtConfig({
hooks: {
'pages:extend'(pages) {
const drop = new Set(['/legacy-dashboard'])
for (let i = pages.length - 1; i >= 0; i--) {
if (drop.has(pages[i]!.path)) pages.splice(i, 1)
}
},
},
})
Proving which file won, from a local module, without guessing:
import { defineNuxtModule } from 'nuxt/kit'
export default defineNuxtModule({
setup(_options, nuxt) {
nuxt.hook('components:extend', (components) => {
const hit = components.find(c => c.pascalName === 'TeamButton')
console.log('TeamButton resolves to', hit?.filePath)
})
},
})
Replacing app/pages/settings.vue replaces the view. If the layer's page relied on a layer middleware or a layer composable, those still exist and still run where they are referenced. Read the layer page you are replacing and decide deliberately what to keep.
The classic duplicate: the layer ships app/plugins/analytics.client.ts and the app already has app/plugins/tracking.client.ts initialising the same SDK. Different names, both run, two SDK instances. Layers should make plugins switchable through app.config or a module option, and document which plugins they add.
Exact plugin ordering across layers (whether the merged list is sorted by file name globally or per layer). Test with two numbered plugins in a layer and the project before quoting a rule.
docs ↗TeamButton from a layer. Override it in the project three ways (app.config knob, wrapper via #layers/..., full replacement) and confirm each with the components:extend logger above.app/pages/legacy-dashboard.vue to the layer, then remove it from the project with pages:extend. Confirm /legacy-dashboard 404s while other layer pages still resolve.01.a.ts in the layer and 00.b.ts in the project under app/plugins/, log from each, and write down the order you observed."Three levels. First, configuration: the layer exposes knobs in app.config, the app sets them and nothing is overridden. Second, wrapping: the app creates a component at the same path, imports the original through the #layers/base/... alias and renders it with extra props or classes, so it keeps receiving the layer's fixes. Third, replacement: same path, new implementation, and the layer's version is simply not registered anymore. Same-name-wins is the whole rule, which is why I namespace everything the layer ships and document the override recipe in its README."
Config merging in practice
How modules, runtimeConfig, app.config, route rules, Vite options and environment overrides behave when a layer and a project both declare them.
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.