Building a shared base layer for multi-brand setups with extends
The extends property in nuxt.config.ts lets you reuse entire Nuxt projects as base layers, including components, composables, layouts, and configuration. For multi-brand setups with a shared core and brand-specific overrides, this is often a lighter-weight solution than a classic monorepo with separate npm packages.
Table of Contents
- 1. What Are Nuxt Layers?
- 2. The extends Property in Detail
- 3. Practical Example: A Base Layer with Shared Components
- 4. Use Case: A Multi-Brand Setup
- 5. Brand-Specific Overrides
- 6. Order and Resolution of Layers
- 7. Difference from a Classic Monorepo with Shared npm Packages
- 8. Limitations and Pitfalls of Nuxt Layers
- 9. Conclusion: Layers as a Lightweight Alternative
- 10. Summary
- 11. FAQ
1. What Are Nuxt Layers?
At its core, a Nuxt layer is nothing more than a self-contained Nuxt directory with the usual folder structure of components, composables, pages, layouts, and its own nuxt.config.ts. Such a layer can be pulled into another Nuxt project as a foundation, making every component, composable, and configuration value inside it automatically available to the consuming project, with no manual import required.
The concept is especially useful in situations where several applications share a substantial part of their functionality but differ in details such as branding, individual pages, or specific features. Instead of sharing code through copy-paste or a separate npm package, the shared core gets extracted as a layer and referenced by each application through the extends property.
2. The extends Property in Detail
The extends property in nuxt.config.ts accepts a path, an array of paths, or even a git source, and loads the referenced layer before the project's own configuration is applied. Nuxt automatically performs a deep merge of configuration options, component directories, composable auto-imports, and layout definitions, so the base layer and the consuming project fuse seamlessly into a single application.
Layers can themselves pull in other layers through their own extends property, which makes it possible to build multi-level hierarchies, for example a very generic base layer shared by all brands, a regional layer built on top for specific countries, and at the very top the concrete, brand-specific application. Each level can deliberately override parts of the levels beneath it.
3. Practical Example: A Base Layer with Shared Components
In the example below, a base layer lives in the layers/base directory and contains a shared header component along with a shared runtime config for the API base URL. Two brand-specific applications pull in this layer via extends and each adds only its own brand-specific pages and color values.
It's worth noting that components from the base layer can be used inside the concrete application exactly like local components, with no extra import needed. If the application overrides a component with the same name locally, the local version automatically takes precedence over the version from the layer.
// layers/base/nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: 'https://api.example.com',
},
},
});
// layers/base/components/AppHeader.vue
// <template><header>Shared header for all brands</header></template>
// apps/brand-a/nuxt.config.ts
export default defineNuxtConfig({
extends: ['../../layers/base'],
app: {
head: { title: 'Brand A' },
},
runtimeConfig: {
public: {
themeColor: '#0066cc',
},
},
});
4. Use Case: A Multi-Brand Setup
A classic multi-brand setup emerges when a company runs several brands with largely identical functionality, such as the same checkout flow, the same product search, and the same account structure, but each with its own branding, its own domain, and sometimes its own extra features. Without a shared foundation, every change to the common logic would have to be replicated in each brand individually, which quickly leads to inconsistencies.
With a base layer, the shared logic, meaning cart composables, common UI components, and shared middleware, is maintained in exactly one place. Each brand-specific application pulls in this layer and adds only what's genuinely brand-specific: logo, color palette, special landing pages, or a different payment provider.
5. Brand-Specific Overrides
Overrides in Nuxt layers work through a filesystem convention: if the concrete application creates a file at the same relative path as in the base layer, for example components/AppHeader.vue, the application's version wins. The same principle applies to pages, layouts, and composables, which lets you replace individual building blocks without duplicating the entire layer.
For configuration values like runtimeConfig, a deep merge applies instead: values from the application add to or override the values from the layer, without losing values that aren't explicitly set. This lets you define sensible defaults in the base layer and adjust only the values that actually differ in the concrete application.
6. Order and Resolution of Layers
When several layers are pulled in through an array in extends, they're processed in the order given, with layers listed later taking precedence over layers listed earlier. The consuming project itself always has the highest priority and can override any value from any layer it pulls in.
In deeply nested layer hierarchies, it pays to document the resolution order, because in practice, bugs often arise from a developer no longer being sure which level a particular configuration value or component actually came from. A clearly named, flat layer tree is usually easier to maintain than many deeply nested levels.
7. Difference from a Classic Monorepo with Shared npm Packages
A classic monorepo approach shares code through self-contained npm packages that need to be versioned, built, and explicitly imported. This brings clean version boundaries, but also adds extra overhead: a shared package has to be rebuilt and updated in the dependent projects before a change becomes visible there.
Nuxt layers skip this build and versioning step entirely. A layer gets pulled directly into the consuming project at build time, and changes to the layer are immediately visible in the dependent project during development, with no separate publish step involved. The tradeoff is looser version control, which is usually a good compromise for tightly collaborating teams in the same repository, but can be a downside for fully independent teams with their own release cycle.
8. Limitations and Pitfalls of Nuxt Layers
A common pitfall is assuming layers behave like independent, isolated modules. In reality, all layers and the consuming project are merged into a single Nuxt application, which means naming collisions between components, composables, or routes across different layers can lead to unexpected behavior when they aren't deliberately intended as an override.
Dependencies from a layer's package.json also aren't installed automatically when the layer is pulled in as a local folder. If a base layer uses a particular library, that library generally also needs to be listed as a dependency in the concrete application's package.json, which is easy to overlook when planning a layer system.
9. Conclusion: Layers as a Lightweight Alternative
Nuxt layers offer a lightweight way to build several closely related applications on a shared foundation, without the organizational overhead of a classic monorepo with separately versioned packages. For multi-brand setups where most of the logic is identical and only branding and a handful of details differ, the concept is particularly well suited.
Anyone working with layers should clearly document the responsibilities between the base layer and the concrete application, and deliberately avoid naming collisions rather than relying blindly on implicit overriding. As complexity grows with many independent teams, it can still make sense later to move to a monorepo with real, versioned packages.
| Aspect | Nuxt Layers (extends) | Monorepo with npm Packages |
|---|---|---|
| Integration | Directly at build time via extends | Explicit import of a package |
| Changes visible | Immediately in dev mode | Only after building and updating the dependency |
| Version control | No separate versioning scheme | Own version numbers per package |
| Setup effort | Low, plain folder structure | Higher, needs a build pipeline per package |
| Best suited for | Tightly collaborating teams, similar apps | Independent teams, own release cycle |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
Nuxt Layers for Shared Configuration: The Essentials at a Glance
Core mechanism
extends property in nuxt.config.ts for a shared base layer
Main effect
Shared components, composables, and config without an npm package
Typical use case
Multi-brand setup with a shared core and brand-specific overrides
Distinction
No build or versioning step unlike classic monorepo packages