项目结构
按业务领域组织 API 接口、类型定义和目录结构
按业务领域组织
推荐按业务领域(domain)而非技术层级来组织代码。当项目有 10+ 个接口时,扁平化的 api/ 目录会变得难以维护。
Nuxt 4 引入了 shared/ 目录(v3.14+),其中 shared/types/ 和 shared/utils/ 会被自动导入到 Vue app 和 Nitro server 两侧,适合放置前后端共享的类型和工具函数。
shared/ 目录中的代码不能导入任何 Vue 或 Nitro 专有代码(如 ref、defineEventHandler 等)。非 shared/utils/ 和 shared/types/ 下的文件不会被自动导入,需通过 #shared 别名手动导入。完整示例
类型定义
前后端共享的业务实体和通用类型放在 shared/types/(会被自动导入到 app 和 server):
shared/types/common.ts
export interface User {
id: string
name: string
email: string
role: UserRole
}
export type UserRole = 'admin' | 'editor' | 'viewer'
shared/types/api.ts
/** 分页查询参数 */
export interface PageQuery {
page?: number
pageSize?: number
}
/** 分页响应 */
export interface PageResult<T> {
items: T[]
total: number
}
/** 通用 API 响应包装 */
export interface ApiResponse<T> {
code: number
data: T
message: string
}
仅 app 侧使用的接口入参/出参类型放在各模块的 types.ts:
app/api/modules/user/types.ts
// User、UserRole 等实体类型已在 shared/types/ 中,自动导入,无需 import
export interface CreateUserRequest {
name: string
email: string
role: UserRole
}
export interface UserListQuery extends PageQuery {
keyword?: string
}
// 直接复用 shared 中的通用分页类型
export type UserListResponse = PageResult<User>
API 方法
每个模块的 index.ts 封装该领域的所有 API 调用。使用 Movk Nuxt 提供的 $api 实例,无需手动创建 $fetch.create():
app/api/modules/user/index.ts
import type { CreateUserRequest, UserListQuery, UserListResponse } from './types'
// User 类型已在 shared/types/ 中定义,自动导入
export function fetchUsers(query: UserListQuery) {
return useNuxtApp().$api<UserListResponse>('/users', { query })
}
export function fetchUser(id: string) {
return useNuxtApp().$api<User>(`/users/${id}`)
}
export function createUser(data: CreateUserRequest) {
return useNuxtApp().$api<User>('/users', { method: 'POST', body: data })
}
export function deleteUser(id: string) {
return useNuxtApp().$api(`/users/${id}`, { method: 'DELETE' })
}
在 server route 中,也可以直接使用 shared/types/ 中的类型(自动导入):
server/api/users/index.get.ts
// User、PageResult 等类型自动导入,无需 import
export default defineEventHandler(async (): Promise<PageResult<User>> => {
const users = await db.user.findMany()
const total = await db.user.count()
return { items: users, total }
})
在 Composable 中使用
app/composables/useUserActions.ts
import { createUser, deleteUser } from '~/api/modules/user'
import type { CreateUserRequest } from '~/api/modules/user/types'
export function useUserActions() {
async function remove(id: string) {
await deleteUser(id)
await refreshNuxtData()
}
async function create(data: CreateUserRequest) {
return await createUser(data)
}
return { remove, create }
}
或者直接使用 useApiFetch 进行响应式数据获取:
app/composables/useUserList.ts
import type { UserListResponse } from '~/api/modules/user/types'
export function useUserList() {
const query = ref({ page: 1, pageSize: 20, keyword: '' })
// useApiFetch 自动基于 URL + query 生成缓存 key
const { data, status, refresh } = useApiFetch<UserListResponse>('/users', {
query,
watch: [query],
})
return {
users: computed(() => data.value?.items ?? []),
total: computed(() => data.value?.total ?? 0),
status,
refresh,
query,
}
}
类型复用策略
| 类型位置 | 适用范围 | 示例 |
|---|---|---|
shared/types/common.ts | 前后端共享的业务实体 | User、Order、FileInfo |
shared/types/api.ts | 前后端共享的 API 通用结构 | PageQuery、PageResult<T>、ApiResponse<T> |
app/api/modules/user/types.ts | 仅 app 侧使用的接口入参/出参 | CreateUserRequest、UserListQuery |
组件文件内或 app/ 下 | Vue 专有类型(依赖 Vue) | Props、Emits、组件类型 |
与 Movk Nuxt 的集成
如果不使用 Movk Nuxt,你需要手动搭建以下基础设施:
plugins/api.ts (手动搭建)
// 1. 插件中创建自定义 $fetch 实例
export default defineNuxtPlugin(() => {
const api = $fetch.create({
baseURL: useRuntimeConfig().public.apiBase,
onRequest({ options }) {
// 手动注入 auth token
const token = useCookie('token')
if (token.value) {
options.headers.set('Authorization', `Bearer ${token.value}`)
}
},
onResponseError({ response }) {
// 手动处理 401
if (response.status === 401) {
navigateTo('/login')
}
// 手动显示 toast
if (import.meta.client) {
useToast().error(response._data?.message ?? '请求失败')
}
},
})
return { provide: { api } }
})
使用 Movk Nuxt 后,上述代码全部由模块配置替代:
nuxt.config.ts
export default defineNuxtConfig({
modules: ['@movk/nuxt'],
movk: {
api: {
endpoints: {
default: { baseURL: '/api' },
},
auth: { enabled: true, tokenPath: 'user.accessToken' },
toast: { success: true, error: true },
},
},
})