Once a layer leaves the repository it was born in, it becomes a dependency, and every question a senior asks about dependencies applies: how is it versioned, how do consumers get updates, how fast is the feedback loop when you change it, and what happens when it breaks. The three distribution channels (workspace, npm registry, git) answer those questions differently, and most teams walk through them in that order.
npm create nuxt -- --template layer @team/nuxt-layer-base scaffolds the layer plus a .playground/ app that extends it. The playground is your dev server and manual test bench; consumers never install it.package.json needs "type": "module", "main": "./nuxt.config.ts" (yes, the config file is the entry), every runtime dependency the layer's code or listed modules need in dependencies, and nuxt in devDependencies (or peerDependencies with a range you actually test). The docs are explicit: "make sure any dependency imported in the layer is explicitly added to the dependencies". Ship the source, not a build: layers are not compiled."@team/nuxt-layer-base": "workspace:*" and extends: ['@team/nuxt-layer-base']. Edits hot-reload in every app, one lockfile pins everything, and publishing later is a pnpm publish away.extends: ['@team/nuxt-layer-base'] stays identical.extends: ['github:team/nuxt-layer-base#v2.3.0'] with GIGET_AUTH in CI and install: true if the layer has dependencies. Tags are your versions; treat them as immutable.extends at a local checkout.app.config defaults that alter rendering; removed or renamed runtimeConfig keys; a higher minimum Nuxt; a module removed from the preset. Semver them like a library and write the migration note before the release.The monorepo layout most teams converge on:
repo/
├─ pnpm-workspace.yaml
├─ packages/
│ ├─ layers/
│ │ ├─ base/ @team/nuxt-layer-base (design system + preset)
│ │ └─ auth/ @team/nuxt-layer-auth (extends base? no: apps compose them)
│ └─ modules/
│ └─ toolkit/ @team/nuxt-toolkit (build-time machinery)
└─ apps/
├─ shop/ extends: ['@team/nuxt-layer-base', '@team/nuxt-layer-auth']
└─ admin/ extends: ['@team/nuxt-layer-base']
packages:
- packages/layers/*
- packages/modules/*
- apps/*
{
"name": "@team/nuxt-layer-base",
"version": "2.3.0",
"type": "module",
"main": "./nuxt.config.ts",
"files": ["nuxt.config.ts", "app", "server", "shared", "public"],
"scripts": {
"dev": "nuxt dev .playground",
"build": "nuxt build .playground",
"test": "vitest run",
"release": "changelogen --release --push && pnpm publish"
},
"dependencies": {
"@nuxt/ui": "^4.11.0",
"@nuxt/eslint": "^1.17.0",
"defu": "^6.1.4"
},
"devDependencies": {
"nuxt": "^4.5.0",
"vitest": "^5.0.0",
"@nuxt/test-utils": "^4.3.0"
},
"peerDependencies": {
"nuxt": "^4.3.0"
}
}
{
"dependencies": {
"@team/nuxt-layer-base": "workspace:*",
"@team/nuxt-layer-auth": "workspace:*",
"nuxt": "^4.5.0"
}
}
export default defineNuxtConfig({
extends: ['@team/nuxt-layer-auth', '@team/nuxt-layer-base'], // auth may override base
})
The same consumer, when the layer moves to a private registry or to git, changes only the source string:
export default defineNuxtConfig({
extends: [
'@team/nuxt-layer-base', // registry: ^2 in package.json
// ['github:team/nuxt-layer-base#v2.3.0', { install: true }], // git alternative
],
})
files (or .npmignore) must exclude .playground, tests and fixtures, and must include nuxt.config.ts, app/, server/, shared/, public/. Forgetting server/ is a common first-publish bug: the playground still works because it sits inside the repo.
A layer built against 4.6 features (a new option, a kit helper) breaks apps on 4.3 with an unhelpful error. Declare peerDependencies.nuxt with the lowest version you test, and run the layer's CI against that lowest version too, not only latest. See Testing and CI.
The exact package.json fields the docs require for npm layers today (main pointing to nuxt.config.ts, dependency rules) and whether files guidance was added.
@team/nuxt-layer-base with the package.json above. Run npm pack and inspect the tarball: is server/ inside? Is .playground out?apps/demo extending the layer with workspace:*. Change a layer component and confirm the app hot-reloads.npx verdaccio), install from it in an app outside the workspace, and repeat the alias exercise from Paths and aliases: this is where alias bugs surface."I would publish it to our private registry as a normal npm package: type: module, main pointing at nuxt.config.ts, every runtime dependency declared, nuxt as a peer with the lowest version we test. Apps add extends: ['@team/nuxt-layer-base'] and Renovate opens the upgrade PRs, so I can see adoption per version. Git tags via giget work without a registry but push tokens and dependency installation onto every consumer's CI. And while the layer is young and only a few apps use it, a pnpm workspace with workspace:* is better than either, because changes hot-reload in every app and nothing needs publishing yet."
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.
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.