The most valuable thing a base layer does for a team is often not a component but a preset: "every app gets Nuxt UI, ESLint, fonts, icons and image optimisation, configured our way, from one extends line". That works because a layer's nuxt.config can list modules and their options exactly like a project's. This page covers both sides of the relationship: layers that carry modules, and modules that must behave correctly when files are spread across layers. The Module authoring section covers the modules themselves.
modules: ['@nuxt/ui', '@nuxt/eslint'] in the layer becomes part of the merged array. The layer's package.json must declare those modules as dependencies so consumers install them.@nuxt/image and its setup runs once. Module options are merged with defu across all layers before that single setup runs.false (image: false, pinia: false). This is documented behaviour, not a trick, and is how you keep a preset opinionated but escapable.moduleDependencies (Nuxt ≥ 4.1) lets a module declare that it depends on another module with version constraints, defaults and overrides. In a toolkit, the team module declares its dependencies this way while the layer stays a plain list.modules/ directories are auto-registered for the project (modules/*.ts, modules/*/index.ts, alphabetical, after nuxt.config modules). A layer can ship the same directory for machinery that belongs with it. Nuxt 5 srcDir. They iterate getLayerDirectories() (or nuxt.options._layers) and scan each layer, project first, so that the project's files override a layer's. @nuxt/content does this for components/content, which is why the callout components used on these pages can live in layers/base._layers.The preset side, as this repository does it:
export default defineNuxtConfig({
$meta: { name: 'base' },
modules: ['@nuxt/ui'], // consumers get Nuxt UI (and its fonts/icons/color-mode) for free
css: [join(currentDir, './app/assets/css/main.css')],
icon: { serverBundle: { collections: ['lucide', 'simple-icons'] } },
})
export default defineNuxtConfig({
modules: ['@nuxt/content', '@vueuse/nuxt', '@nuxt/eslint'], // merged with the layer's: ui runs once
// ui: false <- would switch the inherited module off
})
The module side, scanning every layer with the project winning:
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { defineNuxtModule, getLayerDirectories, addTemplate } from 'nuxt/kit'
export default defineNuxtModule({
meta: { name: 'team-locales', configKey: 'teamLocales' },
setup(_options, nuxt) {
// getLayerDirectories() is ordered project first, then layers by priority.
// Reverse it so lower-priority layers register first and higher ones override the same keys.
const dirs = getLayerDirectories(nuxt).map(l => join(l.root, 'locales')).filter(existsSync).reverse()
addTemplate({
filename: 'team-locales.mjs',
getContents: () => `export const localeDirs = ${JSON.stringify(dirs)}`,
})
},
})
Registering a components directory per layer is the same idea:
import { defineNuxtModule, getLayerDirectories, addComponentsDir } from 'nuxt/kit'
export default defineNuxtModule({
setup(_options, nuxt) {
for (const layer of [...getLayerDirectories(nuxt)].reverse()) {
addComponentsDir({ path: `${layer.app}blocks`, prefix: 'Block', pathPrefix: false })
}
},
})
Two layers both listing @nuxt/ui is fine. Two layers both setting ui.theme.colors to different arrays is a concatenation, and two layers setting ui.colorMode to different booleans is a priority fight you resolve by knowing the order. Keep module options in the base layer only, and let apps override.
"Only add @nuxt/image when the app has images" is an if, and layers have no if. Either the base layer always includes it and apps opt out with image: false, or a second, smaller layer (layers/media) carries it and apps opt in by extending it.
Whether modules/ inside a layer is auto-registered like the project's, and the resulting order relative to modules from nuxt.config. Check the layer guide or test it before claiming it.
@nuxt/image and @nuxt/eslint. Extend it from an app that also lists @nuxt/image; add console.log in a local module on modules:done printing nuxt.options._installedModules.map(m => m.meta?.name) and confirm each appears once.image: false in the app and confirm the module is gone.team-locales module above, add locales/en.json to both the layer and the project, and confirm the project's directory is listed last (so it can override)."The layer is the preset: it lists the modules every app needs and ships their default options, components and composables, so one extends entry configures the app. Nuxt merges the module lists across layers and deduplicates by module, merges the options with the project winning, and an app can switch an inherited module off with image: false. On the other side, any module we write must be layer-aware: it iterates getLayerDirectories() instead of assuming one srcDir, registering lower-priority layers first so the project's files override. Content does exactly that for its components/content folders."
Overriding what a layer ships
How a project replaces, wraps, extends or removes components, pages, layouts, composables, plugins, middleware and server routes inherited from a layer.
Server code and Nitro in layers
What a layer can ship under server/ and shared/, how Nitro merges handlers and config across layers, and why every route you ship is a security decision for every app.