Layers & Monorepo
TL;DR
Nuxt Layers let you compose and extend applications. A layer provides components, composables, pages, server routes, and config that merge into the consuming app. The consuming app can override anything. Combined with monorepo workspaces, layers enable shared base apps, theme packages, and modular architecture. Think of it as inheritance for Nuxt apps.
Mental Model
Base Layer (shared foundation)
βββ provides: components, composables, layouts, config
β
βββ App A (extends base, overrides some components)
βββ App B (extends base, different theme/config)
Layers follow a cascade model:
- Base layer provides defaults
- Additional layers override/extend
- The consuming app always wins (highest priority)
This is like CSS specificity but for your entire app structure. Same-name files in the consumer override the layerβs version.
extends in nuxt.config
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'./layers/base', // Local directory
'@my-org/nuxt-base-layer', // npm package
'github:my-org/nuxt-theme#main', // Git repository
],
})
Resolution order (lowest to highest priority):
- First layer in
extendsarray - Second layer in
extendsarray - β¦ subsequent layers
- The consuming app itself (always wins)
Multiple layers stack. Later layers override earlier ones. Your app overrides all layers.
Creating a Base Layer
A layer is just a Nuxt-compatible directory structure. Minimum: a nuxt.config.ts:
layers/base/
βββ nuxt.config.ts β Required (marks this as a layer)
βββ components/
β βββ BaseButton.vue
β βββ BaseCard.vue
β βββ AppHeader.vue
βββ composables/
β βββ useAuth.ts
βββ layouts/
β βββ default.vue
βββ middleware/
β βββ auth.ts
βββ server/
β βββ api/
β βββ health.get.ts
βββ assets/
β βββ css/
β βββ base.css
βββ app.config.ts
// layers/base/nuxt.config.ts
export default defineNuxtConfig({
// Config provided by this layer
modules: ['@pinia/nuxt'],
css: ['~/assets/css/base.css'], // ~ resolves to the layer's root
runtimeConfig: {
public: {
appName: 'Default App Name',
},
},
})
Now any app that extends this layer gets all those components, composables, layouts, middleware, server routes, and config merged in β without copying code.
What Layers Can Provide
| Feature | Behavior |
|---|---|
| Components | Added to component auto-import scan |
| Composables | Added to composable auto-import scan |
| Utils | Added to utils auto-import scan |
| Pages | Merged into the route table |
| Layouts | Available for use via definePageMeta |
| Middleware | Available for use via definePageMeta |
| Server routes | Merged into Nitro server |
| Plugins | Auto-registered |
| Assets/CSS | Available via ~ alias (layerβs root) |
nuxt.config.ts | Deep-merged with consuming app |
app.config.ts | Deep-merged with consuming app |
Overriding Layer Content
Same-name files in the consuming app override the layer:
layers/base/
βββ components/
β βββ AppHeader.vue β Layer's version
my-app/
βββ components/
β βββ AppHeader.vue β This wins (overrides layer)
layers/base/
βββ composables/
β βββ useAuth.ts β Layer's version (basic auth)
my-app/
βββ composables/
β βββ useAuth.ts β This wins (custom auth logic)
Config merging β deep merge with consuming app winning:
// layers/base/nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
runtimeConfig: {
public: { appName: 'Base App', theme: 'light' },
},
})
// my-app/nuxt.config.ts
export default defineNuxtConfig({
extends: ['./layers/base'],
runtimeConfig: {
public: { appName: 'My Custom App' }, // Overrides, theme: 'light' preserved
},
})
// Result: { appName: 'My Custom App', theme: 'light' }
Config-Only Layers
A layer that provides shared configuration without components or pages:
layers/org-config/
βββ nuxt.config.ts
βββ package.json
// layers/org-config/nuxt.config.ts
export default defineNuxtConfig({
// Organization-wide standards
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@nuxt/image',
],
typescript: {
strict: true,
typeCheck: true,
},
devtools: { enabled: true },
runtimeConfig: {
public: {
sentryDsn: '',
analyticsId: '',
},
},
nitro: {
compressPublicAssets: true,
},
})
Every app in the organization extends this layer β guaranteed consistent tooling, TypeScript settings, and module stack.
Monorepo Setup
Using pnpm workspaces (recommended):
my-monorepo/
βββ package.json
βββ pnpm-workspace.yaml
βββ layers/
β βββ base/
β β βββ package.json β name: "@my-org/base-layer"
β β βββ nuxt.config.ts
β β βββ components/...
β βββ admin/
β βββ package.json β name: "@my-org/admin-layer"
β βββ nuxt.config.ts
β βββ pages/...
βββ apps/
βββ storefront/
β βββ package.json β depends on @my-org/base-layer
β βββ nuxt.config.ts β extends: ['@my-org/base-layer']
β βββ pages/...
βββ admin-panel/
βββ package.json β depends on @my-org/base-layer, @my-org/admin-layer
βββ nuxt.config.ts β extends: ['@my-org/base-layer', '@my-org/admin-layer']
βββ pages/...
# pnpm-workspace.yaml
packages:
- 'layers/*'
- 'apps/*'
// layers/base/package.json
{
"name": "@my-org/base-layer",
"version": "1.0.0",
"type": "module"
}
// apps/storefront/package.json
{
"name": "@my-org/storefront",
"dependencies": {
"@my-org/base-layer": "workspace:*"
}
}
// apps/storefront/nuxt.config.ts
export default defineNuxtConfig({
extends: ['@my-org/base-layer'],
// App-specific config...
})
Hot reload works across packages. When you edit a component in layers/base/, apps that extend it hot-reload automatically (pnpm workspace symlinks enable this).
Layer Development Tips
Testing a layer in isolation:
// layers/base/nuxt.config.ts
export default defineNuxtConfig({
// When running the layer standalone (npm run dev in the layer dir)
// it needs to be a complete app:
devtools: { enabled: true },
})
// layers/base/package.json
{
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build"
}
}
You can cd layers/base && npm run dev to develop the layer in isolation before using it from apps.
Layer-relative paths:
// Inside a layer, ~ resolves to the LAYER's root
// layers/base/nuxt.config.ts
export default defineNuxtConfig({
css: ['~/assets/css/base.css'], // β layers/base/assets/css/base.css
})
In components within the layer, ~/ also resolves to the layer root. But #imports and auto-imports resolve globally across all layers + app.
Publishing Layers as Packages
For sharing across teams/projects:
// layers/base/package.json
{
"name": "@my-org/nuxt-base-layer",
"version": "2.1.0",
"type": "module",
"main": "./nuxt.config.ts",
"files": [
"nuxt.config.ts",
"components",
"composables",
"layouts",
"middleware",
"server",
"assets",
"app.config.ts"
],
"peerDependencies": {
"nuxt": "^4.0.0"
}
}
Publish to npm (or a private registry). Consumers install and extend:
npm install @my-org/nuxt-base-layer
// Consumer's nuxt.config.ts
export default defineNuxtConfig({
extends: ['@my-org/nuxt-base-layer'],
})
Multi-App Architecture Patterns
Pattern 1: Shared Base + App Variants
base-layer β storefront (e-commerce)
β admin-panel (internal tool)
β docs-site (documentation)
All apps share auth, design system, utilities. Each has unique pages and features.
Pattern 2: Feature Layers
app extends [auth-layer, billing-layer, analytics-layer, ui-layer]
Each layer encapsulates one feature domain. The app composes them together.
Pattern 3: Theme Layers
app extends [base-layer, dark-theme-layer]
Theme layer overrides CSS, layout, and design tokens from the base.
Walkthrough
- Create a base layer:
mkdir -p layers/base/{components,composables,layouts}Add
nuxt.config.ts, a default layout, and a sharedBaseButton.vuecomponent. - Create a consuming app:
mkdir apps/mainExtend the base layer. Verify the layout and components are available without copying.
-
Override a component: Create
apps/main/components/BaseButton.vuewith different styling. Verify it replaces the layerβs version. -
Set up the monorepo: Add
pnpm-workspace.yaml. Link packages. Runpnpm devfrom the app β verify hot-reload works when editing layer files. - Add a config-only layer: Create
layers/org-config/with shared module and TypeScript settings. Extend from both apps.
Gotchas
- Deep merge surprises. Config from layers merges deeply. Arrays are concatenated, not replaced. If both the layer and app define
modules: [...], you get BOTH arrays combined. Use layer config sparingly and predictably. - Component name collisions. If two layers provide
Button.vue, the later layer wins. Use prefixes (BaseButton,AdminButton) to avoid unintentional overrides. - Layers canβt be conditional. Once in
extends, a layer always applies. For conditional features, use modules (which can check config/env and decide what to add). - Git-based layers fetch at install time.
extends: ['github:org/repo']downloads atnpm install, not live-linked. For development, use local paths or workspace references. - Layer pages merge into the route table. If a layer provides
pages/admin.vueand your app haspages/admin.vue, your appβs version wins. But if only the layer provides it, it appears in your appβs routes. - Monorepo hot-reload requires workspace protocol. Use
"@my-org/base": "workspace:*"in package.json, not a version number. This creates symlinks that enable file watching. - Layer extends order matters.
extends: ['A', 'B']β B overrides A. Your app overrides both. If A and B both provideHeader.vue, Bβs version is used. - Donβt over-layer. Layers add complexity. Use them when you genuinely have multiple apps sharing code. A single app doesnβt need layers β just organize with directories.
- Debug with
nuxi info. Shows resolved layer stack, merged config, and active modules. Use when layer interactions are confusing.
Exercise
Build a monorepo with layers:
- Base layer (
layers/base/):- Shared components:
BaseButton,BaseCard,BaseModal - Default layout with header/footer
- Auth middleware +
useAuthcomposable - Health check server route
- TypeScript strict mode in config
- Shared components:
- Admin app (
apps/admin/):- Extends base layer
- Overrides the default layout (adds sidebar)
- Adds admin-specific pages:
/users,/settings - Uses auth middleware from the base
- Storefront app (
apps/storefront/):- Extends base layer
- Overrides
BaseButtonwith different styling - Adds store-specific pages:
/products,/cart - Different
app.config.ts(different theme colors)
- Verify:
- Both apps have the shared components and middleware
- Override in storefront doesnβt affect admin
- Hot-reload works across layer boundaries
nuxi infoshows the resolved layer stack