Everything before this page was mechanism. This page is judgement: what makes a layer something twenty apps want to extend, keep extending after the third breaking change, and can safely be owned by one small platform team. The recurring theme is that a layer is an API whose surface is files, so every file you ship is a promise.
app.config keys, runtimeConfig keys, page routes and layouts, server route paths, and the modules in the preset. Anything else is internal and should be marked as such (an _internal/ component folder, pathPrefix to hide it, no README mention).TeamButton, useTeamAuth, /api/_team/health, team.* in app.config, NUXT_TEAM_* in the environment. Prefixes prevent collisions with apps and with other layers, and make grep-based adoption tracking possible.app.config is the theme API. Tokens and UI defaults live there, typed through AppConfigInput, reactive, overridable per app by placing a file. Anything per-environment goes to runtimeConfig instead (why).#layers/base/..., and what breaks if you replace it (patterns). Overrides you did not anticipate become support tickets.base (preset + design system + cross-cutting server pieces) → optional feature layers (auth, analytics, cms) → apps. Feature layers must not extend each other; apps compose them. The base layer changes least and is reviewed hardest.runtimeConfig keys the app fills, dependencies watched by Renovate and advisories (Senior craft).The theme API, typed:
export default defineAppConfig({
team: {
brand: 'Acme',
density: 'comfortable',
analytics: { enabled: false },
},
ui: { colors: { primary: 'emerald', neutral: 'zinc' } },
})
declare module 'nuxt/schema' {
interface AppConfigInput {
team?: {
brand?: string
density?: 'compact' | 'comfortable'
analytics?: { enabled?: boolean }
}
}
}
export {}
A deprecation wrapper that keeps the old name working for one major:
<script setup lang="ts">
// Deprecated in 2.x, removed in 3.0. Use <TeamButton variant="primary">.
import TeamButton from './Button.vue'
if (import.meta.dev) {
console.warn('[nuxt-layer-base] <TeamPrimaryButton> is deprecated; use <TeamButton variant="primary">. See MIGRATION.md#2-1.')
}
</script>
<template>
<TeamButton variant="primary" v-bind="$attrs">
<slot />
</TeamButton>
</template>
Budgeting the layer's cost in the layer's own CI:
# Build an empty fixture with and without the layer and compare the client entry size and a cold TTFB.
pnpm nuxt build test/fixtures/empty && du -sh test/fixtures/empty/.output/public/_nuxt
pnpm nuxt build test/fixtures/basic && du -sh test/fixtures/basic/.output/public/_nuxt
hyperfine --warmup 3 'curl -s http://localhost:3000/ -o /dev/null'
The one-page RFC every base-layer change goes through:
# RFC: <change>
**Problem** — what apps cannot do today, with the two apps that asked.
**Proposal** — the public surface after the change (component/composable/app.config keys), with the override recipe.
**Compatibility** — breaking? deprecation path, codemod, migration note, target major.
**Cost** — bundle/TTFB delta measured on the empty fixture; new dependencies.
**Security** — new server routes/middleware and how they are guarded.
**Rollout** — canary app, adoption tracking, support window for the previous major.
The fastest way to rot a base layer is to accept "just add a prop for our app". Every consumer pays for it forever. Push app-specific needs to the app (override or wrap), or to a feature layer that only that family of apps extends.
If you cannot say which apps still use <TeamPrimaryButton>, you cannot remove it. Track versions from the lockfiles in CI (a script that reads each app's pnpm-lock.yaml) or from a runtime header the layer's health route reports, before the first deprecation.
The current recommended way to type app.config from a layer (AppConfigInput augmentation in nuxt/schema) and any notes on merging strategies for arrays in app.config.
@team/nuxt-layer-base: what it ships (tables of components, composables, app.config keys, runtimeConfig keys, routes), install, the three override recipes, troubleshooting (from the pitfalls table), versioning policy.TeamDataTable using the template above, including a measured bundle delta."I would start by writing down its public surface: prefixed components and composables, typed app.config keys as the theme API, runtimeConfig keys for per-environment values, namespaced server routes, and the module preset. Everything else is internal. Apps customise by configuring first, wrapping second, replacing last, and the README shows all three. I measure what the layer costs an empty app in bundle size and TTFB and budget it in CI, treat every shipped server route as production code in twenty apps, and put the base layer under one owning team with a one-page RFC for surface changes. Breaking changes go deprecate → warn → codemod → remove across a major, with adoption tracked so removal is a decision, not a guess."
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.
Module authoring
What a Nuxt module really is, the three worlds every line of module code lives in, the three primitives everything reduces to, and why a Layers role is a module role in disguise.