Nuxt Layers

Anatomy of a layer

A layer is a Nuxt app directory that another app merges in. What goes where, how Nuxt resolves its srcDir, and how to see the resolved layer list.

The fastest way to understand a layer is to notice that you have already written one: every Nuxt project is a layer, and the project itself is the first entry of the resolved layer list. A layer therefore has no special file format. It is a directory that Nuxt knows how to scan, and the only hard requirement is a nuxt.config.ts, even an empty one, so that Nuxt recognises the directory as a layer.

Know

  • Minimal layer = a folder with nuxt.config.ts. Without that file the folder is ignored silently, which is the first thing to check when "the layer does nothing".
  • The structure mirrors an app. In Nuxt 4 that means app code under app/ and everything else at the root:
    Path in the layerScanned asNotes
    app/components/componentssame auto-import + path-prefix naming as the app
    app/composables/, app/utils/auto-importsname collisions with the app are resolved by priority
    app/pages/, app/layouts/, app/middleware/, app/plugins/routes, layouts, middleware, pluginspages are additive; same route → higher layer wins
    app/app.config.tsapp configdeep-merged, project wins
    app/app.vue, app/error.vueshellused only if the project has none
    server/Nitro handlers, middleware, plugins, utilsscanned across all layers
    shared/shared/utils, shared/types auto-importsNuxt ≥ 3.14
    public/static assetsmerged
    modules/local modulesauto-registered for the project; per-layer behaviour is worth confirming (see below)
    nuxt.config.tsconfigmerged with c12 + defu
  • srcDir is resolved per layer. A layer written for Nuxt 4 keeps its app code in app/; Nuxt detects that folder and uses it as the layer's srcDir. A layer still on the Nuxt 3 layout (top-level components/, pages/) keeps its root as srcDir, so mixed-generation layers can coexist.
  • Every app is a layer. nuxt.options._layers is an array of { cwd, config, configFile, meta }; index 0 is the project, followed by the layers in priority order. Kit exposes the same list resolved to directories with getLayerDirectories(), whose entries carry root, app, server, shared, public, modules, appPages, appLayouts, appMiddleware, appPlugins.
  • Names and aliases. A layer can name itself with $meta: { name: 'base' }; layers inside ~~/layers/ are named after their folder automatically. Named layers get an import alias, #layers/base/..., which resolves to the layer root and is the only safe way to import a layer file by path from the project (Nuxt ≥ 3.16).
  • The starter template ships the canonical shape: npm create nuxt -- --template layer my-layer gives you nuxt.config.ts, an app/ folder with a sample component and composable, a .playground/ app that extends .., and a package.json whose main is ./nuxt.config.ts. The playground is where you develop the layer; consumers never see it.

How it works

npm create nuxt -- --template layer nuxt-team-base
cd nuxt-team-base && pnpm install
pnpm dev            # runs `nuxt dev .playground`, the playground extends the layer
nuxt-team-base/
├─ nuxt.config.ts          # the layer's own config: modules, css, runtimeConfig defaults
├─ app/
│  ├─ app.config.ts        # theme tokens, defaults every app inherits
│  ├─ components/Team/     # prefixed components -> <TeamButton>
│  ├─ composables/         # useTeamAuth(), useTeamFlags()
│  └─ utils/
├─ server/api/_team/       # namespaced routes shipped to every app
├─ shared/types/           # types used by app and server
├─ .playground/            # a real Nuxt app with `extends: ['..']`
└─ package.json            # "main": "./nuxt.config.ts", "type": "module"

Seeing the resolved list is a one-liner in a local module, and a useful thing to have done before an interview:

modules/print-layers.ts
import { defineNuxtModule, getLayerDirectories } from 'nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'print-layers' },
  setup(_options, nuxt) {
    // Index 0 is the project. Earlier entries override later ones.
    for (const [i, layer] of getLayerDirectories(nuxt).entries()) {
      console.log(i, layer.root, '→ app:', layer.app, 'server:', layer.server)
    }
  },
})

In this repository the equivalent output has two entries: the project and layers/base. Open layers/base/nuxt.config.ts and note what it does not contain: no pages, no plugins, no extends. A design-system layer can be that small.

Gotcha· A layer's config file is not its entry point

Nothing "runs" a layer. nuxt.config.ts is loaded by c12 and merged; the directories are scanned. If you need to compute something (read the file system, branch on options), that logic belongs in a module, which the layer can list in its modules array. Keep layer configs declarative, and keep console.log out of them: they execute once per config load, in Node, and only there.

Verify before the interview:

Whether a modules/ directory inside a layer is auto-registered. The modules/ docs describe the project's directory; confirm the per-layer behaviour in the layer authoring guide or by testing it in a playground.

docs ↗

Exercise

Exercise
  • Scaffold nuxt-team-base with the layer template, read .playground/nuxt.config.ts and package.json, and write down what main points to and why.
  • Add modules/print-layers.ts from above to the playground, run pnpm dev, and confirm the project appears at index 0 and the layer at index 1.
  • Delete the layer's nuxt.config.ts, restart, and observe that the layer silently disappears from the list. Put it back.

Be able to say

Be able to say· What does a layer consist of?

"A layer is just a Nuxt app directory: a nuxt.config.ts, an app/ folder with components, composables, pages and app.config, a server/ folder, public/, shared/. Nuxt resolves each layer's srcDir, scans the same conventions it scans in the project, and merges the configs. The project is the first item in _layers, so everything a layer ships is a default the project can override. The only hard requirement is the config file; without it the folder is not a layer at all."