Every internals conversation ends here, because the two mechanisms overlap just enough to make the choice feel arbitrary. It is not. This page is the one-minute version — the Layers section covers priority, merging, aliases, publishing and testing in depth.
app.config. Modules ship build-time behaviour and injected runtime — generated templates, transforms, registered handlers, types, reactions to the consumer's config.nuxt.config into yours and Nuxt resolves its directories. It cannot run anything at build time. The moment your toolkit needs to inspect the consumer's config, generate a registry, validate options or add a Vite transform, that logic has to be a module.app.config, sensible defaults) that lists its own module in modules for the machinery. Consumers get everything from one extends entry.modules arrays concatenate under defu and Nuxt deduplicates by meta.name, so the layer and the project can both list the module and it runs once; and module options resolve as defu(inline, nuxt.config[configKey], defaults), so the project beats the layer beats the defaults.export default defineNuxtConfig({
modules: ['@acme/toolkit'], // the layer ships the machinery with the scaffold
css: [join(currentDir, './app/assets/toolkit.css')],
appConfig: { toolkit: { density: 'comfortable' } },
})
export default defineNuxtConfig({
extends: ['@acme/toolkit-layer'],
modules: ['@acme/toolkit'], // listing it again is harmless: deduplicated by meta.name
toolkit: { density: 'compact' }, // the project's options beat the layer's
})
Layers are quicker to start, so teams ship the scaffold first and bolt build logic on later as a modules/ file inside the layer. It works, but that is now an unversioned module consumers cannot pin, configure or disable independently. If there is any chance you will need build-time behaviour, publish the module as its own package from day one and let the layer depend on it.
playground-layer/ that lists your module in modules, overrides one of its components and sets an app.config value. Extend it from a playground that also lists the module, and confirm the module's setup runs exactly once."Layers ship app code and conventions; modules ship build-time behaviour and the runtime they inject. The test I use is whether anything needs to execute while Nuxt starts: a layer is declarative, so it can give you components, pages, server routes and merged config, but it cannot inspect the consumer's setup, generate a registry or add a transform. The moment I need that, it is a module. In practice a company toolkit is both — a layer for the scaffold that lists its own module in modules, so the team gets everything from one extends entry. That is safe because modules arrays concatenate and Nuxt deduplicates by module name, and because module options resolve with the project's config beating the layer's beating my defaults."
Rendering modes 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.
Hydration errors
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.