Nuxt 4 Layer Modular Architecture: Enterprise Frontend Engineering Complete Guide 2026
Nuxt 4 Layer Modular Architecture: Enterprise Frontend Engineering Complete Guide 2026
When your frontend team scales from 5 to 50 people, when your project evolves from a monolith to a 10+ sub-product matrix, the traditional Nuxt project structure quickly spirals out of control. Code duplication, inconsistent configurations, version fragmentation... these problems can't be solved by coding standards alone — they require architectural solutions. Nuxt 4's Layer mechanism is built for exactly this — it lets you compose frontend capabilities like building blocks, achieving true enterprise-grade modularity.
Core Concepts at a Glance
| Concept | Description | Use Case |
|---|---|---|
| Nuxt Layer | Reusable Nuxt application fragment containing config/components/composables | Cross-project reuse |
extends |
Declare Layer dependencies in nuxt.config | Layer composition |
nuxt.config.ts |
Layer entry configuration file | Layer definition |
| Layer Priority | Later-declared Layers override earlier ones | Conflict resolution |
| App Config | Runtime-overridable configuration | Environment differentiation |
| Module vs Layer | Module is a plugin extension, Layer is an application fragment | Different abstraction levels |
| Workspace | Multi-package monorepo with shared dependencies | Enterprise development |
| Layer Publishing | Distribute Layers as npm packages | Cross-team reuse |
Five Key Pain Points
- Severe Code Duplication: Across 10 sub-products, login modules, permission systems, and layout components are duplicated — fixing one bug means changing 10 repos
- Inconsistent Configuration: Different projects have different ESLint, TypeScript, and Tailwind configs, increasing onboarding costs
- Version Fragmentation: Shared logic spreads through copy-paste, making unified upgrades impossible and accumulating technical debt
- Team Collaboration Difficulties: Multiple teams maintain separate projects without unified development paradigms
- Low Deployment Efficiency: Each project has independent build/deploy pipelines, wasting CI/CD resources
Step-by-Step: 5 Core Patterns
Pattern 1: Layer Basic Configuration
Runtime: Node.js 20+ / Nuxt 4.0+ / pnpm 9+
Create a base Layer with shared components, composables, and configuration:
# Create Layer project
mkdir nuxt-layer-base && cd nuxt-layer-base
pnpm init
pnpm add -D nuxt@latest
// nuxt.config.ts - Layer entry configuration
export default defineNuxtConfig({
compatibilityDate: '2026-06-21',
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
'@pinia/nuxt',
'@vueuse/nuxt',
],
tailwindcss: {
configPath: '~/tailwind.config.ts',
},
appConfig: {
ui: {
primary: 'blue',
theme: 'light',
},
api: {
baseURL: process.env.API_BASE_URL || '/api',
},
},
typescript: {
strict: true,
shim: false,
},
css: [
'~/assets/css/main.css',
],
plugins: [
'~/plugins/api.client.ts',
],
routeRules: {
'/admin/**': { ssr: false },
},
})
// composables/useApi.ts - Shared API request composable
interface UseApiOptions {
baseUrl?: string
headers?: Record<string, string>
timeout?: number
}
interface ApiResponse<T> {
data: Ref<T | null>
error: Ref<Error | null>
pending: Ref<boolean>
execute: () => Promise<void>
}
export function useApi<T>(
url: string,
options: UseApiOptions = {}
): ApiResponse<T> {
const config = useAppConfig()
const baseUrl = options.baseUrl || config.api.baseURL
const timeout = options.timeout || 30000
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const pending = ref(false)
const execute = async () => {
pending.value = true
error.value = null
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
const response = await fetch(`${baseUrl}${url}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
signal: controller.signal,
})
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`)
}
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
pending.value = false
}
}
return { data, error, pending, execute }
}
<!-- components/BaseButton.vue - Shared base button component -->
<script setup lang="ts">
interface BaseButtonProps {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<BaseButtonProps>(), {
variant: 'primary',
size: 'md',
loading: false,
disabled: false,
})
const variantClasses: Record<string, string> = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
danger: 'bg-red-600 hover:bg-red-700 text-white',
ghost: 'bg-transparent hover:bg-gray-100 text-gray-600',
}
const sizeClasses: Record<string, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
</script>
<template>
<button
:class="[
'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
variantClasses[variant],
sizeClasses[size],
]"
:disabled="disabled || loading"
>
<svg
v-if="loading"
class="animate-spin -ml-1 mr-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<slot />
</button>
</template>
Pattern 2: Multi-Layer Composition
In enterprise projects, you typically need to compose multiple Layers:
// nuxt.config.ts - Main app configuration, composing multiple Layers
export default defineNuxtConfig({
extends: [
'@company/nuxt-layer-base',
'@company/nuxt-layer-auth',
'@company/nuxt-layer-admin',
'./layers/custom',
],
})
// layers/auth/nuxt.config.ts - Auth Layer
export default defineNuxtConfig({
extends: ['@company/nuxt-layer-base'],
modules: [
'@sidebase/nuxt-auth',
],
auth: {
provider: {
type: 'local',
endpoints: {
signIn: { path: '/auth/login', method: 'post' },
signOut: { path: '/auth/logout', method: 'post' },
signUp: { path: '/auth/register', method: 'post' },
getSession: { path: '/auth/session', method: 'get' },
},
token: {
signInResponseTokenPointer: '/token/access_token',
headerName: 'Authorization',
type: 'Bearer',
},
},
globalAppMiddleware: true,
},
pages: true,
routeRules: {
'/login': { auth: false },
'/register': { auth: false },
},
})
// composables/usePermission.ts - Permission check composable
interface Permission {
code: string
name: string
description?: string
}
interface Role {
id: string
name: string
permissions: Permission[]
}
interface UserInfo {
id: string
name: string
email: string
roles: Role[]
}
export function usePermission() {
const { data: user } = useAuth()
const userPermissions = computed<Set<string>>(() => {
if (!user.value) return new Set()
const perms = (user.value as UserInfo).roles.flatMap(
(role: Role) => role.permissions.map((p: Permission) => p.code)
)
return new Set(perms)
})
const hasPermission = (code: string): boolean => {
return userPermissions.value.has(code)
}
const hasAnyPermission = (codes: string[]): boolean => {
return codes.some((code) => userPermissions.value.has(code))
}
const hasAllPermissions = (codes: string[]): boolean => {
return codes.every((code) => userPermissions.value.has(code))
}
const hasRole = (roleName: string): boolean => {
if (!user.value) return false
return (user.value as UserInfo).roles.some(
(r: Role) => r.name === roleName
)
}
return {
userPermissions,
hasPermission,
hasAnyPermission,
hasAllPermissions,
hasRole,
}
}
<!-- components/PermissionGuard.vue - Permission guard component -->
<script setup lang="ts">
interface PermissionGuardProps {
permission?: string
permissions?: string[]
mode?: 'any' | 'all'
role?: string
fallback?: boolean
}
const props = withDefaults(defineProps<PermissionGuardProps>(), {
mode: 'any',
fallback: false,
})
const { hasPermission, hasAnyPermission, hasAllPermissions, hasRole } = usePermission()
const isAllowed = computed(() => {
if (props.role) return hasRole(props.role)
if (props.permission) return hasPermission(props.permission)
if (props.permissions) {
return props.mode === 'all'
? hasAllPermissions(props.permissions)
: hasAnyPermission(props.permissions)
}
return true
})
</script>
<template>
<slot v-if="isAllowed" />
<slot v-else name="fallback">
<span v-if="fallback" class="text-gray-400 text-sm">No permission</span>
</slot>
</template>
Pattern 3: Module Publishing and Reuse
Publish Layers as npm packages for cross-team reuse:
// package.json - Layer package configuration
{
"name": "@company/nuxt-layer-base",
"version": "2.1.0",
"description": "Enterprise Nuxt base Layer - UI components, utilities, base config",
"type": "module",
"exports": {
".": {
"import": "./nuxt.config.ts",
"require": "./nuxt.config.ts"
},
"./components/*": "./components/*",
"./composables/*": "./composables/*"
},
"files": [
"components",
"composables",
"layouts",
"plugins",
"assets",
"utils",
"nuxt.config.ts",
"app.config.ts",
"tailwind.config.ts"
],
"peerDependencies": {
"nuxt": "^4.0.0",
"@nuxtjs/tailwindcss": "^6.0.0",
"@pinia/nuxt": "^0.9.0"
},
"keywords": ["nuxt", "nuxt-layer", "ui-components", "enterprise"],
"license": "MIT"
}
// app.config.ts - Runtime-overridable configuration
export default defineAppConfig({
ui: {
primary: 'blue',
theme: 'light',
radius: 'rounded-lg',
animations: true,
},
toast: {
duration: 3000,
position: 'top-right' as const,
},
table: {
pageSize: 20,
pageSizeOptions: [10, 20, 50, 100],
},
api: {
baseURL: '/api',
timeout: 30000,
retryCount: 3,
retryDelay: 1000,
},
})
Pattern 4: Enterprise Layer Template
Create standardized enterprise Layer templates for unified development paradigms:
// nuxt.config.ts - Standard Layer template configuration
export default defineNuxtConfig({
compatibilityDate: '2026-06-21',
meta: {
name: '@company/nuxt-layer-payment',
version: '1.0.0',
description: 'Payment module Layer',
},
extends: ['@company/nuxt-layer-base'],
modules: [],
typescript: {
strict: true,
},
pages: true,
runtimeConfig: {
paymentSecretKey: '',
paymentWebhookSecret: '',
public: {
paymentProvider: 'stripe',
paymentCurrency: 'CNY',
paymentMethods: ['alipay', 'wechat', 'card'],
},
},
})
// composables/usePayment.ts - Payment core composable
interface PaymentOptions {
amount: number
currency?: string
description?: string
metadata?: Record<string, string>
}
interface PaymentResult {
id: string
status: 'pending' | 'completed' | 'failed' | 'refunded'
paymentUrl?: string
}
export function usePayment() {
const config = useRuntimeConfig()
const { $api } = useNuxtApp()
const createPayment = async (options: PaymentOptions): Promise<PaymentResult> => {
const response = await $api('/payments/create', {
method: 'POST',
body: {
amount: options.amount,
currency: options.currency || config.public.paymentCurrency,
description: options.description,
metadata: options.metadata,
},
})
return response as PaymentResult
}
const verifyPayment = async (paymentId: string): Promise<PaymentResult> => {
const response = await $api(`/payments/${paymentId}/verify`, {
method: 'GET',
})
return response as PaymentResult
}
const refundPayment = async (
paymentId: string,
reason?: string
): Promise<PaymentResult> => {
const response = await $api(`/payments/${paymentId}/refund`, {
method: 'POST',
body: { reason },
})
return response as PaymentResult
}
return {
createPayment,
verifyPayment,
refundPayment,
}
}
Pattern 5: Production-Level Layer Governance
Establish Layer version management, quality assurance, and publishing workflows:
# .github/workflows/layer-release.yml
name: Layer Release
on:
push:
tags:
- 'layer-*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm lint
- run: pnpm typecheck
publish:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm publish --no-git-checks --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
// scripts/layer-check.ts - Layer quality check script
import { readdirSync } from 'fs'
import { resolve, join } from 'path'
interface LayerCheckResult {
name: string
hasNuxtConfig: boolean
hasAppConfig: boolean
hasReadme: boolean
hasTypes: boolean
hasTests: boolean
componentCount: number
composableCount: number
score: number
}
function checkLayer(layerPath: string): LayerCheckResult {
const name = layerPath.split('/').pop() || ''
const files = readdirSync(layerPath)
const hasNuxtConfig = files.includes('nuxt.config.ts')
const hasAppConfig = files.includes('app.config.ts')
const hasReadme = files.includes('README.md')
const hasTypes = files.includes('types') || files.includes('index.d.ts')
const hasTests = files.includes('__tests__') || files.includes('tests')
let componentCount = 0
let composableCount = 0
try {
componentCount = readdirSync(join(layerPath, 'components')).length
} catch {}
try {
composableCount = readdirSync(join(layerPath, 'composables')).length
} catch {}
let score = 0
if (hasNuxtConfig) score += 25
if (hasAppConfig) score += 15
if (hasReadme) score += 15
if (hasTypes) score += 20
if (hasTests) score += 25
return {
name,
hasNuxtConfig,
hasAppConfig,
hasReadme,
hasTypes,
hasTests,
componentCount,
composableCount,
score,
}
}
const layersDir = resolve('layers')
const layers = readdirSync(layersDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => join(layersDir, d.name))
console.log('\n📊 Layer Quality Report\n')
console.log('─'.repeat(80))
const results = layers.map(checkLayer)
results.forEach((r) => {
const status = r.score >= 80 ? '✅' : r.score >= 50 ? '⚠️' : '❌'
console.log(`${status} ${r.name.padEnd(30)} Score: ${r.score}/100`)
console.log(` Components: ${r.componentCount} | Composables: ${r.composableCount}`)
console.log(` Config: ${r.hasNuxtConfig ? '✓' : '✗'} | App: ${r.hasAppConfig ? '✓' : '✗'} | Docs: ${r.hasReadme ? '✓' : '✗'} | Types: ${r.hasTypes ? '✓' : '✗'} | Tests: ${r.hasTests ? '✓' : '✗'}`)
})
const avgScore = results.reduce((s, r) => s + r.score, 0) / results.length
console.log('─'.repeat(80))
console.log(`📈 Average Score: ${avgScore.toFixed(1)}/100\n`)
Pitfall Guide
Pitfall 1: Layer Circular Dependencies
// ❌ Wrong: Layer A extends B, Layer B extends A
// ✅ Correct: Establish clear Layer dependency hierarchy
// base → auth → admin (unidirectional)
export default defineNuxtConfig({
extends: ['@company/nuxt-layer-base'],
})
Pitfall 2: Server-Side Code Leaking in Layers
// ❌ Wrong: Using server modules directly in composables
export function useDatabase() {
const { connect } = require('mongoose') // Client will crash!
}
// ✅ Correct: Use server directory for server-side code
export function useApi() {
const config = useRuntimeConfig()
return { baseUrl: config.public.apiBaseURL }
}
Pitfall 3: Confusing App Config with Runtime Config
// ❌ Wrong: Putting secrets in app.config
export default defineAppConfig({
apiSecretKey: 'sk-xxx', // This gets exposed to the client!
})
// ✅ Correct: Use runtimeConfig for secrets
export default defineNuxtConfig({
runtimeConfig: {
apiSecretKey: '',
public: {
apiBaseURL: '/api',
},
},
})
Pitfall 4: Layer Component Naming Conflicts
<!-- ❌ Wrong: Multiple Layers have same-named components -->
<!-- ✅ Correct: Use namespace prefixes -->
<!-- layers/base/components/BaseButton.vue -->
<!-- layers/admin/components/AdminButton.vue -->
Pitfall 5: Ignoring Layer Tree-shaking
// ❌ Wrong: Importing all modules from a Layer
export default defineNuxtConfig({
modules: ['@company/nuxt-layer-everything'],
})
// ✅ Correct: Compose fine-grained Layers on demand
export default defineNuxtConfig({
extends: [
'@company/nuxt-layer-base',
'@company/nuxt-layer-auth',
],
})
Error Troubleshooting Table
| Error Message | Cause | Solution |
|---|---|---|
Could not resolve "@company/nuxt-layer-xxx" |
Layer package not installed or wrong path | pnpm add -D @company/nuxt-layer-xxx or check workspace path |
Nuxt module should be a function |
Layer nuxt.config export format error | Ensure using defineNuxtConfig() export |
Circular dependency detected |
Circular references between Layers | Check extends chain, ensure unidirectional dependencies |
Hydration mismatch |
Server/client rendering inconsistency | Check <ClientOnly> wrapping and conditional rendering |
Unknown component <XxxButton> |
Layer component not auto-imported | Check component naming and file location |
Cannot read property of undefined |
Nuxt instance not ready when composable used in plugin | Use callOnce or defer invocation |
Tailwind classes not applied |
Layer Tailwind config not recognized by main project | Ensure tailwind.config.ts is properly referenced |
Type errors in Layer composables |
Missing type declarations | Add index.d.ts in Layer root |
Layer overrides not working |
Wrong extends order | Later-declared Layers have higher priority |
Build performance slow |
Too many Layers or large static assets | Use fine-grained Layer splitting, optimize images/fonts |
Advanced Optimization
1. Debug Layers with Nuxt DevTools
export default defineNuxtConfig({
devtools: {
enabled: true,
timeline: { enabled: true },
},
})
// DevTools lets you inspect Layer inheritance chain, component sources, and composable call relationships
2. Layer Version Auto-Sync
{
"devDependencies": {
"@company/nuxt-layer-base": "workspace:*",
"@company/nuxt-layer-auth": "workspace:*"
}
}
3. Layer Testing Strategy
// layers/base/__tests__/useApi.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useApi } from '../composables/useApi'
vi.mock('#imports', () => ({
useAppConfig: () => ({ api: { baseURL: '/api' } }),
ref: vi.fn((val: unknown) => ({ value: val })),
computed: vi.fn((fn: () => unknown) => ({ value: fn() })),
}))
describe('useApi', () => {
it('should create API instance with correct base URL', () => {
const { execute } = useApi('/users')
expect(execute).toBeDefined()
})
})
4. Layer Documentation Auto-Generation
// scripts/generate-layer-docs.ts
import { readdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
function generateLayerDocs(layerPath: string) {
const name = layerPath.split('/').pop()
let doc = `# @company/nuxt-layer-${name}\n\n`
const componentsDir = join(layerPath, 'components')
try {
const components = readdirSync(componentsDir).filter((f) => f.endsWith('.vue'))
doc += `## Components (${components.length})\n\n`
components.forEach((c) => {
doc += `### ${c.replace('.vue', '')}\n\n`
})
} catch {}
writeFileSync(join(layerPath, 'README.md'), doc)
}
5. Layer Performance Monitoring
// plugins/layer-metrics.client.ts
export default defineNuxtPlugin(() => {
if (process.dev) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes('component')) {
console.log(`[Layer Metrics] ${entry.name}: ${entry.duration.toFixed(2)}ms`)
}
}
})
observer.observe({ entryTypes: ['measure'] })
onNuxtReady(() => {
const navEntry = performance.getEntriesByType('navigation')[0]
if (navEntry) {
console.log(`[Layer Metrics] Page Load: ${navEntry.loadEventEnd.toFixed(0)}ms`)
}
})
}
})
Comparison Analysis
| Feature | Nuxt Layer | Nuxt Module | Monorepo Sharing | Git Submodule |
|---|---|---|---|---|
| Reuse Granularity | App fragment | Plugin feature | Code snippet | Code repo |
| Config Inheritance | ✅ Automatic | ❌ Manual | ❌ Manual | ❌ Manual |
| Component Auto-Import | ✅ | ✅ | ❌ Needs config | ❌ Needs config |
| Version Management | npm version | npm version | workspace | git commit |
| Override Mechanism | ✅ Declaration order | ❌ | ❌ | ❌ |
| Learning Cost | Medium | Medium | Low | Low |
| Use Case | Cross-project reuse | Feature extension | Single team | Simple sharing |
Summary
Nuxt 4's Layer mechanism provides a true modular solution for large-scale frontend projects:
- Layer Basic Configuration: Complete encapsulation from components and composables to Tailwind configuration
- Multi-Layer Composition: Declare dependency chains via
extends, later declarations override earlier ones - Module Publishing: Distribute as npm packages with semantic versioning and changelogs
- Enterprise Templates: Standardized Layer structure for unified development paradigms
- Production Governance: CI/CD publishing, quality checks, performance monitoring, and documentation generation
Recommendation: Introduce Layers when 3+ projects need shared code; establish Layer governance processes when the team exceeds 10 people; publish to private npm registries for cross-team reuse.
Recommended Online Tools
- /en/json/format - JSON formatter, essential for debugging Nuxt configurations
- /en/dev/curl-to-code - HTTP request to code converter, quickly generate request logic in Layers
- /en/encode/hash - Hash calculator for Layer version verification
- /en/text/diff - Text diff tool for comparing Layer configuration differences across versions
Try these browser-local tools — no sign-up required →