nuxt

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
This skill defines opinionated Nuxt 4 architecture: a BFF server layer, DDD-inspired contexts, a strict component hierarchy, and clean code principles applied to the Nuxt ecosystem.
Nuxt 4 provides a full-stack framework with file-based routing, server API routes, auto-imports, and SSR/SSG capabilities. The patterns here ensure a maintainable, scalable application by combining Nuxt's conventions with solid software design principles (see
guidelines
skill).

本规范定义了约定式的 Nuxt 4 架构:包含 BFF 服务层、领域驱动设计(DDD)启发的上下文、严格的组件层级,以及应用于 Nuxt 生态的整洁代码原则。
Nuxt 4 是一个全栈框架,提供基于文件的路由、服务端 API 路由、自动导入以及 SSR/SSG 能力。本文档中的模式结合 Nuxt 约定与可靠的软件设计原则(参考
guidelines
规范),确保应用可维护、可扩展。

Project Structure

项目结构

.
├── app/
│   ├── app.vue                        # Root component — calls useApp init
│   ├── error.vue                      # Global error page
│   ├── pages/                         # File-based routing
│   │   ├── index.vue
│   │   └── users/
│   │       └── [id].vue
│   ├── layouts/                       # Layout components
│   │   ├── default.vue
│   │   └── dashboard.vue
│   ├── middleware/                    # Route middleware
│   │   ├── auth.ts                    # Global or named auth guard
│   │   └── role.ts
│   ├── plugins/                       # App-level plugins
│   │   ├── analytics.client.ts        # Client-only plugin
│   │   └── sentry.ts
│   ├── components/
│   │   ├── app/                       # Shared application components
│   │   │   ├── AppHeader.vue
│   │   │   ├── AppFooter.vue
│   │   │   └── AppLoadingState.vue
│   │   ├── ui/                        # Independent, reusable UI components (no domain logic)
│   │   │   ├── BaseCard.vue
│   │   │   └── BaseEmptyState.vue
│   │   ├── home/                      # Home page components
│   │   │   ├── HomeHero.vue
│   │   │   ├── HomeLatestArticles.vue
│   │   │   └── HomeFeaturedProducts.vue
│   │   └── user/                      # User module components (matches the user domain)
│   │       ├── list/                  # User list view
│   │       │   ├── UserList.vue       # Orchestrates user list (no "Container" suffix)
│   │       │   ├── UserListItem.vue
│   │       │   └── UserListFilters.vue
│   │       ├── detail/                # User detail view
│   │       │   ├── UserDetail.vue     # Orchestrates user detail
│   │       │   ├── UserProfile.vue
│   │       │   └── UserActivityFeed.vue
│   │       └── add/                   # User add/edit views
│   │           ├── UserAdd.vue        # Orchestrates user creation
│   │           └── UserAddForm.vue
│   ├── composables/                   # Auto-imported composables (filename without "use" prefix)
│   │   ├── app.ts                     # exports useApp()
│   │   ├── pages.ts                   # exports usePages()
│   │   └── user.ts                    # exports useUser()
│   └── stores/                        # Pinia stores (auto-imported via nuxt.config)
│       ├── app.store.ts               # exports useAppStore
│       └── user.store.ts              # exports useUserStore
├── server/
│   ├── api/
│   │   ├── app/
│   │   │   └── index.get.ts           # App initialization endpoint
│   │   ├── pages/
│   │   │   ├── index.get.ts           # Home page data endpoint — returns ALL home data
│   │   │   └── users/
│   │   │       └── [id].get.ts        # User page data endpoint — returns ALL user data
│   │   └── user/
│   │       ├── create.post.ts
│   │       └── [id].delete.ts
│   ├── utils/                         # Auto-imported server utilities — flat, one file per domain
│   │   ├── user.ts                    # createUser, findUser, deleteUser — ALL in one file
│   │   ├── product.ts
│   │   └── app.ts
│   └── contexts/                      # NOT auto-imported — explicit imports only
│       ├── shared/
│       │   ├── services/
│       │   │   └── PostgresService.ts
│       │   └── errors/
│       │       └── ServerError.ts
│       └── user/
│           ├── domain/
│           │   ├── User.ts
│           │   ├── UserRepository.ts
│           │   └── UserError.ts
│           ├── application/
│           │   ├── UserCreator.ts
│           │   └── UserFinder.ts
│           └── infrastructure/
│               └── PostgresUserRepository.ts
└── shared/
    ├── types/                         # Flat — no subfolders — auto-importable
    │   ├── App.ts                     # App initialization types (interface App)
    │   ├── Page.ts                    # All page types (special — not split by module)
    │   └── User.ts                    # Domain types + AuthUser
    └── utils/                         # Flat — no subfolders — auto-importable
        ├── user.ts                    # Zod schemas and utilities for user module
        └── product.ts
Key rules:
  • app/
    contains all Vue/client code (Nuxt 4 default)
  • server/contexts/
    is never auto-imported — always use explicit
    import
    statements
  • server/utils/
    is flat: one file per domain, all functions for that domain in the same file
  • shared/types/
    and
    shared/utils/
    are flat (no subfolders) and are auto-importable
  • Components are organized by module context (matching the domain/web module), not by technical type
  • Container-style components have no
    Container
    suffix
    UserDetail
    , not
    UserDetailContainer
  • Composable filenames have no
    use
    prefix
    (
    app.ts
    ), but the exported function still does (
    useApp
    )
  • Store filenames use
    .store.ts
    suffix (
    user.store.ts
    ), exported as
    useUserStore

.
├── app/
│   ├── app.vue                        # 根组件 — 调用 useApp 初始化
│   ├── error.vue                      # 全局错误页面
│   ├── pages/                         # 基于文件的路由
│   │   ├── index.vue
│   │   └── users/
│   │       └── [id].vue
│   ├── layouts/                       # 布局组件
│   │   ├── default.vue
│   │   └── dashboard.vue
│   ├── middleware/                    # 路由中间件
│   │   ├── auth.ts                    # 全局或命名鉴权守卫
│   │   └── role.ts
│   ├── plugins/                       # 应用级插件
│   │   ├── analytics.client.ts        # 仅客户端插件
│   │   └── sentry.ts
│   ├── components/
│   │   ├── app/                       # 共享应用组件
│   │   │   ├── AppHeader.vue
│   │   │   ├── AppFooter.vue
│   │   │   └── AppLoadingState.vue
│   │   ├── ui/                        # 独立可复用 UI 组件(无领域逻辑)
│   │   │   ├── BaseCard.vue
│   │   │   └── BaseEmptyState.vue
│   │   ├── home/                      # 首页组件
│   │   │   ├── HomeHero.vue
│   │   │   ├── HomeLatestArticles.vue
│   │   │   └── HomeFeaturedProducts.vue
│   │   └── user/                      # 用户模块组件(匹配用户领域)
│   │       ├── list/                  # 用户列表视图
│   │       │   ├── UserList.vue       # 编排用户列表(无 "Container" 后缀)
│   │       │   ├── UserListItem.vue
│   │       │   └── UserListFilters.vue
│   │       ├── detail/                # 用户详情视图
│   │       │   ├── UserDetail.vue     # 编排用户详情
│   │       │   ├── UserProfile.vue
│   │       │   └── UserActivityFeed.vue
│   │       └── add/                   # 用户新增/编辑视图
│   │           ├── UserAdd.vue        # 编排用户创建流程
│   │           └── UserAddForm.vue
│   ├── composables/                   # 自动导入的组合式函数(文件名无 "use" 前缀)
│   │   ├── app.ts                     # 导出 useApp()
│   │   ├── pages.ts                   # 导出 usePages()
│   │   └── user.ts                    # 导出 useUser()
│   └── stores/                        # Pinia 状态存储(通过 nuxt.config 自动导入)
│       ├── app.store.ts               # 导出 useAppStore
│       └── user.store.ts              # 导出 useUserStore
├── server/
│   ├── api/
│   │   ├── app/
│   │   │   └── index.get.ts           # 应用初始化端点
│   │   ├── pages/
│   │   │   ├── index.get.ts           # 首页数据端点 — 返回所有首页所需数据
│   │   │   └── users/
│   │   │       └── [id].get.ts        # 用户页面数据端点 — 返回所有用户页面所需数据
│   │   └── user/
│   │       ├── create.post.ts
│   │       └── [id].delete.ts
│   ├── utils/                         # 自动导入的服务端工具 — 扁平化,每个领域一个文件
│   │   ├── user.ts                    # createUser, findUser, deleteUser — 全部在一个文件中
│   │   ├── product.ts
│   │   └── app.ts
│   └── contexts/                      # 不自动导入 — 仅显式导入
│       ├── shared/
│       │   ├── services/
│       │   │   └── PostgresService.ts
│       │   └── errors/
│       │       └── ServerError.ts
│       └── user/
│           ├── domain/
│           │   ├── User.ts
│           │   ├── UserRepository.ts
│           │   └── UserError.ts
│           ├── application/
│           │   ├── UserCreator.ts
│           │   └── UserFinder.ts
│           └── infrastructure/
│               └── PostgresUserRepository.ts
└── shared/
    ├── types/                         # 扁平化 — 无子文件夹 — 可自动导入
    │   ├── App.ts                     # 应用初始化类型(interface App)
    │   ├── Page.ts                    # 所有页面类型(特殊 — 不按模块拆分)
    │   └── User.ts                    # 领域类型 + 鉴权用户类型
    └── utils/                         # 扁平化 — 无子文件夹 — 可自动导入
        ├── user.ts                    # 用户模块的 Zod 校验规则与工具函数
        └── product.ts
核心规则:
  • app/
    包含所有 Vue/客户端代码(Nuxt 4 默认约定)
  • server/contexts/
    永远不自动导入 — 始终使用显式
    import
    语句
  • server/utils/
    是扁平化的:每个领域一个文件,该领域的所有函数都在同一个文件中
  • shared/types/
    shared/utils/
    是扁平化的(无嵌套文件夹)且可自动导入
  • 组件按模块上下文组织(匹配领域/网页模块),而非技术类型
  • 容器型组件无
    Container
    后缀 — 例如
    UserDetail
    ,而非
    UserDetailContainer
  • 组合式函数文件名无
    use
    前缀(如
    app.ts
    ),但导出的函数仍保留
    use
    前缀(如
    useApp
  • 状态存储文件名使用
    .store.ts
    后缀(如
    user.store.ts
    ),导出为
    useUserStore

Nuxt Config

Nuxt 配置

ts
// nuxt.config.ts
export default defineNuxtConfig({
  
  // Define components path without prefix
  components: [
    { path: '@/components', pathPrefix: false },
  ],
  
  // Define Devtools
  devtools: {
    enabled: import.meta.env.DEVTOOLS_ENABLED || false,
    timeline: {
      enabled: true,
    },
  },
  
  // Define CSS
  css: [
    '@/assets/css/main.css',
  ],
  
  // Runtime config
  runtimeConfig: {
    privateConfig: '',
    public: {
      publicConfig: ''
    }
  }
  
  compatibilityDate: '2025-07-15',
  
  // Auto-import stores from app/stores/
  pinia: {
    storesDirs: ['./stores/**.store.ts'],
  },
  
  modules: [
    '@pinia/nuxt',
    '@nuxt/ui', // if using Nuxt UI
    '@nuxtjs/i18n', // if using i18n
  ],
})

ts
// nuxt.config.ts
export default defineNuxtConfig({
  
  // 定义组件路径,无需前缀
  components: [
    { path: '@/components', pathPrefix: false },
  ],
  
  // 定义开发工具
  devtools: {
    enabled: import.meta.env.DEVTOOLS_ENABLED || false,
    timeline: {
      enabled: true,
    },
  },
  
  // 定义 CSS
  css: [
    '@/assets/css/main.css',
  ],
  
  // 运行时配置
  runtimeConfig: {
    privateConfig: '',
    public: {
      publicConfig: ''
    }
  }
  
  compatibilityDate: '2025-07-15',
  
  // 自动导入 app/stores/ 中的状态存储
  pinia: {
    storesDirs: ['./stores/**.store.ts'],
  },
  
  modules: [
    '@pinia/nuxt',
    '@nuxt/ui', // 如果使用 Nuxt UI
    '@nuxtjs/i18n', // 如果使用国际化
  ],
})

Auto-Imports

自动导入

Nuxt auto-imports everything in
app/composables/
,
app/stores/
(via config),
app/components/
, and
server/utils/
. Leverage this everywhere except
server/contexts/
.
✅ Use auto-imports:
ts
// app/pages/index.vue — no imports needed
const pages = usePages()
const { data } = await useAsyncData('home', () => pages.getHomePage())
const userStore = useUserStore()
❌ Never use auto-imports in
server/contexts/
:
ts
// server/contexts/user/application/UserCreator.ts
import type { UserRepository } from '../domain/UserRepository'       // explicit import
import type { CreateUserDto } from '../../../../shared/types/User'   // explicit import

export class UserCreator {
  constructor(private readonly repository: UserRepository) {}

  async create(dto: CreateUserDto): Promise<void> {
    // ...
  }
}

Nuxt 会自动导入
app/composables/
app/stores/
(通过配置)、
app/components/
以及
server/utils/
中的所有内容。除了
server/contexts/
之外,其他地方都可以充分利用这一特性。
✅ 推荐使用自动导入:
ts
// app/pages/index.vue — 无需手动导入
const pages = usePages()
const { data } = await useAsyncData('home', () => pages.getHomePage())
const userStore = useUserStore()
❌ 禁止在
server/contexts/
中使用自动导入:
ts
// server/contexts/user/application/UserCreator.ts
import type { UserRepository } from '../domain/UserRepository'       // 显式导入
import type { CreateUserDto } from '../../../../shared/types/User'   // 显式导入

export class UserCreator {
  constructor(private readonly repository: UserRepository) {}

  async create(dto: CreateUserDto): Promise<void> {
    // ...
  }
}

Backend for Frontend (BFF) Pattern

前端后端(BFF)模式

The server layer acts exclusively as a BFF — it aggregates, transforms, and exposes data tailored for the Vue frontend. No business logic lives in the Vue layer.
There are three types of endpoints, each with a clear purpose:
服务层仅作为 BFF 存在 — 它聚合、转换并暴露专为 Vue 前端定制的数据。所有业务逻辑都不放在 Vue 层中。
服务端有三类端点,各有明确用途:

1. App Endpoint

1. 应用初始化端点

Path:
server/api/app/index.get.ts
Returns all data needed to bootstrap the application (user session, config, feature flags, translations metadata, etc.). Called once on app mount.
ts
// server/api/app/index.get.ts
import type { App } from '~~/shared/types/App'

export default defineEventHandler(async (event): Promise<App> => {
  const [config, user] = await Promise.all([
    getAppConfig(event),
    getAuthUser(event),
  ])

  return { config, user }
})
Composable:
app/composables/app.ts
ts
// app/composables/app.ts
export function useApp() {
  const appStore = useAppStore()
  const userStore = useUserStore()

  async function getAppData(): Promise<App> {
    return $fetch('/api/app')
  }

  async function init(): Promise<void> {
    const data = await getAppData()
    appStore.setConfig(data.config)
    userStore.setCurrentUser(data.user)
  }

  return { init }
}
Usage in
app/app.vue
:
vue
<!-- app/app.vue -->
<script setup lang="ts">
const { init } = useApp()

// callOnce ensures this runs once on SSR and not again on client hydration
await callOnce(init)

// If using nuxt-i18n, re-init on locale change:
// const { locale } = useI18n()
// watch(locale, init)
</script>

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
Types:
shared/types/App.ts
ts
// shared/types/App.ts
export interface App {
  config: AppConfig
  user: User | null
}

export interface AppConfig {
  featureFlags: Record<string, boolean>
  locale: string
}
AuthUser
is not a separate type — it is the
User
interface defined in
shared/types/User.ts
. If the authenticated user shape differs from the domain user, extend from
User
in
User.ts
.

路径:
server/api/app/index.get.ts
返回应用启动所需的所有数据(用户会话、配置、功能开关、国际化元数据等)。仅在应用挂载时调用一次。
ts
// server/api/app/index.get.ts
import type { App } from '~~/shared/types/App'

export default defineEventHandler(async (event): Promise<App> => {
  const [config, user] = await Promise.all([
    getAppConfig(event),
    getAuthUser(event),
  ])

  return { config, user }
})
组合式函数:
app/composables/app.ts
ts
// app/composables/app.ts
export function useApp() {
  const appStore = useAppStore()
  const userStore = useUserStore()

  async function getAppData(): Promise<App> {
    return $fetch('/api/app')
  }

  async function init(): Promise<void> {
    const data = await getAppData()
    appStore.setConfig(data.config)
    userStore.setCurrentUser(data.user)
  }

  return { init }
}
app/app.vue
中使用:
vue
<!-- app/app.vue -->
<script setup lang="ts">
const { init } = useApp()

// callOnce 确保该逻辑仅在 SSR 时运行一次,客户端 hydration 时不再重复执行
await callOnce(init)

// 如果使用 nuxt-i18n,在语言切换时重新初始化:
// const { locale } = useI18n()
// watch(locale, init)
</script>

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
类型定义:
shared/types/App.ts
ts
// shared/types/App.ts
export interface App {
  config: AppConfig
  user: User | null
}

export interface AppConfig {
  featureFlags: Record<string, boolean>
  locale: string
}
AuthUser
不是独立类型 — 它是
shared/types/User.ts
中定义的
User
接口。如果鉴权用户的结构与领域用户不同,可在
User.ts
中基于
User
接口扩展。

2. Page Endpoints

2. 页面数据端点

Each Nuxt page calls exactly one server endpoint that returns all data the page needs in a single request. This avoids waterfalls and keeps pages simple.
Convention:
server/api/pages/{route}.get.ts
mirrors
app/pages/{route}.vue
. Each endpoint fetches everything the page needs and returns it in one response.
ts
// server/api/pages/index.get.ts  →  app/pages/index.vue
import type { HomePageData } from '~~/shared/types/Page'

export default defineEventHandler(async (event): Promise<HomePageData> => {
  const [banner, products] = await Promise.all([
    getHeroBanner(event),
    getFeaturedProducts(event),
  ])

  return { banner, products }
})
ts
// server/api/pages/users/[id].get.ts  →  app/pages/users/[id].vue
import type { UserPageData } from '~~/shared/types/Page'

export default defineEventHandler(async (event): Promise<UserPageData> => {
  const id = getRouterParam(event, 'id')!

  const [user, activity] = await Promise.all([
    findUser(event, id),
    getUserActivity(event, id),
  ])

  return { user, activity }
})
Note:
findUser
and
getUserActivity
are auto-imported from
server/utils/user.ts
.
Composable:
app/composables/pages.ts
— all page fetchers in one place.
ts
// app/composables/pages.ts
export function usePages() {
  async function getHomePage(): Promise<HomePageData> {
    return $fetch('/api/pages')
  }

  async function getUserPage(id: string): Promise<UserPageData> {
    return $fetch(`/api/pages/users/${id}`)
  }

  return { getHomePage, getUserPage }
}
Types:
shared/types/Page.ts
all page types together (not split by module, pages are a cross-cutting concern).
ts
// shared/types/Page.ts
export interface HomePageData {
  banner: Banner
  products: Product[]
}

export interface UserPageData {
  user: User
  activity: UserActivity[]
}

export interface Banner {
  title: string
  imageUrl: string
  ctaLabel: string
  ctaUrl: string
}

每个 Nuxt 页面仅调用一个服务端端点,该端点返回页面所需的全部数据。这样可以避免请求瀑布流,保持页面逻辑简洁。
约定:
server/api/pages/{route}.get.ts
app/pages/{route}.vue
一一对应。每个端点获取页面所需的所有数据,并在一次响应中返回。
ts
// server/api/pages/index.get.ts  →  app/pages/index.vue
import type { HomePageData } from '~~/shared/types/Page'

export default defineEventHandler(async (event): Promise<HomePageData> => {
  const [banner, products] = await Promise.all([
    getHeroBanner(event),
    getFeaturedProducts(event),
  ])

  return { banner, products }
})
ts
// server/api/pages/users/[id].get.ts  →  app/pages/users/[id].vue
import type { UserPageData } from '~~/shared/types/Page'

export default defineEventHandler(async (event): Promise<UserPageData> => {
  const id = getRouterParam(event, 'id')!

  const [user, activity] = await Promise.all([
    findUser(event, id),
    getUserActivity(event, id),
  ])

  return { user, activity }
})
注意:
findUser
getUserActivity
是从
server/utils/user.ts
自动导入的。
组合式函数:
app/composables/pages.ts
— 所有页面数据获取逻辑集中管理。
ts
// app/composables/pages.ts
export function usePages() {
  async function getHomePage(): Promise<HomePageData> {
    return $fetch('/api/pages')
  }

  async function getUserPage(id: string): Promise<UserPageData> {
    return $fetch(`/api/pages/users/${id}`)
  }

  return { getHomePage, getUserPage }
}
类型定义:
shared/types/Page.ts
所有页面类型集中管理(不按模块拆分,页面是跨领域关注点)。
ts
// shared/types/Page.ts
export interface HomePageData {
  banner: Banner
  products: Product[]
}

export interface UserPageData {
  user: User
  activity: UserActivity[]
}

export interface Banner {
  title: string
  imageUrl: string
  ctaLabel: string
  ctaUrl: string
}

3. Use-Case Endpoints

3. 业务用例端点

Business operation endpoints (mutations and domain queries). Organized by module.
ts
// server/api/user/create.post.ts
import type { User } from '~~/shared/types/User'

export default defineEventHandler(async (event): Promise<User> => {
  const body = await readValidatedBody(event, createUserSchema.parse)
  return createUser(event, body)
})
ts
// server/api/user/[id].delete.ts
export default defineEventHandler(async (event): Promise<void> => {
  const id = getRouterParam(event, 'id')!
  await deleteUser(event, id)
})
Composable:
app/composables/user.ts
ts
// app/composables/user.ts
export function useUser() {
  const userStore = useUserStore()

  async function createUser(dto: CreateUserDto): Promise<User> {
    const user = await $fetch('/api/user/create', {
      method: 'POST',
      body: dto,
    })
    userStore.addUser(user)
    return user
  }

  async function deleteUser(id: string): Promise<void> {
    await $fetch(`/api/user/${id}`, { method: 'DELETE' })
    userStore.removeUser(id)
  }

  return { createUser, deleteUser }
}

业务操作端点(数据变更与领域查询)。按模块组织。
ts
// server/api/user/create.post.ts
import type { User } from '~~/shared/types/User'

export default defineEventHandler(async (event): Promise<User> => {
  const body = await readValidatedBody(event, createUserSchema.parse)
  return createUser(event, body)
})
ts
// server/api/user/[id].delete.ts
export default defineEventHandler(async (event): Promise<void> => {
  const id = getRouterParam(event, 'id')!
  await deleteUser(event, id)
})
组合式函数:
app/composables/user.ts
ts
// app/composables/user.ts
export function useUser() {
  const userStore = useUserStore()

  async function createUser(dto: CreateUserDto): Promise<User> {
    const user = await $fetch('/api/user/create', {
      method: 'POST',
      body: dto,
    })
    userStore.addUser(user)
    return user
  }

  async function deleteUser(id: string): Promise<void> {
    await $fetch(`/api/user/${id}`, { method: 'DELETE' })
    userStore.removeUser(id)
  }

  return { createUser, deleteUser }
}

Server Layer: Contexts & Utils

服务层:上下文与工具函数

server/contexts/
— Domain logic with explicit imports

server/contexts/
— 显式导入的领域逻辑

Follows DDD (Domain-Driven Design) patterns. Never rely on Nuxt auto-imports here. All imports are explicit to keep the domain layer framework-agnostic and testable.
server/contexts/
└── user/
    ├── domain/
    │   ├── User.ts                    # Domain entity
    │   ├── UserRepository.ts          # Repository interface
    │   └── UserError.ts               # Domain errors
    ├── application/
    │   ├── UserCreator.ts             # Use case
    │   └── UserFinder.ts              # Use case
    └── infrastructure/
        └── PostgresUserRepository.ts  # Concrete implementation
Domain entity:
ts
// server/contexts/user/domain/User.ts
export interface User {
  id: string
  name: string
  email: string
  createdAt: Date
}
Repository interface (DIP):
ts
// server/contexts/user/domain/UserRepository.ts
import type { User } from './User'
import type { CreateUserDto } from '../../../../shared/types/User'

export interface UserRepository {
  findById(id: string): Promise<User | null>
  create(dto: CreateUserDto): Promise<User>
  delete(id: string): Promise<void>
}
Use case with dependency injection:
ts
// server/contexts/user/application/UserCreator.ts
import type { UserRepository } from '../domain/UserRepository'
import type { CreateUserDto } from '../../../../shared/types/User'
import type { User } from '../domain/User'
import { UserError } from '../domain/UserError'

export class UserCreator {
  constructor(private readonly repository: UserRepository) {}

  async create(dto: CreateUserDto): Promise<User> {
    const existing = await this.repository.findByEmail(dto.email)
    if (existing) throw new UserError('EMAIL_ALREADY_EXISTS', dto.email)
    return this.repository.create(dto)
  }
}
Concrete implementation:
ts
// server/contexts/user/infrastructure/PostgresUserRepository.ts
import type { UserRepository } from '../domain/UserRepository'
import type { User } from '../domain/User'
import type { CreateUserDto } from '../../../../shared/types/User'
import { PostgresService } from '../../shared/services/PostgresService'

export class PostgresUserRepository implements UserRepository {
  constructor(private readonly db: PostgresService) {}

  async findById(id: string): Promise<User | null> {
    return this.db.query('SELECT * FROM users WHERE id = $1', [id])
  }

  async create(dto: CreateUserDto): Promise<User> {
    return this.db.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [dto.name, dto.email],
    )
  }

  async delete(id: string): Promise<void> {
    await this.db.query('DELETE FROM users WHERE id = $1', [id])
  }
}
遵循领域驱动设计(DDD)模式。永远不要依赖 Nuxt 的自动导入。所有导入都是显式的,以保持领域层与框架无关,便于测试。
server/contexts/
└── user/
    ├── domain/
    │   ├── User.ts                    # 领域实体
    │   ├── UserRepository.ts          # 仓储接口
    │   └── UserError.ts               # 领域错误
    ├── application/
    │   ├── UserCreator.ts             # 业务用例
    │   └── UserFinder.ts              # 业务用例
    └── infrastructure/
        └── PostgresUserRepository.ts  # 具体实现
领域实体:
ts
// server/contexts/user/domain/User.ts
export interface User {
  id: string
  name: string
  email: string
  createdAt: Date
}
仓储接口(依赖倒置原则):
ts
// server/contexts/user/domain/UserRepository.ts
import type { User } from './User'
import type { CreateUserDto } from '../../../../shared/types/User'

export interface UserRepository {
  findById(id: string): Promise<User | null>
  create(dto: CreateUserDto): Promise<User>
  delete(id: string): Promise<void>
}
带依赖注入的业务用例:
ts
// server/contexts/user/application/UserCreator.ts
import type { UserRepository } from '../domain/UserRepository'
import type { CreateUserDto } from '../../../../shared/types/User'
import type { User } from '../domain/User'
import { UserError } from '../domain/UserError'

export class UserCreator {
  constructor(private readonly repository: UserRepository) {}

  async create(dto: CreateUserDto): Promise<User> {
    const existing = await this.repository.findByEmail(dto.email)
    if (existing) throw new UserError('EMAIL_ALREADY_EXISTS', dto.email)
    return this.repository.create(dto)
  }
}
具体实现:
ts
// server/contexts/user/infrastructure/PostgresUserRepository.ts
import type { UserRepository } from '../domain/UserRepository'
import type { User } from '../domain/User'
import type { CreateUserDto } from '../../../../shared/types/User'
import { PostgresService } from '../../shared/services/PostgresService'

export class PostgresUserRepository implements UserRepository {
  constructor(private readonly db: PostgresService) {}

  async findById(id: string): Promise<User | null> {
    return this.db.query('SELECT * FROM users WHERE id = $1', [id])
  }

  async create(dto: CreateUserDto): Promise<User> {
    return this.db.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [dto.name, dto.email],
    )
  }

  async delete(id: string): Promise<void> {
    await this.db.query('DELETE FROM users WHERE id = $1', [id])
  }
}

server/utils/
— Use-case orchestrators (auto-imported, flat)

server/utils/
— 业务用例编排器(自动导入,扁平化)

These are the bridge between API route handlers and the context layer. They wire up dependencies (DI) and expose simple functions to the endpoint handlers.
Key rule: All functions for a given domain live in one file — no subfolders.
server/utils/user.ts
contains
createUser
,
findUser
,
deleteUser
, etc.
ts
// server/utils/user.ts — ALL user utilities in one file
import { UserCreator } from '~~/server/contexts/user/application/UserCreator'
import { UserFinder } from '~~/server/contexts/user/application/UserFinder'
import { PostgresUserRepository } from '~~/server/contexts/user/infrastructure/PostgresUserRepository'
import { PostgresService } from '~~/server/contexts/shared/services/PostgresService'
import type { CreateUserDto } from '~~/shared/types/User'
import type { User } from '~~/shared/types/User'
import type { H3Event } from 'h3'

export async function createUser(event: H3Event, dto: CreateUserDto): Promise<User> {
  const db = new PostgresService()
  const repository = new PostgresUserRepository(db)
  const creator = new UserCreator(repository)
  return creator.create(dto)
}

export async function findUser(event: H3Event, id: string): Promise<User> {
  const db = new PostgresService()
  const repository = new PostgresUserRepository(db)
  const finder = new UserFinder(repository)
  return finder.find(id)
}

export async function deleteUser(event: H3Event, id: string): Promise<void> {
  const db = new PostgresService()
  const repository = new PostgresUserRepository(db)
  await repository.delete(id)
}

export async function getUserActivity(event: H3Event, id: string): Promise<UserActivity[]> {
  // ...
}
Key rule:
server/utils/
functions are auto-imported in route handlers.
server/contexts/
classes are never auto-imported — always explicitly imported inside
server/utils/
.

这是 API 路由处理器与上下文层之间的桥梁。它负责依赖注入(DI)并向端点处理器暴露简洁的函数。
核心规则: 同一领域的所有函数都放在一个文件中 — 无嵌套文件夹。
server/utils/user.ts
包含
createUser
findUser
deleteUser
等所有用户相关函数。
ts
// server/utils/user.ts — 所有用户工具函数都在一个文件中
import { UserCreator } from '~~/server/contexts/user/application/UserCreator'
import { UserFinder } from '~~/server/contexts/user/application/UserFinder'
import { PostgresUserRepository } from '~~/server/contexts/user/infrastructure/PostgresUserRepository'
import { PostgresService } from '~~/server/contexts/shared/services/PostgresService'
import type { CreateUserDto } from '~~/shared/types/User'
import type { User } from '~~/shared/types/User'
import type { H3Event } from 'h3'

export async function createUser(event: H3Event, dto: CreateUserDto): Promise<User> {
  const db = new PostgresService()
  const repository = new PostgresUserRepository(db)
  const creator = new UserCreator(repository)
  return creator.create(dto)
}

export async function findUser(event: H3Event, id: string): Promise<User> {
  const db = new PostgresService()
  const repository = new PostgresUserRepository(db)
  const finder = new UserFinder(repository)
  return finder.find(id)
}

export async function deleteUser(event: H3Event, id: string): Promise<void> {
  const db = new PostgresService()
  const repository = new PostgresUserRepository(db)
  await repository.delete(id)
}

export async function getUserActivity(event: H3Event, id: string): Promise<UserActivity[]> {
  // ...
}
核心规则:
server/utils/
中的函数会在路由处理器中自动导入。
server/contexts/
中的类永远不会自动导入 — 始终在
server/utils/
中显式导入。

Shared Layer

共享层

shared/
is available on both client and server. Everything here is flat (no subfolders) and auto-importable.
shared/
目录同时在客户端和服务端可用。其中所有内容都是扁平化(无嵌套文件夹)且可自动导入的。

shared/types/
— Types and enums, one file per module

shared/types/
— 类型与枚举,每个模块一个文件

ts
// shared/types/User.ts
export interface User {
  id: string
  name: string
  email: string
  role: UserRole
  roles: UserRole[]    // for authenticated user — roles array
  createdAt: string
}

export interface CreateUserDto {
  name: string
  email: string
  role: UserRole
}

export enum UserRole {
  Admin = 'admin',
  User = 'user',
}
The authenticated user type is
User
(from
shared/types/User.ts
). There is no separate
AuthUser
type — the same
User
interface represents domain users and authenticated users alike. If extra auth-only fields are needed, use intersection or optional fields.
Special files:
  • shared/types/App.ts
    — App initialization data (see App Endpoint section)
  • shared/types/Page.ts
    — All page response types (not split by module, pages are a special cross-cutting concern)
ts
// shared/types/User.ts
export interface User {
  id: string
  name: string
  email: string
  role: UserRole
  roles: UserRole[]    // 鉴权用户的角色数组
  createdAt: string
}

export interface CreateUserDto {
  name: string
  email: string
  role: UserRole
}

export enum UserRole {
  Admin = 'admin',
  User = 'user',
}
鉴权用户类型就是
User
(来自
shared/types/User.ts
)。没有独立的
AuthUser
类型 — 同一个
User
接口既表示领域用户,也表示鉴权用户。如果需要额外的鉴权专属字段,可使用交叉类型或可选字段。
特殊文件:
  • shared/types/App.ts
    — 应用初始化数据类型(参考应用初始化端点章节)
  • shared/types/Page.ts
    — 所有页面响应类型(不按模块拆分,页面是特殊的跨领域关注点)

shared/utils/
— Zod schemas and utilities, one file per module

shared/utils/
— Zod 校验规则与工具函数,每个模块一个文件

ts
// shared/utils/user.ts — ALL user schemas and utilities in one file
import { z } from 'zod'
import { UserRole } from '../types/User'

export const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  role: z.nativeEnum(UserRole),
})

export type CreateUserInput = z.infer<typeof createUserSchema>

export function formatUserDisplayName(user: { name: string; email: string }): string {
  return `${user.name} <${user.email}>`
}

ts
// shared/utils/user.ts — 所有用户校验规则与工具函数都在一个文件中
import { z } from 'zod'
import { UserRole } from '../types/User'

export const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  role: z.nativeEnum(UserRole),
})

export type CreateUserInput = z.infer<typeof createUserSchema>

export function formatUserDisplayName(user: { name: string; email: string }): string {
  return `${user.name} <${user.email}>`
}

Component Architecture

组件架构

Components follow a strict three-layer hierarchy. Each layer has a single, well-defined responsibility.
组件遵循严格的三层层级结构。每一层都有单一、明确的职责。

Layer 1: Page Component (View)

第一层:页面组件(视图层)

Location:
app/pages/
Rules:
  • As simple as possible — just data fetching and layout composition
  • Uses
    useAsyncData
    to fetch page data via
    usePages
    composable — one single endpoint call per page
  • Renders a single orchestrating component, passing data as props
  • No business logic, no direct store access (except reading loading state)
vue
<!-- app/pages/index.vue -->
<script setup lang="ts">
const { getHomePage } = usePages()

const { data, status } = await useAsyncData('home', getHomePage)
</script>

<template>
  <div>
    <Home
      v-if="data"
      :data="data"
    />
    <AppLoadingState v-else-if="status === 'pending'" />
  </div>
</template>
vue
<!-- app/pages/users/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const { getUserPage } = usePages()

const { data, status } = await useAsyncData(
  `user-${route.params.id}`,
  () => getUserPage(route.params.id as string),
)
</script>

<template>
  <UserDetail
    v-if="data"
    :data="data"
  />
</template>
位置:
app/pages/
规则:
  • 尽可能简洁 — 仅负责数据获取与布局组合
  • 使用
    useAsyncData
    通过
    usePages
    组合式函数获取页面数据 — 每个页面仅调用一个端点
  • 渲染一个编排组件,将数据作为 props 传递
  • 无业务逻辑,无直接状态存储访问(仅允许读取加载状态)
vue
<!-- app/pages/index.vue -->
<script setup lang="ts">
const { getHomePage } = usePages()

const { data, status } = await useAsyncData('home', getHomePage)
</script>

<template>
  <div>
    <Home
      v-if="data"
      :data="data"
    />
    <AppLoadingState v-else-if="status === 'pending'" />
  </div>
</template>
vue
<!-- app/pages/users/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const { getUserPage } = usePages()

const { data, status } = await useAsyncData(
  `user-${route.params.id}`,
  () => getUserPage(route.params.id as string),
)
</script>

<template>
  <UserDetail
    v-if="data"
    :data="data"
  />
</template>

Layer 2: Orchestrating Component (no "Container" suffix)

第二层:编排组件(无 "Container" 后缀)

Location:
app/components/{module}/
— inside the relevant module subfolder.
Rules:
  • Named after the view it orchestrates (e.g.
    UserDetail
    ,
    UserList
    ,
    Home
    ) — no
    Container
    suffix
  • Receives page data as props
  • Connects to Pinia stores for reactive state and actions
  • Calls use-case composables for mutations
  • Computes derived state
  • Passes only what each child component needs
  • Never contains raw HTML — only child presentational components
vue
<!-- app/components/user/detail/UserDetail.vue -->
<script setup lang="ts">
import type { UserPageData } from '~/shared/types/Page'

const props = defineProps<{
  data: UserPageData
}>()

const userStore = useUserStore()
const { deleteUser } = useUser()

// Hydrate store with server data
userStore.setUser(props.data.user)

// Derived state
const isCurrentUser = computed(() =>
  userStore.currentUser?.id === props.data.user.id
)

async function handleDelete(): Promise<void> {
  await deleteUser(props.data.user.id)
  await navigateTo('/users')
}
</script>

<template>
  <div>
    <UserProfile
      :user="data.user"
      :is-current-user="isCurrentUser"
      @delete="handleDelete"
    />
    <UserActivityFeed :activities="data.activity" />
  </div>
</template>
vue
<!-- app/components/home/HomeLatestArticles.vue — presentational, part of Home -->
<script setup lang="ts">
import type { Article } from '~/shared/types/Page'

defineProps<{
  articles: Article[]
}>()
</script>

<template>
  <section>
    <h2>Latest Articles</h2>
    <ul>
      <li v-for="article in articles" :key="article.id">
        {{ article.title }}
      </li>
    </ul>
  </section>
</template>
Component folder anatomy for a module (e.g.
user
):
components/user/
├── list/
│   ├── UserList.vue           # Orchestrates list view (connects store, composables)
│   ├── UserListItem.vue       # Presentational — one user row
│   └── UserListFilters.vue    # Presentational — filter controls
├── detail/
│   ├── UserDetail.vue         # Orchestrates detail view
│   ├── UserProfile.vue        # Presentational — user info card
│   └── UserActivityFeed.vue   # Presentational — activity list
└── add/
    ├── UserAdd.vue            # Orchestrates add/create flow
    └── UserAddForm.vue        # Presentational — create form
位置:
app/components/{module}/
— 对应模块的子文件夹中。
规则:
  • 以其编排的视图命名(例如
    UserDetail
    UserList
    Home
    )—
    Container
    后缀
  • 通过 props 接收页面数据
  • 连接 Pinia 状态存储以处理响应式状态与操作
  • 调用业务用例组合式函数处理数据变更
  • 计算派生状态
  • 仅传递子组件所需的数据
  • 不包含原生 HTML — 仅渲染子展示组件
vue
<!-- app/components/user/detail/UserDetail.vue -->
<script setup lang="ts">
import type { UserPageData } from '~/shared/types/Page'

const props = defineProps<{
  data: UserPageData
}>()

const userStore = useUserStore()
const { deleteUser } = useUser()

// 用服务端数据初始化状态存储
userStore.setUser(props.data.user)

// 派生状态
const isCurrentUser = computed(() =>
  userStore.currentUser?.id === props.data.user.id
)

async function handleDelete(): Promise<void> {
  await deleteUser(props.data.user.id)
  await navigateTo('/users')
}
</script>

<template>
  <div>
    <UserProfile
      :user="data.user"
      :is-current-user="isCurrentUser"
      @delete="handleDelete"
    />
    <UserActivityFeed :activities="data.activity" />
  </div>
</template>
vue
<!-- app/components/home/HomeLatestArticles.vue — 展示组件,属于首页模块 -->
<script setup lang="ts">
import type { Article } from '~/shared/types/Page'

defineProps<{
  articles: Article[]
}>()
</script>

<template>
  <section>
    <h2>最新文章</h2>
    <ul>
      <li v-for="article in articles" :key="article.id">
        {{ article.title }}
      </li>
    </ul>
  </section>
</template>
模块组件文件夹结构示例(如
user
模块):
components/user/
├── list/
│   ├── UserList.vue           # 编排列表视图(连接状态存储、组合式函数)
│   ├── UserListItem.vue       # 展示组件 — 单个用户行
│   └── UserListFilters.vue    # 展示组件 — 筛选控件
├── detail/
│   ├── UserDetail.vue         # 编排详情视图
│   ├── UserProfile.vue        # 展示组件 — 用户信息卡片
│   └── UserActivityFeed.vue   # 展示组件 — 活动列表
└── add/
    ├── UserAdd.vue            # 编排新增/创建流程
    └── UserAddForm.vue        # 展示组件 — 创建表单

Layer 3: Presentational Component

第三层:展示组件

Location: Inside the relevant module subfolder, or
app/components/ui/
for truly generic components.
Rules:
  • Completely independent — no Pinia, no composables, no $fetch
  • Communicates only through
    props
    ,
    v-model
    , and
    emit
  • Fully reusable within its module (or across the app if in
    ui/
    )
  • Uses Nuxt UI components internally
  • Can contain local UI state (e.g., open/close toggles)
vue
<!-- app/components/user/detail/UserProfile.vue -->
<script setup lang="ts">
import type { User } from '~/shared/types/User'

const props = defineProps<{
  user: User
  isCurrentUser: boolean
}>()

const emit = defineEmits<{
  delete: []
}>()

const showConfirm = ref(false)

function confirmDelete(): void {
  showConfirm.value = true
}

function handleConfirm(): void {
  showConfirm.value = false
  emit('delete')
}
</script>

<template>
  <UCard>
    <template #header>
      <div class="flex items-center justify-between">
        <h1 class="text-xl font-bold">{{ user.name }}</h1>
        <UBadge :label="user.role" />
      </div>
    </template>

    <p class="text-gray-500">{{ user.email }}</p>

    <template #footer>
      <UButton
        v-if="isCurrentUser"
        color="red"
        variant="ghost"
        @click="confirmDelete"
      >
        Delete account
      </UButton>
    </template>

    <UModal v-model="showConfirm">
      <UCard>
        <p>Are you sure you want to delete your account?</p>
        <template #footer>
          <div class="flex gap-2">
            <UButton color="red" @click="handleConfirm">Confirm</UButton>
            <UButton variant="ghost" @click="showConfirm = false">Cancel</UButton>
          </div>
        </template>
      </UCard>
    </UModal>
  </UCard>
</template>
Summary table:
LayerLocationPinia$fetchPropsEmits
Page
pages/
❌ (via
usePages
)
Orchestrating
components/{module}/
✅ (via composables)
Presentational
components/{module}/
or
components/ui/

位置: 对应模块的子文件夹中,或
app/components/ui/
(通用组件)。
规则:
  • 完全独立 — 无 Pinia、无组合式函数、无 $fetch
  • 仅通过
    props
    v-model
    emit
    通信
  • 在所属模块内可复用(若在
    ui/
    中则全应用可复用)
  • 内部使用 Nuxt UI 组件
  • 可包含本地 UI 状态(如展开/收起切换)
vue
<!-- app/components/user/detail/UserProfile.vue -->
<script setup lang="ts">
import type { User } from '~/shared/types/User'

const props = defineProps<{
  user: User
  isCurrentUser: boolean
}>()

const emit = defineEmits<{
  delete: []
}>()

const showConfirm = ref(false)

function confirmDelete(): void {
  showConfirm.value = true
}

function handleConfirm(): void {
  showConfirm.value = false
  emit('delete')
}
</script>

<template>
  <UCard>
    <template #header>
      <div class="flex items-center justify-between">
        <h1 class="text-xl font-bold">{{ user.name }}</h1>
        <UBadge :label="user.role" />
      </div>
    </template>

    <p class="text-gray-500">{{ user.email }}</p>

    <template #footer>
      <UButton
        v-if="isCurrentUser"
        color="red"
        variant="ghost"
        @click="confirmDelete"
      >
        删除账户
      </UButton>
    </template>

    <UModal v-model="showConfirm">
      <UCard>
        <p>确定要删除你的账户吗?</p>
        <template #footer>
          <div class="flex gap-2">
            <UButton color="red" @click="handleConfirm">确认</UButton>
            <UButton variant="ghost" @click="showConfirm = false">取消</UButton>
          </div>
        </template>
      </UCard>
    </UModal>
  </UCard>
</template>
层级总结表:
层级位置Pinia$fetchPropsEmits
页面组件
pages/
❌(通过
usePages
编排组件
components/{module}/
✅(通过组合式函数)
展示组件
components/{module}/
components/ui/

State Management with Pinia

Pinia 状态管理

Stores hold reactive client-side state and are organized by domain module.
ts
// app/stores/user.store.ts
import type { User } from '~/shared/types/User'

export const useUserStore = defineStore('user', () => {
  // State
  const currentUser = ref<User | null>(null)
  const users = ref<User[]>([])

  // Getters
  const isAuthenticated = computed(() => currentUser.value !== null)
  const isAdmin = computed(() =>
    currentUser.value?.roles.includes(UserRole.Admin) ?? false
  )

  // Actions
  function setCurrentUser(user: User | null): void {
    currentUser.value = user
  }

  function setUsers(list: User[]): void {
    users.value = list
  }

  function addUser(user: User): void {
    users.value.push(user)
  }

  function removeUser(id: string): void {
    users.value = users.value.filter(u => u.id !== id)
  }

  return {
    currentUser,
    users,
    isAuthenticated,
    isAdmin,
    setCurrentUser,
    setUsers,
    addUser,
    removeUser,
  }
})
Key rules:
  • Always use
    defineStore
    with the setup store syntax (composition API style)
  • Store files use
    .store.ts
    suffix:
    user.store.ts
    ,
    app.store.ts
  • Exported store function keeps the
    use
    prefix:
    useUserStore
    ,
    useAppStore
  • Stores are auto-imported via
    imports.dirs: ['stores']
    in
    nuxt.config.ts
  • Stores are only accessed in orchestrating components and composables — never in presentational components
  • Stores are hydrated from server data in orchestrating components or the
    useApp.init()
    flow

状态存储保存客户端响应式状态,按领域模块组织。
ts
// app/stores/user.store.ts
import type { User } from '~/shared/types/User'

export const useUserStore = defineStore('user', () => {
  // 状态
  const currentUser = ref<User | null>(null)
  const users = ref<User[]>([])

  // 计算属性
  const isAuthenticated = computed(() => currentUser.value !== null)
  const isAdmin = computed(() =>
    currentUser.value?.roles.includes(UserRole.Admin) ?? false
  )

  // 操作
  function setCurrentUser(user: User | null): void {
    currentUser.value = user
  }

  function setUsers(list: User[]): void {
    users.value = list
  }

  function addUser(user: User): void {
    users.value.push(user)
  }

  function removeUser(id: string): void {
    users.value = users.value.filter(u => u.id !== id)
  }

  return {
    currentUser,
    users,
    isAuthenticated,
    isAdmin,
    setCurrentUser,
    setUsers,
    addUser,
    removeUser,
  }
})
核心规则:
  • 始终使用
    defineStore
    组合式 API 语法(setup store)
  • 状态存储文件使用
    .store.ts
    后缀:
    user.store.ts
    app.store.ts
  • 导出的状态存储函数保留
    use
    前缀:
    useUserStore
    useAppStore
  • 通过
    nuxt.config.ts
    中的
    imports.dirs: ['stores']
    自动导入状态存储
  • 仅在编排组件组合式函数中访问状态存储 — 永远不要在展示组件中访问
  • 在编排组件或
    useApp.init()
    流程中用服务端数据初始化状态存储

Layouts, Middleware & Plugins

布局、中间件与插件

Layouts —
app/layouts/

布局 —
app/layouts/

Layouts wrap pages and provide shared structure (header, sidebar, footer). Nuxt automatically wraps pages using
<NuxtLayout>
in
app.vue
.
vue
<!-- app/layouts/default.vue -->
<template>
  <div class="flex flex-col min-h-screen">
    <AppHeader />
    <main class="flex-1">
      <slot />
    </main>
    <AppFooter />
  </div>
</template>
vue
<!-- app/layouts/dashboard.vue -->
<template>
  <div class="flex">
    <DashboardSidebar />
    <main class="flex-1 p-6">
      <slot />
    </main>
  </div>
</template>
Use a specific layout in a page:
vue
<!-- app/pages/dashboard/index.vue -->
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' })
</script>
Disable layout:
vue
<script setup lang="ts">
definePageMeta({ layout: false })
</script>

布局组件包裹页面,提供共享结构(头部、侧边栏、底部)。Nuxt 会自动在
app.vue
中通过
<NuxtLayout>
包裹页面。
vue
<!-- app/layouts/default.vue -->
<template>
  <div class="flex flex-col min-h-screen">
    <AppHeader />
    <main class="flex-1">
      <slot />
    </main>
    <AppFooter />
  </div>
</template>
vue
<!-- app/layouts/dashboard.vue -->
<template>
  <div class="flex">
    <DashboardSidebar />
    <main class="flex-1 p-6">
      <slot />
    </main>
  </div>
</template>
在页面中使用指定布局:
vue
<!-- app/pages/dashboard/index.vue -->
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' })
</script>
禁用布局:
vue
<script setup lang="ts">
definePageMeta({ layout: false })
</script>

Middleware —
app/middleware/

中间件 —
app/middleware/

Middleware runs before a route is rendered. Use it for authentication guards, role checks, and redirects.
Types:
  • Named middleware — explicitly applied per page via
    definePageMeta
  • Global middleware — filename ends with
    .global.ts
    — runs on every route change
ts
// app/middleware/auth.ts — named middleware
export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()

  if (!userStore.isAuthenticated) {
    return navigateTo('/login')
  }
})
ts
// app/middleware/role.ts — named middleware with meta
export default defineNuxtRouteMiddleware((to) => {
  const userStore = useUserStore()

  const requiredRole = to.meta.requiredRole as UserRole | undefined
  if (requiredRole && !userStore.currentUser?.roles.includes(requiredRole)) {
    return navigateTo('/unauthorized')
  }
})
Apply middleware in a page:
vue
<!-- app/pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: ['auth', 'role'],
  requiredRole: UserRole.Admin,
})
</script>
Global middleware example:
ts
// app/middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to) => {
  if (import.meta.client) {
    trackPageView(to.fullPath)
  }
})

中间件在路由渲染前运行。用于鉴权守卫、角色校验与页面重定向。
类型:
  • 命名中间件 — 通过
    definePageMeta
    显式应用到指定页面
  • 全局中间件 — 文件名以
    .global.ts
    结尾 — 在每次路由变更时运行
ts
// app/middleware/auth.ts — 命名中间件
export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()

  if (!userStore.isAuthenticated) {
    return navigateTo('/login')
  }
})
ts
// app/middleware/role.ts — 带元信息的命名中间件
export default defineNuxtRouteMiddleware((to) => {
  const userStore = useUserStore()

  const requiredRole = to.meta.requiredRole as UserRole | undefined
  if (requiredRole && !userStore.currentUser?.roles.includes(requiredRole)) {
    return navigateTo('/unauthorized')
  }
})
在页面中应用中间件:
vue
<!-- app/pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: ['auth', 'role'],
  requiredRole: UserRole.Admin,
})
</script>
全局中间件示例:
ts
// app/middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to) => {
  if (import.meta.client) {
    trackPageView(to.fullPath)
  }
})

Plugins —
app/plugins/

插件 —
app/plugins/

Plugins run once when the Nuxt app is created. Use them to register third-party integrations, provide global helpers, or configure libraries.
Filename conventions:
  • plugin.ts
    — runs on both server and client
  • plugin.client.ts
    — runs only on the client
  • plugin.server.ts
    — runs only on the server
  • Plugins are ordered by filename (prefix with numbers if order matters:
    01.analytics.client.ts
    )
ts
// app/plugins/sentry.ts — isomorphic plugin
export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  Sentry.init({
    dsn: config.public.sentryDsn,
    environment: config.public.environment,
  })

  nuxtApp.vueApp.config.errorHandler = (error) => {
    Sentry.captureException(error)
  }
})
ts
// app/plugins/analytics.client.ts — client-only plugin
export default defineNuxtPlugin(() => {
  // Safe to access window/document here
  const analytics = new Analytics({ token: useRuntimeConfig().public.analyticsToken })

  return {
    provide: {
      analytics,
    },
  }
})
Access provided values in components:
ts
const { $analytics } = useNuxtApp()
$analytics.track('page_view')

插件在 Nuxt 应用创建时运行一次。用于注册第三方集成、提供全局工具或配置库。
文件名约定:
  • plugin.ts
    — 在服务端和客户端都运行
  • plugin.client.ts
    — 仅在客户端运行
  • plugin.server.ts
    — 仅在服务端运行
  • 插件按文件名排序(若需指定顺序,可添加数字前缀:
    01.analytics.client.ts
ts
// app/plugins/sentry.ts — 同构插件
export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  Sentry.init({
    dsn: config.public.sentryDsn,
    environment: config.public.environment,
  })

  nuxtApp.vueApp.config.errorHandler = (error) => {
    Sentry.captureException(error)
  }
})
ts
// app/plugins/analytics.client.ts — 仅客户端插件
export default defineNuxtPlugin(() => {
  // 此处可安全访问 window/document
  const analytics = new Analytics({ token: useRuntimeConfig().public.analyticsToken })

  return {
    provide: {
      analytics,
    },
  }
})
在组件中访问插件提供的内容:
ts
const { $analytics } = useNuxtApp()
$analytics.track('page_view')

Composables

组合式函数

Composables encapsulate reactive logic and API calls. They are the primary way Vue components interact with the server.
Conventions:
  • Exported function always has
    use
    prefix:
    useUser
    ,
    useApp
  • Filename has no
    use
    prefix:
    user.ts
    ,
    app.ts
    ,
    pages.ts
  • Located in
    app/composables/
    (flat, auto-imported)
  • One file per domain module
  • Use-case composables call
    $fetch
    directly —
    useAsyncData
    is the page's responsibility
  • Composables may read and write Pinia stores
Use-case composable pattern:
ts
// app/composables/user.ts
export function useUser() {
  const userStore = useUserStore()
  const toast = useToast()

  async function createUser(dto: CreateUserDto): Promise<User | null> {
    try {
      const user = await $fetch<User>('/api/user/create', {
        method: 'POST',
        body: dto,
      })
      userStore.addUser(user)
      toast.add({ title: 'User created', color: 'green' })
      return user
    } catch (error) {
      toast.add({ title: 'Error creating user', color: 'red' })
      return null
    }
  }

  async function deleteUser(id: string): Promise<boolean> {
    try {
      await $fetch(`/api/user/${id}`, { method: 'DELETE' })
      userStore.removeUser(id)
      return true
    } catch {
      toast.add({ title: 'Error deleting user', color: 'red' })
      return false
    }
  }

  return { createUser, deleteUser }
}

组合式函数封装响应式逻辑与 API 调用。是 Vue 组件与服务端交互的主要方式。
约定:
  • 导出的函数始终带
    use
    前缀:
    useUser
    useApp
  • 文件名无
    use
    前缀:
    user.ts
    app.ts
    pages.ts
  • 位于
    app/composables/
    (扁平化,自动导入)
  • 每个领域模块一个文件
  • 业务用例组合式函数直接调用
    $fetch
    useAsyncData
    是页面组件的职责
  • 组合式函数可读写 Pinia 状态存储
业务用例组合式函数模式:
ts
// app/composables/user.ts
export function useUser() {
  const userStore = useUserStore()
  const toast = useToast()

  async function createUser(dto: CreateUserDto): Promise<User | null> {
    try {
      const user = await $fetch<User>('/api/user/create', {
        method: 'POST',
        body: dto,
      })
      userStore.addUser(user)
      toast.add({ title: '用户创建成功', color: 'green' })
      return user
    } catch (error) {
      toast.add({ title: '用户创建失败', color: 'red' })
      return null
    }
  }

  async function deleteUser(id: string): Promise<boolean> {
    try {
      await $fetch(`/api/user/${id}`, { method: 'DELETE' })
      userStore.removeUser(id)
      return true
    } catch {
      toast.add({ title: '用户删除失败', color: 'red' })
      return false
    }
  }

  return { createUser, deleteUser }
}

Nuxt UI

Nuxt UI

Nuxt UI provides a design system built on Tailwind CSS. Use its components as building blocks inside presentational components.
Core components:
ComponentPurpose
UButton
All interactive buttons
UCard
Content containers with header/footer slots
UModal
Dialog overlays
UForm
/
UFormField
Form containers with validation
UInput
,
USelect
,
UTextarea
Form inputs
UTable
Data tables
UBadge
Status labels
UAlert
Feedback messages
UNavigationMenu
Navigation menus
UToast
/
useToast
Toast notifications
Form with Zod validation:
vue
<!-- app/components/user/add/UserAddForm.vue -->
<script setup lang="ts">
import type { CreateUserDto } from '~/shared/types/User'
import { createUserSchema } from '~/shared/utils/user'

const props = defineProps<{
  loading: boolean
}>()

const emit = defineEmits<{
  submit: [dto: CreateUserDto]
}>()

const state = reactive<CreateUserDto>({
  name: '',
  email: '',
  role: UserRole.User,
})

async function handleSubmit(): Promise<void> {
  emit('submit', { ...state })
}
</script>

<template>
  <UForm
    :schema="createUserSchema"
    :state="state"
    @submit="handleSubmit"
  >
    <UFormField label="Name" name="name">
      <UInput v-model="state.name" />
    </UFormField>

    <UFormField label="Email" name="email">
      <UInput v-model="state.email" type="email" />
    </UFormField>

    <UFormField label="Role" name="role">
      <USelect
        v-model="state.role"
        :options="Object.values(UserRole)"
      />
    </UFormField>

    <UButton type="submit" :loading="loading">
      Create User
    </UButton>
  </UForm>
</template>
Toast notifications (use in orchestrating components or composables):
ts
const toast = useToast()

async function handleCreate(dto: CreateUserDto): Promise<void> {
  try {
    await createUser(dto)
    toast.add({ title: 'User created', color: 'green' })
  } catch {
    toast.add({ title: 'Failed to create user', color: 'red' })
  }
}

Nuxt UI 提供基于 Tailwind CSS 的设计系统。在展示组件中使用其组件作为基础构建块。
核心组件:
组件用途
UButton
所有交互按钮
UCard
带头部/底部插槽的内容容器
UModal
对话框浮层
UForm
/
UFormField
带校验的表单容器
UInput
,
USelect
,
UTextarea
表单输入控件
UTable
数据表格
UBadge
状态标签
UAlert
反馈提示
UNavigationMenu
导航菜单
UToast
/
useToast
消息提示框
带 Zod 校验的表单:
vue
<!-- app/components/user/add/UserAddForm.vue -->
<script setup lang="ts">
import type { CreateUserDto } from '~/shared/types/User'
import { createUserSchema } from '~/shared/utils/user'

const props = defineProps<{
  loading: boolean
}>()

const emit = defineEmits<{
  submit: [dto: CreateUserDto]
}>()

const state = reactive<CreateUserDto>({
  name: '',
  email: '',
  role: UserRole.User,
})

async function handleSubmit(): Promise<void> {
  emit('submit', { ...state })
}
</script>

<template>
  <UForm
    :schema="createUserSchema"
    :state="state"
    @submit="handleSubmit"
  >
    <UFormField label="姓名" name="name">
      <UInput v-model="state.name" />
    </UFormField>

    <UFormField label="邮箱" name="email">
      <UInput v-model="state.email" type="email" />
    </UFormField>

    <UFormField label="角色" name="role">
      <USelect
        v-model="state.role"
        :options="Object.values(UserRole)"
      />
    </UFormField>

    <UButton type="submit" :loading="loading">
      创建用户
    </UButton>
  </UForm>
</template>
消息提示框(在编排组件或组合式函数中使用):
ts
const toast = useToast()

async function handleCreate(dto: CreateUserDto): Promise<void> {
  try {
    await createUser(dto)
    toast.add({ title: '用户创建成功', color: 'green' })
  } catch {
    toast.add({ title: '用户创建失败', color: 'red' })
  }
}

Data Fetching Patterns

数据获取模式

useAsyncData
in pages

页面组件中的
useAsyncData

Use
useAsyncData
in pages to fetch data server-side with proper SSR support and deduplication.
ts
// Basic usage — one endpoint call per page
const { data, status, refresh } = await useAsyncData('home', () => usePages().getHomePage())

// With route-based key and param
const route = useRoute()
const { data } = await useAsyncData(
  `user-${route.params.id}`,
  () => usePages().getUserPage(route.params.id as string),
)
在页面组件中使用
useAsyncData
进行服务端数据获取,支持 SSR 与请求去重。
ts
// 基础用法 — 每个页面仅调用一个端点
const { data, status, refresh } = await useAsyncData('home', () => usePages().getHomePage())

// 带路由参数的动态键
const route = useRoute()
const { data } = await useAsyncData(
  `user-${route.params.id}`,
  () => usePages().getUserPage(route.params.id as string),
)

$fetch
in composables

组合式函数中的
$fetch

Use
$fetch
for mutations and imperative calls from composables (no SSR caching needed).
ts
const user = await $fetch<User>('/api/user/create', {
  method: 'POST',
  body: dto,
})
在组合式函数中使用
$fetch
处理数据变更与命令式调用(无需 SSR 缓存)。
ts
const user = await $fetch<User>('/api/user/create', {
  method: 'POST',
  body: dto,
})

callOnce
in
app.vue

app.vue
中的
callOnce

Ensures the app initialization runs exactly once during SSR and is not repeated on client hydration.
ts
await callOnce(async () => {
  const { init } = useApp()
  await init()
})

确保应用初始化逻辑仅在 SSR 时运行一次,客户端 hydration 时不再重复执行。
ts
await callOnce(async () => {
  const { init } = useApp()
  await init()
})

Error Handling

错误处理

Server side — throw
createError
:
ts
// server/api/user/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')!
  const user = await findUser(event, id)

  if (!user) {
    throw createError({ statusCode: 404, message: 'User not found' })
  }

  return user
})
Domain errors in contexts:
ts
// server/contexts/user/domain/UserError.ts
export class UserError extends Error {
  constructor(
    public readonly code: 'EMAIL_ALREADY_EXISTS' | 'NOT_FOUND',
    public readonly detail?: string,
  ) {
    super(`UserError: ${code}`)
  }
}
Map domain errors to HTTP errors in
server/utils/user.ts
:
ts
export async function createUser(event: H3Event, dto: CreateUserDto): Promise<User> {
  try {
    const db = new PostgresService()
    const repository = new PostgresUserRepository(db)
    const creator = new UserCreator(repository)
    return await creator.create(dto)
  } catch (error) {
    if (error instanceof UserError) {
      throw createError({ statusCode: 409, message: error.message })
    }
    throw createError({ statusCode: 500, message: 'Internal server error' })
  }
}
Client side — handle errors in composables, never let them bubble raw to the UI:
ts
// app/composables/user.ts
async function createUser(dto: CreateUserDto): Promise<User | null> {
  try {
    return await $fetch<User>('/api/user/create', { method: 'POST', body: dto })
  } catch (error: unknown) {
    const msg = error instanceof Error ? error.message : 'Unknown error'
    toast.add({ title: msg, color: 'red' })
    return null
  }
}
Global error page:
vue
<!-- app/error.vue -->
<script setup lang="ts">
const props = defineProps<{
  error: { statusCode: number; message: string }
}>()

function handleClear(): void {
  clearError({ redirect: '/' })
}
</script>

<template>
  <div>
    <h1>{{ error.statusCode }}</h1>
    <p>{{ error.message }}</p>
    <UButton @click="handleClear">Go home</UButton>
  </div>
</template>

服务端 — 抛出
createError
:
ts
// server/api/user/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')!
  const user = await findUser(event, id)

  if (!user) {
    throw createError({ statusCode: 404, message: '用户不存在' })
  }

  return user
})
上下文层的领域错误:
ts
// server/contexts/user/domain/UserError.ts
export class UserError extends Error {
  constructor(
    public readonly code: 'EMAIL_ALREADY_EXISTS' | 'NOT_FOUND',
    public readonly detail?: string,
  ) {
    super(`UserError: ${code}`)
  }
}
server/utils/user.ts
中将领域错误映射为 HTTP 错误:
ts
export async function createUser(event: H3Event, dto: CreateUserDto): Promise<User> {
  try {
    const db = new PostgresService()
    const repository = new PostgresUserRepository(db)
    const creator = new UserCreator(repository)
    return await creator.create(dto)
  } catch (error) {
    if (error instanceof UserError) {
      throw createError({ statusCode: 409, message: error.message })
    }
    throw createError({ statusCode: 500, message: '内部服务错误' })
  }
}
客户端 — 在组合式函数中处理错误,不要让原始错误冒泡到 UI:
ts
// app/composables/user.ts
async function createUser(dto: CreateUserDto): Promise<User | null> {
  try {
    return await $fetch<User>('/api/user/create', { method: 'POST', body: dto })
  } catch (error: unknown) {
    const msg = error instanceof Error ? error.message : '未知错误'
    toast.add({ title: msg, color: 'red' })
    return null
  }
}
全局错误页面:
vue
<!-- app/error.vue -->
<script setup lang="ts">
const props = defineProps<{
  error: { statusCode: number; message: string }
}>()

function handleClear(): void {
  clearError({ redirect: '/' })
}
</script>

<template>
  <div>
    <h1>{{ error.statusCode }}</h1>
    <p>{{ error.message }}</p>
    <UButton @click="handleClear">返回首页</UButton>
  </div>
</template>

Best Practices

最佳实践

Apply SOLID principles to the Nuxt stack

在 Nuxt 栈中应用 SOLID 原则

PrincipleNuxt application
SRPPages only fetch. Orchestrating components only coordinate. Presentational components only render.
OCPAdd new pages/use cases without touching existing endpoints or stores.
LSPRepository implementations are fully substitutable for their interfaces.
ISPSmall, focused composables (
useUser
,
useOrder
) instead of one god composable.
DIP
server/utils/
depends on context interfaces, not concrete implementations.
原则Nuxt 应用中的实践
单一职责原则(SRP)页面组件仅负责数据获取。编排组件仅负责协调。展示组件仅负责渲染。
开闭原则(OCP)新增页面/用例时无需修改现有端点或状态存储。
里氏替换原则(LSP)仓储实现可完全替代其接口。
接口隔离原则(ISP)小而专注的组合式函数(
useUser
useOrder
),而非单一的全能组合式函数。
依赖倒置原则(DIP)
server/utils/
依赖上下文接口,而非具体实现。

Keep pages dumb

保持页面组件简洁

Pages should contain no business logic. They call one endpoint and render one orchestrating component.
vue
<!-- ✅ Good — page is just a loader -->
<script setup lang="ts">
const { getHomePage } = usePages()
const { data } = await useAsyncData('home', getHomePage)
</script>

<!-- ❌ Bad — page doing too much -->
<script setup lang="ts">
const { data } = await useAsyncData('home', () => $fetch('/api/pages'))
const userStore = useUserStore()
const filtered = computed(() => data.value?.products.filter(p => p.active))
userStore.setProducts(filtered.value ?? [])
</script>
页面组件不应包含任何业务逻辑。仅调用一个端点并渲染一个编排组件。
vue
<!-- ✅ 良好实践 — 页面仅作为加载器 -->
<script setup lang="ts">
const { getHomePage } = usePages()
const { data } = await useAsyncData('home', getHomePage)
</script>

<!-- ❌ 不良实践 — 页面职责过重 -->
<script setup lang="ts">
const { data } = await useAsyncData('home', () => $fetch('/api/pages'))
const userStore = useUserStore()
const filtered = computed(() => data.value?.products.filter(p => p.active))
userStore.setProducts(filtered.value ?? [])
</script>

Keep presentational components pure

保持展示组件纯净

Presentational components must never import or call anything from outside their own file except types, Nuxt UI, and Vue primitives. If a component needs data from a store, pass it as a prop from the orchestrating component.
vue
<!-- ✅ Good -->
<script setup lang="ts">
import type { User } from '~/shared/types/User'
defineProps<{ user: User }>()
</script>

<!-- ❌ Bad — presentational touching Pinia -->
<script setup lang="ts">
const userStore = useUserStore()
</script>
展示组件除了类型定义、Nuxt UI 和 Vue 原生 API 外,不应导入或调用任何外部内容。如果组件需要状态存储中的数据,请从编排组件通过 props 传递。
vue
<!-- ✅ 良好实践 -->
<script setup lang="ts">
import type { User } from '~/shared/types/User'
defineProps<{ user: User }>()
</script>

<!-- ❌ 不良实践 — 展示组件直接操作 Pinia -->
<script setup lang="ts">
const userStore = useUserStore()
</script>

One composable per domain module

每个领域模块一个组合式函数

Split composables by bounded context, not by technical concern. Avoid composables like
useApi
or
useFetch
that grow to touch everything.
app/composables/
├── app.ts       # exports useApp() — App bootstrap
├── pages.ts     # exports usePages() — All page fetchers
├── user.ts      # exports useUser() — User use cases
├── order.ts     # exports useOrder() — Order use cases
└── product.ts   # exports useProduct()
按限界上下文拆分组合式函数,而非按技术关注点。避免创建
useApi
useFetch
这类全能型组合式函数。
app/composables/
├── app.ts       # 导出 useApp() — 应用初始化
├── pages.ts     # 导出 usePages() — 所有页面数据获取
├── user.ts      # 导出 useUser() — 用户业务用例
├── order.ts     # 导出 useOrder() — 订单业务用例
└── product.ts   # 导出 useProduct()

Never put business logic in the Vue layer

不要在 Vue 层放置业务逻辑

Business logic (validation beyond form UX, domain rules, calculations) belongs in
server/contexts/
. The Vue layer (pages, orchestrating components, composables) should only orchestrate calls and manage UI state.
ts
// ❌ Bad — business rule in a composable
export function useUser() {
  async function createUser(dto: CreateUserDto) {
    if (dto.role === UserRole.Admin && !currentUser.isAdmin) {
      throw new Error('Only admins can create admins')
    }
    return $fetch('/api/user/create', { method: 'POST', body: dto })
  }
}

// ✅ Good — business rule enforced in context application layer
// server/contexts/user/application/UserCreator.ts
export class UserCreator {
  async create(dto: CreateUserDto, requesterId: string): Promise<User> {
    if (dto.role === UserRole.Admin) {
      await this.authService.requireAdmin(requesterId)
    }
    return this.repository.create(dto)
  }
}
业务逻辑(除表单 UX 之外的校验、领域规则、计算)应放在
server/contexts/
中。Vue 层(页面组件、编排组件、组合式函数)仅负责协调调用与管理 UI 状态。
ts
// ❌ 不良实践 — 业务规则放在组合式函数中
export function useUser() {
  async function createUser(dto: CreateUserDto) {
    if (dto.role === UserRole.Admin && !currentUser.isAdmin) {
      throw new Error('仅管理员可创建管理员用户')
    }
    return $fetch('/api/user/create', { method: 'POST', body: dto })
  }
}

// ✅ 良好实践 — 业务规则在上下文应用层中实现
// server/contexts/user/application/UserCreator.ts
export class UserCreator {
  async create(dto: CreateUserDto, requesterId: string): Promise<User> {
    if (dto.role === UserRole.Admin) {
      await this.authService.requireAdmin(requesterId)
    }
    return this.repository.create(dto)
  }
}

Validate at the boundary

在边界处校验数据

Use Zod schemas from
shared/utils/
to validate all incoming data at the API boundary using
readValidatedBody
or
getValidatedQuery
. Never trust unvalidated input inside
server/utils/
or
server/contexts/
.
ts
// server/api/user/create.post.ts
export default defineEventHandler(async (event) => {
  // Validate at the edge — if this throws, H3 returns 422 automatically
  const body = await readValidatedBody(event, createUserSchema.parse)
  return createUser(event, body)
})
使用
shared/utils/
中的 Zod 校验规则,在 API 边界通过
readValidatedBody
getValidatedQuery
校验所有输入数据。永远不要在
server/utils/
server/contexts/
中信任未校验的输入。
ts
// server/api/user/create.post.ts
export default defineEventHandler(async (event) => {
  // 在边界处校验 — 若校验失败,H3 自动返回 422 错误
  const body = await readValidatedBody(event, createUserSchema.parse)
  return createUser(event, body)
})

Use
callOnce
for app initialization

使用
callOnce
进行应用初始化

Never initialize the app inside
onMounted
or a
watch
without
callOnce
. This prevents double-fetching during SSR hydration.
ts
// ✅ Good
await callOnce(init)

// ❌ Bad — runs twice (server + client)
onMounted(() => init())
永远不要在
onMounted
watch
中直接初始化应用而不使用
callOnce
。这会避免 SSR hydration 时重复请求数据。
ts
// ✅ 良好实践
await callOnce(init)

// ❌ 不良实践 — 执行两次(服务端 + 客户端)
onMounted(() => init())

Co-locate types with their domain

类型与领域代码共存

Types live in
shared/types/ModuleName.ts
, Zod schemas and utils in
shared/utils/moduleName.ts
. Never define types inline inside Vue components or server route handlers.
ts
// ✅ Good — shared/types/Product.ts
export interface Product {
  id: string
  name: string
  price: number
  currency: Currency
}

export enum Currency {
  EUR = 'EUR',
  USD = 'USD',
}
类型定义放在
shared/types/ModuleName.ts
,Zod 校验规则与工具函数放在
shared/utils/moduleName.ts
。永远不要在 Vue 组件或服务端路由处理器中内联定义类型。
ts
// ✅ 良好实践 — shared/types/Product.ts
export interface Product {
  id: string
  name: string
  price: number
  currency: Currency
}

export enum Currency {
  EUR = 'EUR',
  USD = 'USD',
}

Avoid over-engineering with YAGNI

避免过度设计,遵循 YAGNI 原则

Nuxt provides excellent conventions — don't add abstraction layers that duplicate framework features.
  • Don't create a generic
    Repository<T>
    base class unless you have multiple implementations that truly share behavior.
  • Don't add an event bus if Nuxt's built-in SSE or simple store reactivity covers the need.
  • Do start with the simplest solution and refactor when the need is proven.
Nuxt 已提供优秀的约定 — 不要添加重复框架功能的抽象层。
  • 不要创建通用的
    Repository<T>
    基类,除非你有多个真正共享行为的实现。
  • 不要添加事件总线,如果 Nuxt 内置的 SSE 或简单的状态存储响应式已满足需求。
  • 从最简单的解决方案开始,在需求明确时再重构。

Naming conventions

命名约定

ArtifactConventionExample
Pages
kebab-case
user-profile.vue
Components
PascalCase
UserProfile.vue
Composable files
camelCase
(no
use
prefix)
user.ts
Composable functions
camelCase
with
use
prefix
useUser()
Store files
camelCase
+
.store.ts
suffix
user.store.ts
Store functions
camelCase
with
use
prefix
useUserStore
Server utils
camelCase
, flat, one file per domain
server/utils/user.ts
Context classes
PascalCase
UserCreator.ts
Types/interfaces
PascalCase
CreateUserDto
Enums
PascalCase
values
UserRole.Admin
API endpoints
[resource].[method].ts
create.post.ts
Shared type files
PascalCase
shared/types/User.ts
Shared util files
camelCase
shared/utils/user.ts
Component folders
kebab-case
by module
components/user/detail/
<!-- Source references: - https://nuxt.com/docs - https://pinia.vuejs.org - https://ui.nuxt.com - https://zod.dev - Guidelines skill (guidelines/SKILL.md) — SOLID, DDD, Clean Code -->
元素约定示例
页面组件短横线命名法
user-profile.vue
组件大驼峰命名法
UserProfile.vue
组合式函数文件小驼峰命名法(无
use
前缀)
user.ts
组合式函数小驼峰命名法(带
use
前缀)
useUser()
状态存储文件小驼峰命名法 +
.store.ts
后缀
user.store.ts
状态存储函数小驼峰命名法(带
use
前缀)
useUserStore
服务端工具函数小驼峰命名法,扁平化,每个领域一个文件
server/utils/user.ts
上下文类大驼峰命名法
UserCreator.ts
类型/接口大驼峰命名法
CreateUserDto
枚举大驼峰命名法的枚举值
UserRole.Admin
API 端点
[资源].[方法].ts
create.post.ts
共享类型文件大驼峰命名法
shared/types/User.ts
共享工具函数文件小驼峰命名法
shared/utils/user.ts
组件文件夹短横线命名法(按模块)
components/user/detail/
<!-- 参考资料: - https://nuxt.com/docs - https://pinia.vuejs.org - https://ui.nuxt.com - https://zod.dev - Guidelines 规范(guidelines/SKILL.md)— SOLID、DDD、整洁代码 -->