"Should this be a layer or a module?" is the question the whole interview orbits, because the job title says layers and the guide you studied says modules. The honest answer is that a team toolkit is usually both, split along one line: layers ship app code and conventions, modules ship build-time behaviour and injected runtime. This page gives you the decision rule, the composition, and the architectural use of layers beyond "shared UI": one layer per domain.
app.config, nuxt.config presets (modules, route rules, Vite options). Zero build tooling, hot reload, override by file placement.addTemplate, addTypeTemplate), transforming source (addBuildPlugin), reacting to hooks (pages:extend, nitro:config), registering things conditionally, validating options, printing warnings, adding DevTools tabs. See Module authoring.if, it is a module. A layer cannot branch on the consumer's options; a module's setup(options, nuxt) can.modules and passes team defaults; the module does the machinery. Consumers see one extends entry. This is how design systems (@nuxt/ui is a module plus a set of conventions), content platforms and auth kits are typically shipped.layers/catalog, layers/checkout, layers/account) gives each team its own pages, components, server routes and stores inside one deployable app, with the project as the shell.server/api paths must be namespaced by convention; one dependency graph, so a heavy library in one domain layer is in everyone's build; cross-domain imports are possible but should be forbidden by lint rules, or the "modules" are modules only on paper.| Need | Layer | Module | Why |
|---|---|---|---|
| Shared components, composables, layouts | ✅ | ➖ | discovered by convention, overridable by path |
| Nuxt config preset (modules, ESLint, fonts, route rules) | ✅ | ➖ | config merges; no code needed |
| Theme tokens with type-safe overrides | ✅ (app.config + AppConfigInput) | ➖ | reactive, merged, typed |
| Generated route/component registries, virtual files | ➖ | ✅ | addTemplate, #build/... |
| Code transforms, compile-time flags | ➖ | ✅ | addBuildPlugin, vite.define |
| "Enable X only when option Y" | ➖ | ✅ | needs setup() logic |
| Auth pages + middleware + session composable | ✅ | ➖ | app code; server routes shipped alongside |
| Validation of team config, helpful errors | ➖ | ✅ | useLogger, throw in setup |
| Whole product domain (pages, API, store) | ✅ | ➖ | DDD layer inside one app |
| DevTools tab, CLI integration | ➖ | ✅ | build-world APIs |
The composed toolkit as consumers see it:
export default defineNuxtConfig({
extends: ['@team/nuxt-layer-base'], // components, composables, presets…
teamToolkit: { analytics: true }, // …and options for the module the layer brought along
})
export default defineNuxtConfig({
modules: ['@team/nuxt-toolkit', '@nuxt/ui'],
teamToolkit: { analytics: false, generateRouteMap: true }, // team defaults, app overrides
})
Domain layers inside one app:
apps/shop/
├─ nuxt.config.ts extends: ['@team/nuxt-layer-base'] (auto-scans layers/)
├─ app/app.vue the shell
└─ layers/
├─ 1.catalog/
│ ├─ app/pages/products/** routes owned by the catalog team
│ ├─ app/components/Catalog*/
│ └─ server/api/catalog/**
├─ 2.checkout/
└─ 3.account/
// Keep domains honest: a domain layer may import from the base layer, never from a sibling domain.
export default [{
files: ['layers/*/**/*.{ts,vue}'],
rules: {
'no-restricted-imports': ['error', {
patterns: [{ group: ['#layers/[0-9].*'], message: 'Cross-domain imports are not allowed; use the base layer or an event.' }],
}],
},
}]
Publishing TeamButton as a plain Vue package works, but you lose auto-registration, app.config theming, the Nuxt-aware nuxt.config preset, server routes and override-by-path. If the team's stack is Nuxt, the layer is the more capable unit; keep a plain library only for consumers outside Nuxt.
Two teams both adding app/pages/index.vue in their domain layer do not get two home pages; the higher-priority layer silently wins. Namespace routes (/catalog/...) and API paths per domain, and decide up front who owns the shell pages.
/admin). For each, decide layer or module in one sentence using the if rule.layers/1.catalog and layers/2.checkout, each with its own pages and a server/api/<domain>/ route. Add the ESLint rule above and try a cross-domain import.@team/nuxt-layer-base."Layers ship app code and conventions: components, composables, pages, server routes, config presets, theme tokens, all discovered by convention and overridable by placing a file. Modules ship build-time behaviour: generated files, transforms, hooks, conditional registration. My rule is that if it needs an if, it is a module. A team toolkit is usually both, with the layer listing the module so consumers get everything from one extends. Layers also work as architecture: one layer per product domain inside one app, which gives ownership boundaries and file-based routing per team, as long as routes and API paths are namespaced and cross-domain imports are forbidden."
Testing a layer 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.
Pitfalls catalogue
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.