Nuxt Layers

Publishing and consuming a layer

The layer starter and its playground, npm packaging rules, workspace versus registry versus git tags, the dev experience of each, and what counts as a breaking change.

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.

Know

  • Start from the template. 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.
  • npm packaging rules. 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.
  • Workspace first. In a pnpm monorepo, apps depend on "@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.
  • Registry for many repos. A private registry (Verdaccio, GitHub Packages, Artifactory, npm Teams) gives semver ranges, changelogs, Renovate PRs and a clear "which version are you on" answer. extends: ['@team/nuxt-layer-base'] stays identical.
  • Git when there is no registry. 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.
  • Dev experience differs. Local paths and workspace links watch and hot-reload; an npm-installed layer changes only when you publish and bump; a git layer is downloaded once and cached. For fast iteration on a published layer, temporarily point extends at a local checkout.
  • Breaking changes for a layer are anything that forces consumers to edit code or config: renamed or removed components, composables or pages; changed 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.

How it works

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']
pnpm-workspace.yaml
packages:
  - packages/layers/*
  - packages/modules/*
  - apps/*
packages/layers/base/package.json
{
  "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"
  }
}
apps/shop/package.json
{
  "dependencies": {
    "@team/nuxt-layer-base": "workspace:*",
    "@team/nuxt-layer-auth": "workspace:*",
    "nuxt": "^4.5.0"
  }
}
apps/shop/nuxt.config.ts
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:

nuxt.config.ts
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
  ],
})
Gotcha· Exclude the playground, include the source

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.

Gotcha· Nuxt version skew

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.

Verify before the interview:

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.

docs ↗

Exercise

Exercise
  • Turn a scratch layer into @team/nuxt-layer-base with the package.json above. Run npm pack and inspect the tarball: is server/ inside? Is .playground out?
  • In a pnpm workspace, create apps/demo extending the layer with workspace:*. Change a layer component and confirm the app hot-reloads.
  • Publish to a local Verdaccio (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.

Be able to say

Be able to say· How would you distribute a layer to twenty apps in different repositories?

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