服务器 api
在 Nuxt 中,构建一个 Server API 可以通过 server/api 目录来实现。
Server API 允许在 Nuxt 应用内部创建服务器端接口,用来处理后端逻辑,例如读取请求参数、处理表单提交、连接数据库、调用第三方服务、做权限校验等。
Nuxt 3 的服务端能力基于 Nitro,因此在编写 Server API 时,建议优先使用 Nitro / h3 提供的工具函数,例如 defineEventHandler、getQuery、readBody、getValidatedQuery、readValidatedBody、createError、setResponseStatus 等。
创建 Server API 路由
创建 server/api 目录
在 Nuxt 项目的根目录下创建 server/api 目录:
my-nuxt4-app/
├── server/
│ ├── api/
│ │ └── hello.ts
├── pages/
│ └── index.vue
├── nuxt.config.ts
└── package.json建议 Server API 文件使用 TypeScript,即 .ts 文件,而不是 .js 文件。这样可以在请求参数、请求体、返回值和业务逻辑中获得更好的类型提示与类型安全。
创建一个 API 路由文件
在 server/api 目录下创建一个 API 路由文件,例如 hello.ts:
// server/api/hello.ts
export default defineEventHandler(() => {
return {
message: 'Hello, world!'
}
})在这个示例中:
defineEventHandler用于定义一个事件处理器,处理传入的请求并返回响应。server/api/hello.ts会自动映射为/api/hello。- 返回的普通对象会被 Nuxt / Nitro 自动序列化为 JSON。
访问 API 路由
通过 <http://localhost:3000/api/hello> 访问这个 API 路由,得到的响应将是:
{
"message": "Hello, world!"
}在客户端使用 API
使用 useFetch 获取 API 数据
可以在页面或组件中使用 useFetch 钩子来获取 API 数据:
<!-- pages/index.vue -->
<template>
<div>
<h1>{{ message }}</h1>
<p v-if="pending">Loading...</p>
<p v-if="error">Failed to load data.</p>
</div>
</template>
<script setup lang="ts">
const { data, pending, error } = await useFetch('/api/hello')
const message = computed(() => data.value?.message ?? '')
</script>在这个示例中:
useFetch用于从/api/hello获取数据。pending表示请求是否正在进行。error表示请求是否失败。data.value中保存服务端返回的数据。
如果希望增强类型提示,可以为返回值定义类型:
<script setup lang="ts">
interface HelloResponse {
message: string
}
const { data, pending, error } = await useFetch<HelloResponse>('/api/hello')
</script>这样在访问 data.value?.message 时,编辑器可以提供更准确的类型提示。
创建更复杂的 API 路由
可以创建更复杂的 API 路由,包括处理请求参数、请求体、动态路由参数、响应状态码、错误处理和权限校验等。
处理请求参数
可以使用 getQuery 读取 URL query 参数:
// server/api/greet.ts
export default defineEventHandler((event) => {
const { name } = getQuery(event)
return {
message: `Hello, ${name || 'world'}!`
}
})通过 <http://localhost:3000/api/greet?name=Nuxt> 访问这个 API 路由,得到的响应将是:
{
"message": "Hello, Nuxt!"
}不过,getQuery 只负责读取参数,不负责类型校验。实际项目中不建议直接信任 query 参数,尤其是当参数会参与数据库查询、权限判断或业务计算时,应使用 getValidatedQuery 做输入验证。
使用 getValidatedQuery 校验请求参数
在 Nitro 中,推荐使用 getValidatedQuery 配合 zod 或 valibot 对 query 参数进行类型安全校验。
以 zod 为例,先安装依赖:
pnpm add zod然后创建带校验的 API:
// server/api/greet.get.ts
import { z } from 'zod'
const querySchema = z.object({
name: z.string().min(1).max(50).optional()
})
export default defineEventHandler(async (event) => {
const query = await getValidatedQuery(event, querySchema.parse)
return {
message: `Hello, ${query.name ?? 'world'}!`
}
})在这个示例中:
getValidatedQuery会读取并校验 query 参数。querySchema.parse用于执行zod校验。name必须是字符串,并且长度在 1 到 50 之间。- 如果参数不符合规则,接口会返回错误响应,而不是继续执行后续业务逻辑。
这比直接使用 getQuery 更安全,也更适合生产环境。
处理请求体
可以使用 readBody 读取 POST、PUT、PATCH 等请求中的请求体:
// server/api/echo.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
return {
message: `You said: ${body.message}`
}
})可以向 <http://localhost:3000/api/echo> 发送一个 POST 请求,并在请求体中包含 JSON 数据:
{
"message": "Hello, API!"
}响应将是:
{
"message": "You said: Hello, API!"
}不过,和 getQuery 类似,readBody 只负责读取请求体,不负责校验数据格式。实际项目中更推荐使用 readValidatedBody。
使用 readValidatedBody 校验请求体
在处理用户提交的数据时,应使用 readValidatedBody 配合 zod 或 valibot 对请求体做校验。
// server/api/echo.post.ts
import { z } from 'zod'
const bodySchema = z.object({
message: z.string().min(1).max(200)
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, bodySchema.parse)
return {
message: `You said: ${body.message}`
}
})在这个示例中:
readValidatedBody会读取并校验请求体。message必须是字符串。message不能为空,并且最多 200 个字符。- 如果请求体不符合规则,请求会被提前拦截。
这是 Nitro Server API 中处理输入数据的推荐方式。
使用 valibot 校验请求体
除了 zod,也可以使用 valibot 做输入验证。
安装依赖:
pnpm add valibot示例:
// server/api/contact.post.ts
import * as v from 'valibot'
const contactSchema = v.object({
email: v.pipe(v.string(), v.email()),
content: v.pipe(v.string(), v.minLength(1), v.maxLength(1000))
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, (value) => v.parse(contactSchema, value))
return {
success: true,
email: body.email,
message: 'Contact message received.'
}
})在这个示例中:
email必须是合法邮箱格式。content必须是 1 到 1000 个字符的字符串。- 校验逻辑集中在 schema 中,便于维护和复用。
HTTP 方法限定路由
Nuxt / Nitro 支持通过文件名约定限制 API 路由的 HTTP 方法。
例如:
server/
└── api/
├── hello.get.ts
├── hello.post.ts
├── user.put.ts
└── user.delete.ts创建只响应 GET 请求的 API
// server/api/hello.get.ts
export default defineEventHandler(() => {
return {
message: 'This route only handles GET requests.'
}
})这个文件会映射为:
GET /api/hello创建只响应 POST 请求的 API
// server/api/hello.post.ts
import { z } from 'zod'
const bodySchema = z.object({
name: z.string().min(1).max(50)
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, bodySchema.parse)
return {
message: `Hello, ${body.name}!`
}
})这个文件会映射为:
POST /api/hello也就是说,hello.post.ts 只用于处理 POST 请求,而不会处理 GET 请求。
常见 HTTP 方法文件名
常见的 HTTP 方法限定文件名包括:
server/api/example.get.ts
server/api/example.post.ts
server/api/example.put.ts
server/api/example.patch.ts
server/api/example.delete.ts建议在实际项目中优先使用这种文件名约定,而不是在同一个 API 文件中手动判断 event.node.req.method。这样路由职责更清晰,也更符合 Nuxt / Nitro 的约定式开发风格。
动态路由参数
Server API 也支持动态路由参数。
例如创建用户详情接口:
server/
└── api/
└── users/
└── [id].get.ts示例代码:
// server/api/users/[id].get.ts
import { z } from 'zod'
const idSchema = z.coerce.number().int().positive()
export default defineEventHandler((event) => {
const rawId = getRouterParam(event, 'id')
const id = idSchema.parse(rawId)
return {
id,
name: `User ${id}`
}
})访问 <http://localhost:3000/api/users/1> 时,响应示例:
{
"id": 1,
"name": "User 1"
}在这个示例中:
[id].get.ts表示动态路由。getRouterParam(event, 'id')用于读取路由参数。z.coerce.number()可以将字符串形式的id转为数字。int().positive()用于确保id是正整数。
动态路由参数同样不应直接信任,建议始终进行类型转换和合法性校验。
错误处理与状态码
在 Server API 中,可以使用 createError 抛出标准错误响应。
// server/api/users/[id].get.ts
import { z } from 'zod'
const idSchema = z.coerce.number().int().positive()
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
export default defineEventHandler((event) => {
const rawId = getRouterParam(event, 'id')
const id = idSchema.parse(rawId)
const user = users.find((item) => item.id === id)
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
return user
})在这个示例中:
- 当用户不存在时,使用
createError返回 404。 - 不建议直接返回
{ error: 'xxx' }作为错误处理方式。 - 使用标准 HTTP 状态码有利于客户端统一处理错误。
如果是创建资源,可以使用 setResponseStatus 设置响应状态码:
// server/api/posts.post.ts
import { z } from 'zod'
const bodySchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1).max(5000)
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, bodySchema.parse)
const post = {
id: Date.now(),
title: body.title,
content: body.content
}
setResponseStatus(event, 201)
return {
post
}
})在这个示例中:
- 创建成功后返回 201 状态码。
setResponseStatus(event, 201)明确表达“资源已创建”。
路由中间件
除了 server/api,Nuxt 还支持通过 server/middleware 创建服务端中间件。
中间件适合处理横切逻辑,例如:
- 鉴权。
- 日志记录。
- 统计请求耗时。
- 注入请求上下文。
- 统一设置响应头。
- 对部分 API 做访问限制。
创建服务端中间件
目录结构示例:
my-nuxt4-app/
├── server/
│ ├── api/
│ │ └── admin/
│ │ └── profile.get.ts
│ └── middleware/
│ └── auth.ts
├── pages/
│ └── index.vue
├── nuxt.config.ts
└── package.json示例代码:
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const url = getRequestURL(event)
if (!url.pathname.startsWith('/api/admin')) {
return
}
const authorization = getHeader(event, 'authorization')
const config = useRuntimeConfig(event)
if (authorization !== `Bearer ${config.apiToken}`) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized'
})
}
event.context.auth = {
role: 'admin'
}
})在这个示例中:
- 中间件会在 API 路由处理前执行。
- 只有
/api/admin开头的接口会触发鉴权逻辑。 - 鉴权失败时抛出 401 错误。
- 鉴权成功后,将用户信息写入
event.context.auth,后续 API 可以读取。
在 API 中读取中间件注入的上下文
// server/api/admin/profile.get.ts
export default defineEventHandler((event) => {
return {
profile: {
role: event.context.auth?.role ?? 'guest'
}
}
})在实际项目中,可以在中间件中解析用户 token,并将用户 ID、角色、权限等信息写入 event.context,从而避免在每个 API 中重复写鉴权逻辑。
使用运行时配置
Server API 中不应直接把密钥、数据库连接字符串、第三方 API Token 等敏感信息写死在代码里。推荐使用 Nuxt 的 runtimeConfig。
在 nuxt.config.ts 中配置:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
apiToken: process.env.API_TOKEN,
public: {
siteName: 'My Nuxt App'
}
}
})在 Server API 中读取:
// server/api/config-demo.get.ts
export default defineEventHandler((event) => {
const config = useRuntimeConfig(event)
return {
hasApiToken: Boolean(config.apiToken),
siteName: config.public.siteName
}
})在这个示例中:
runtimeConfig.apiToken只在服务端可用。runtimeConfig.public.siteName可以在客户端和服务端使用。- 敏感配置应放在非
public字段下。
组合一个更完整的 CRUD 示例
下面是一个更接近真实项目的文章接口示例,包含方法限定、请求体验证、动态路由参数和错误处理。
目录结构:
server/
└── api/
└── posts/
├── index.get.ts
├── index.post.ts
└── [id].get.ts获取文章列表
// server/api/posts/index.get.ts
const posts = [
{
id: 1,
title: 'Nuxt Server API',
content: 'Introduction to Nuxt Server API.'
},
{
id: 2,
title: 'Nitro Best Practices',
content: 'Using validation and method-specific routes.'
}
]
export default defineEventHandler(() => {
return {
posts
}
})创建文章
// server/api/posts/index.post.ts
import { z } from 'zod'
const bodySchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1).max(5000)
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, bodySchema.parse)
const post = {
id: Date.now(),
title: body.title,
content: body.content
}
setResponseStatus(event, 201)
return {
post
}
})获取文章详情
// server/api/posts/[id].get.ts
import { z } from 'zod'
const idSchema = z.coerce.number().int().positive()
const posts = [
{
id: 1,
title: 'Nuxt Server API',
content: 'Introduction to Nuxt Server API.'
},
{
id: 2,
title: 'Nitro Best Practices',
content: 'Using validation and method-specific routes.'
}
]
export default defineEventHandler((event) => {
const rawId = getRouterParam(event, 'id')
const id = idSchema.parse(rawId)
const post = posts.find((item) => item.id === id)
if (!post) {
throw createError({
statusCode: 404,
statusMessage: 'Post not found'
})
}
return {
post
}
})在这个 CRUD 示例中:
index.get.ts用于获取列表。index.post.ts用于创建资源。[id].get.ts用于获取详情。- 所有用户输入都经过校验。
- 创建资源时返回 201。
- 资源不存在时返回 404。
客户端提交数据
可以在页面中使用 $fetch 向 Server API 提交数据。
<!-- pages/posts/create.vue -->
<template>
<form @submit.prevent="submit">
<input v-model="title" placeholder="Title" />
<textarea v-model="content" placeholder="Content" />
<button type="submit">Create Post</button>
<p v-if="message">{{ message }}</p>
</form>
</template>
<script setup lang="ts">
const title = ref('')
const content = ref('')
const message = ref('')
async function submit() {
try {
const response = await $fetch('/api/posts', {
method: 'POST',
body: {
title: title.value,
content: content.value
}
})
message.value = 'Post created successfully.'
console.log(response)
} catch (error) {
message.value = 'Failed to create post.'
console.error(error)
}
}
</script>在这个示例中:
$fetch适合在事件处理函数中主动发起请求。useFetch更适合在页面加载或组件初始化时获取数据。- 客户端可以做基础表单校验,但服务端仍然必须做最终校验。
Server API 最佳实践
优先使用 TypeScript
建议所有 Server API 文件都使用 .ts:
server/api/hello.ts
server/api/users/[id].get.ts
server/api/posts/index.post.tsTypeScript 可以帮助你在开发阶段发现类型错误,并提升代码可维护性。
不要直接信任客户端输入
不要直接信任以下数据:
- query 参数。
- route params。
- request body。
- headers。
- cookies。
推荐做法:
const query = await getValidatedQuery(event, querySchema.parse)
const body = await readValidatedBody(event, bodySchema.parse)客户端提交的数据只能作为“未验证输入”,必须在服务端再次校验。
使用 HTTP 方法限定文件名
推荐:
server/api/users.get.ts
server/api/users.post.ts
server/api/users/[id].put.ts
server/api/users/[id].delete.ts不推荐把所有方法都写在一个文件中:
// 不推荐
export default defineEventHandler((event) => {
if (event.node.req.method === 'GET') {
// ...
}
if (event.node.req.method === 'POST') {
// ...
}
})方法拆分后,每个文件只负责一种请求,代码更清晰,也更容易测试。
使用标准 HTTP 状态码
常见状态码:
200 OK
201 Created
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Entity
500 Internal Server Error示例:
throw createError({
statusCode: 404,
statusMessage: 'Resource not found'
})创建资源时:
setResponseStatus(event, 201)使用 runtimeConfig 管理配置
敏感配置不要写死在代码中,也不要放在 public 字段下。
推荐:
const config = useRuntimeConfig(event)
const apiToken = config.apiToken不推荐:
const apiToken = 'hard-coded-token'统一处理鉴权逻辑
如果多个 API 都需要鉴权,推荐放在 server/middleware 中统一处理,而不是在每个 API 文件中重复写同样的代码。
适合放在中间件中的逻辑:
- 检查 token。
- 解析用户身份。
- 注入
event.context.user。 - 校验访问路径。
- 记录请求日志。
避免返回敏感信息
接口响应中不要返回:
- 密码。
- token。
- session secret。
- 数据库连接信息。
- 内部错误堆栈。
- 第三方服务密钥。
错误响应应尽量简洁:
throw createError({
statusCode: 500,
statusMessage: 'Internal server error'
})不要把内部错误对象完整返回给客户端。
保持接口返回结构稳定
建议接口返回结构保持稳定,例如:
return {
data: {
id: 1,
name: 'Alice'
}
}或者:
return {
success: true,
data: {
id: 1,
name: 'Alice'
}
}不要在同一个接口中有时返回对象、有时返回字符串、有时返回数组。稳定的数据结构有利于客户端维护。
为复杂逻辑拆分 service 层
当 API 逻辑变复杂时,不建议把所有业务代码都写在路由文件中。可以将业务逻辑拆到 server/services 中。
目录示例:
server/
├── api/
│ └── users/
│ └── [id].get.ts
└── services/
└── user.ts示例:
// server/services/user.ts
export async function getUserById(id: number) {
return {
id,
name: `User ${id}`
}
}在 API 中调用:
// server/api/users/[id].get.ts
import { z } from 'zod'
import { getUserById } from '../../services/user'
const idSchema = z.coerce.number().int().positive()
export default defineEventHandler(async (event) => {
const rawId = getRouterParam(event, 'id')
const id = idSchema.parse(rawId)
const user = await getUserById(id)
return {
user
}
})这样可以让 API 路由只负责请求处理,让 service 层负责业务逻辑。
推荐目录结构
一个较完整的 Nuxt Server API 目录结构可以参考:
my-nuxt4-app/
├── server/
│ ├── api/
│ │ ├── hello.get.ts
│ │ ├── greet.get.ts
│ │ ├── echo.post.ts
│ │ ├── posts/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ └── [id].get.ts
│ │ └── admin/
│ │ └── profile.get.ts
│ ├── middleware/
│ │ └── auth.ts
│ └── services/
│ └── user.ts
├── pages/
│ ├── index.vue
│ └── posts/
│ └── create.vue
├── nuxt.config.ts
└── package.json这个结构中:
server/api用于定义 API 路由。server/middleware用于定义服务端中间件。server/services用于放置业务逻辑。- API 文件使用
.get.ts、.post.ts等方法限定命名。 - 输入校验集中在 API 入口层完成。
- 复杂业务逻辑从 API 路由中拆分出去。
小结
Nuxt Server API 提供了一种在 Nuxt 应用内部编写后端接口的方式。通过 server/api 目录,可以快速创建 API 路由,并在页面或组件中通过 useFetch、$fetch 等方式调用。
在实际项目中,建议遵循以下原则:
- 使用 TypeScript 编写 Server API。
- 使用
defineEventHandler定义 API 处理函数。 - 使用
getValidatedQuery校验 query 参数。 - 使用
readValidatedBody校验请求体。 - 使用
.get.ts、.post.ts等文件名约定限定 HTTP 方法。 - 使用
server/middleware处理鉴权、日志等通用逻辑。 - 使用
createError和setResponseStatus处理错误和状态码。 - 使用
runtimeConfig管理敏感配置。 - 将复杂业务逻辑拆分到 service 层。
- 不要直接信任客户端传入的任何数据。
通过这些实践,可以让 Nuxt Server API 更安全、更清晰,也更适合在生产项目中长期维护。