Nuxt Layers

Layers vs modules, and layers as architecture

The decision rule, the cases where a team needs both, domain-driven design with one layer per domain, and the places where a layer is the wrong tool.

"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.

Know

  • What a layer is good at: anything Nuxt discovers by convention. Components, composables, utils, pages, layouts, middleware, plugins, server handlers, app.config, nuxt.config presets (modules, route rules, Vite options). Zero build tooling, hot reload, override by file placement.
  • What a module is good at: anything that needs to happen at build time. Reading the file system, generating code or types (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.
  • The rule of thumb: if it needs an if, it is a module. A layer cannot branch on the consumer's options; a module's setup(options, nuxt) can.
  • Both, composed: the layer lists the module in 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 as architecture: the docs list "modular architecture and Domain-Driven Design patterns in large-scale projects" as a use case. One layer per domain (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.
  • The costs of domain layers: one router and one Nitro server, so route paths and 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.
  • Where a layer is wrong: conditional behaviour, generated code, transforms, anything versioned separately from the app's UI (a build-time tool used by non-Nuxt projects), and anything that must not be overridable by placing a file (security middleware that an app could accidentally shadow).

How it works

NeedLayerModuleWhy
Shared components, composables, layoutsdiscovered 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 filesaddTemplate, #build/...
Code transforms, compile-time flagsaddBuildPlugin, vite.define
"Enable X only when option Y"needs setup() logic
Auth pages + middleware + session composableapp code; server routes shipped alongside
Validation of team config, helpful errorsuseLogger, throw in setup
Whole product domain (pages, API, store)DDD layer inside one app
DevTools tab, CLI integrationbuild-world APIs

The composed toolkit as consumers see it:

apps/shop/nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@team/nuxt-layer-base'],   // components, composables, presets…
  teamToolkit: { analytics: true },     // …and options for the module the layer brought along
})
packages/layers/base/nuxt.config.ts
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/
eslint.config.mjs
// 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.' }],
    }],
  },
}]
Gotcha· A component library is not a layer

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.

Gotcha· Domain layers are still one app

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.

Exercise

Exercise
  • Take three real requirements from a past project (for example: shared header, "drop console.log in production", auth guard on /admin). For each, decide layer or module in one sentence using the if rule.
  • Split a scratch app into 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.
  • Write the one-paragraph README section "Why a layer and not an npm component library" for @team/nuxt-layer-base.

Be able to say

Be able to say· Layer or module: how do you decide, and how do they combine in a team toolkit?

"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."