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

  1. Severe Code Duplication: Across 10 sub-products, login modules, permission systems, and layout components are duplicated — fixing one bug means changing 10 repos
  2. Inconsistent Configuration: Different projects have different ESLint, TypeScript, and Tailwind configs, increasing onboarding costs
  3. Version Fragmentation: Shared logic spreads through copy-paste, making unified upgrades impossible and accumulating technical debt
  4. Team Collaboration Difficulties: Multiple teams maintain separate projects without unified development paradigms
  5. 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.

  • /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 →

#Nuxt4 Layer#模块化架构#企业级前端#Nuxt Layer#前端工程化#2026#前端开发