Nuxt: 服务端 API Routes
最后更新:2026-08-26
Bob 需要给 MegaShop 加后端接口——商品增删改查、购物车操作、用户认证。传统方案要单独部署一个 Express 服务器,前后端联调头疼。Charlie 发现 Nuxt 3 的 server/ 目录就能写 API,前后端一个项目搞定。
1. 你将学到
- server/api/ 路由定义与 HTTP 方法映射
- 事件处理:defineEventHandler / readBody / getQuery / getRouterParam
- server/middleware/ 全局拦截
- 跨域与 CORS 处理
- MegaShop /api/products / /api/cart / /api/auth 接口实战
2. 一个管理员的真实故事
(1) 痛点:前后端分离的联调噩梦
Bob 用 Express 写了独立的 API 服务器,端口 4000。前端 Nuxt 在 3000。开发时要配 proxy、处理跨域,部署要两套 CI/CD。Alice 的购物车请求偶尔因跨域被拦截,线上报错。
(2) Nuxt Server API 的解法
Nuxt 3 的 server/ 目录直接在项目内写 API,同域同端口,无跨域问题:
TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler(() => {
return { items: products, total: 1000000 }
})
(3) 收益:全栈一体
Bob 只维护一个项目,API 和前端同域部署,Alice 再也不会遇到跨域错误,部署流程简化 50%。
3. API 路由定义
(1) 文件路由映射规则
| 文件路径 | HTTP 方法 | 路由 URL |
|---|---|---|
| server/api/products.ts | ALL | /api/products |
| server/api/products/index.ts | ALL | /api/products |
| server/api/products/index.get.ts | GET | /api/products |
| server/api/products/index.post.ts | POST | /api/products |
| server/api/products/[id].get.ts | GET | /api/products/:id |
| server/api/products/[id].put.ts | PUT | /api/products/:id |
| server/api/products/[id].delete.ts | DELETE | /api/products/:id |
| server/api/auth/login.post.ts | POST | /api/auth/login |
(2) API 请求处理生命周期
sequenceDiagram
participant C as Client
participant M as Server Middleware
participant H as Event Handler
participant D as Data Source
C->>M: HTTP Request
M->>M: Auth check / CORS / Logging
M->>H: Pass event
H->>D: Query / Mutation
D-->>H: Data result
H-->>M: HTTP Response
M-->>C: JSON Response
▶ 示例:GET 商品列表
TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler((event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 20
const category = query.category as string
let filtered = mockProducts
if (category) {
filtered = filtered.filter(p => p.category === category)
}
const start = (page - 1) * limit
return {
items: filtered.slice(start, start + limit),
total: filtered.length,
page,
limit
}
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:POST 创建商品
TYPESCRIPT
// server/api/products/index.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate required fields
if (!body.name || !body.price) {
throw createError({
statusCode: 400,
message: 'Name and price are required'
})
}
const newProduct = {
id: mockProducts.length + 1,
name: body.name,
price: Number(body.price),
category: body.category || 'uncategorized',
inStock: body.inStock ?? true,
image: body.image || '/images/placeholder.webp'
}
mockProducts.push(newProduct)
return { product: newProduct, message: 'Product created successfully' }
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:动态参数路由
TYPESCRIPT
// server/api/products/[id].get.ts
export default defineEventHandler((event) => {
const id = Number(getRouterParam(event, 'id'))
const product = mockProducts.find(p => p.id === id)
if (!product) {
throw createError({
statusCode: 404,
message: 'Product not found'
})
}
return product
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:PUT 更新商品
TYPESCRIPT
// server/api/products/[id].put.ts
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'))
const body = await readBody(event)
const index = mockProducts.findIndex(p => p.id === id)
if (index === -1) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
mockProducts[index] = { ...mockProducts[index], ...body }
return { product: mockProducts[index], message: 'Product updated' }
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:DELETE 删除商品
TYPESCRIPT
// server/api/products/[id].delete.ts
export default defineEventHandler((event) => {
const id = Number(getRouterParam(event, 'id'))
const index = mockProducts.findIndex(p => p.id === id)
if (index === -1) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
const deleted = mockProducts.splice(index, 1)
return { product: deleted[0], message: 'Product deleted' }
})
输出:
TEXT
📖 仅展示
// 执行成功
4. 事件处理工具函数
(1) 请求工具速查
| 函数 | 用途 | 示例 |
|---|---|---|
| getQuery(event) | 获取 URL 查询参数 | { page: '1', limit: '20' } |
| getRouterParam(event, key) | 获取路径参数 | /products/123 → '123' |
| readBody(event) | 读取请求体 | { name: 'Product', price: 99 } |
| getHeader(event, key) | 获取请求头 | 'Bearer token...' |
| getCookie(event, key) | 获取 Cookie | 'session-id-xxx' |
| setCookie(event, key, val, opts) | 设置 Cookie | setCookie(event, 'token', jwt, { httpOnly: true }) |
| setHeader(event, key, val) | 设置响应头 | setHeader(event, 'x-total', '1000') |
| createError(opts) | 抛出错误 | createError({ statusCode: 404 }) |
(2) 响应格式约定
| 场景 | 状态码 | 响应体 |
|---|---|---|
| 查询成功 | 200 | { items: [], total: 0 } |
| 创建成功 | 201 | { product: {}, message: '...' } |
| 更新成功 | 200 | { product: {}, message: '...' } |
| 删除成功 | 200 | { message: '...' } |
| 参数错误 | 400 | { statusCode: 400, message: '...' } |
| 未认证 | 401 | { statusCode: 401, message: '...' } |
| 未找到 | 404 | { statusCode: 404, message: '...' } |
5. Server Middleware
▶ 示例:全局日志中间件
TYPESCRIPT
// server/middleware/logger.ts
export default defineEventHandler((event) => {
const start = Date.now()
const method = getMethod(event)
const url = getRequestURL(event)
// Log after response
event.node.res.on('finish', () => {
const duration = Date.now() - start
const status = event.node.res.statusCode
console.log(`${method} ${url} → ${status} (${duration}ms)`)
})
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Auth 中间件
TYPESCRIPT
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const protectedPaths = ['/api/admin', '/api/orders', '/api/cart']
const url = getRequestURL(event)
const isProtected = protectedPaths.some(path => url.pathname.startsWith(path))
if (!isProtected) return
const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) {
throw createError({ statusCode: 401, message: 'Authentication required' })
}
// Verify JWT token (simplified)
try {
const payload = verifyToken(token)
event.context.user = payload
} catch {
throw createError({ statusCode: 401, message: 'Invalid token' })
}
})
输出:
TEXT
📖 仅展示
// 执行成功
6. CORS 跨域处理
▶ 示例:Nitro 内置 CORS 配置
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/api/**': { cors: true }
}
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:自定义 CORS 中间件
TYPESCRIPT
// server/middleware/cors.ts
export default defineEventHandler((event) => {
const origin = getHeader(event, 'origin') || '*'
setHeader(event, 'Access-Control-Allow-Origin', origin)
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization')
setHeader(event, 'Access-Control-Max-Age', '86400')
// Handle preflight
if (getMethod(event) === 'OPTIONS') {
event.node.res.statusCode = 204
return ''
}
})
输出:
TEXT
📖 仅展示
// 执行成功
(1) CORS 方案对比
| 方案 | 配置方式 | 灵活性 | 适用场景 |
|---|---|---|---|
| routeRules cors: true | nuxt.config.ts | 低(全开/全关) | 开发环境 |
| 自定义中间件 | server/middleware/ | 高(细粒度) | 生产环境 |
| Nitro routeRules 高级 | routeRules per-route | 中 | 混合需求 |
7. 综合示例:MegaShop API 体系
TYPESCRIPT
// server/api/cart/index.get.ts - Get cart items
export default defineEventHandler((event) => {
const userId = event.context.user?.id
if (!userId) {
throw createError({ statusCode: 401, message: 'Login required' })
}
const cart = mockCarts.find(c => c.userId === userId)
return cart || { userId, items: [], total: 0 }
})
TYPESCRIPT
// server/api/cart/index.post.ts - Add item to cart
export default defineEventHandler(async (event) => {
const userId = event.context.user?.id
const { productId, quantity = 1 } = await readBody(event)
const product = mockProducts.find(p => p.id === productId)
if (!product) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
if (!product.inStock) {
throw createError({ statusCode: 400, message: 'Product out of stock' })
}
// Add or update cart item
let cart = mockCarts.find(c => c.userId === userId)
if (!cart) {
cart = { userId, items: [], total: 0 }
mockCarts.push(cart)
}
const existing = cart.items.find(i => i.productId === productId)
if (existing) {
existing.quantity += quantity
} else {
cart.items.push({ productId, name: product.name, price: product.price, quantity })
}
cart.total = cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
return { cart, message: 'Item added to cart' }
})
❓ 常见问题
Q server/ 下的代码会被客户端 bundle 包含吗?
A 不会。Nitro 单独打包 server/ 代码,不会泄漏到客户端。server 代码可以安全使用 Node.js API 和密钥。
Q API 路由文件命名中 .get/.post 是必须的吗?
A 不必须。没有方法后缀的文件响应所有 HTTP 方法。加后缀可以精确控制每个方法的处理逻辑,推荐使用。
Q createError 和 throw new Error 有什么区别?
A createError 是 H3 提供的,会返回正确的 HTTP 状态码和 JSON 响应。throw new Error 会返回 500 内部错误。API 路由中推荐用 createError。
Q Server middleware 和 client middleware 有什么区别?
A Server middleware 在 server/middleware/,拦截 API 请求(鉴权/日志/CORS)。Client middleware 在 middleware/,拦截页面路由跳转(权限守卫/重定向)。
Q 如何在 API 中使用 runtimeConfig 的私有变量?
A 直接用 useRuntimeConfig() 获取,私有变量只在服务端可用:
const config = useRuntimeConfig() → config.databaseUrl。Q API 能不能返回流式响应(SSE)?
A 可以。用 event.node.res.write() 逐块写入数据,配合 Content-Type: text/event-stream 实现 SSE。Nitro 完整支持 Node.js 原生响应 API。
📖 小节
- Nuxt 3 server/ 目录即 API:文件路径映射路由,方法后缀映射 HTTP 方法
- defineEventHandler + readBody/getQuery/getRouterParam 处理请求
- Server middleware 全局拦截:日志、鉴权、CORS
- 同域部署天然无跨域,需要时可用 routeRules cors 或自定义中间件
- MegaShop 用 /api/products + /api/cart + /api/auth 构建完整后端
📝 作业
- 基础题(难度⭐):创建 GET /api/products 和 GET /api/products/[id] 接口,返回 mock 商品数据
- 进阶题(难度⭐⭐):实现完整的 CRUD API(GET/POST/PUT/DELETE),添加参数校验和错误处理
- 挑战题(难度⭐⭐⭐):实现 server middleware 鉴权,未登录用户访问 /api/cart 返回 401,登录后正常操作
---|