API 评审清单
API 评审关注对外/对内接口的契约设计——路径、方法、参数、响应、错误、版本、安全。一个设计糟糕的 API 会让所有调用方都受苦,而且改起来成本极高(涉及所有客户端)。
要点
- API 评审关注接口契约:路径、方法、参数、响应、错误、版本
- 重点检查:命名规范、参数校验、错误处理、分页、幂等、安全
- 好的 API 设计让调用方能据此行动,错误信息能指导修复
- Hono 的 OpenAPI 集成可以自动生成文档,减少文档与代码不一致
1. 适用时机
- 新增对外 API(给前端、移动端、第三方调用)
- 新增对内 API(跨服务调用)
- 修改已有 API(加字段、改语义、改错误码)
- 引入新的 API 风格约定(从 REST 切到 GraphQL 等)
不需要 API 评审的场景:
- 内部函数签名调整(不暴露为 HTTP 接口)
- 同一接口的纯性能优化(行为不变)
2. 评审前准备
- OpenAPI/Swagger 文档:接口路径、方法、参数、响应 schema
- 请求/响应示例:至少一个正常、一个错误的完整示例
- 调用方列表:谁会调用这个接口?已通知了吗?
- 兼容性分析:对老客户端有没有破坏性变更?
- 性能估算:预期 QPS、平均响应时间、P99
3. 核心检查项
3.1 路径与方法
- 路径命名清晰,符合团队约定(kebab-case、复数名词等)
- 资源名用名词,不用动词(
/users而不是/getUsers) - HTTP 方法语义正确(GET 查询、POST 创建、PUT 替换、PATCH 部分更新、DELETE 删除)
- 路径层级合理(不超过 3-4 层,太深考虑拆分)
- 没有暴露内部实现细节(不要
/internal/db/users)
Hono 示例:RESTful 路由设计
import { Hono } from 'hono'
const app = new Hono()
// 好的路由设计
app.get('/api/v1/users', (c) => {}) // 获取用户列表
app.get('/api/v1/users/:id', (c) => {}) // 获取单个用户
app.post('/api/v1/users', (c) => {}) // 创建用户
app.put('/api/v1/users/:id', (c) => {}) // 替换用户
app.patch('/api/v1/users/:id', (c) => {}) // 部分更新用户
app.delete('/api/v1/users/:id', (c) => {}) // 删除用户
// 嵌套资源
app.get('/api/v1/users/:userId/orders', (c) => {}) // 获取用户的订单
// 坏的路由设计(避免)
app.get('/api/v1/getUsers', (c) => {}) // 动词
app.get('/api/v1/user', (c) => {}) // 单数
app.post('/api/v1/createUser', (c) => {}) // 动词
app.get('/api/v1/users/searchAndExport', (c) => {}) // 混合操作3.2 请求参数
- 路径参数、查询参数、请求体分工清晰
- 路径参数:资源标识(
/users/:id) - 查询参数:过滤、排序、分页(
?status=active&sort=-created) - 请求体:复杂对象、创建/更新操作
- 路径参数:资源标识(
- 必填/可选字段标注清楚
- 字段类型明确(字符串、数字、枚举、日期格式)
- 参数校验规则明确(长度、范围、格式、正则)
- 默认值合理,文档里有说明
Hono 示例:参数校验
import { Hono } from 'hono'
import { validator } from 'hono/validator'
import { z } from 'zod'
const app = new Hono()
// 使用 Zod 定义请求 schema
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['user', 'admin']).default('user')
})
// 请求体校验
app.post('/api/v1/users', async (c) => {
const body = await c.req.json()
const result = CreateUserSchema.safeParse(body)
if (!result.success) {
return c.json({
code: 'VALIDATION_ERROR',
message: '请求参数校验失败',
details: result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message
}))
}, 422)
}
const user = await createUser(result.data)
return c.json(user, 201)
})
// 查询参数校验
const ListUsersSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
status: z.enum(['active', 'inactive']).optional(),
sort: z.string().default('-created_at')
})
app.get('/api/v1/users', async (c) => {
const query = c.req.query()
const result = ListUsersSchema.safeParse(query)
if (!result.success) {
return c.json({
code: 'VALIDATION_ERROR',
message: '查询参数校验失败',
details: result.error.issues
}, 400)
}
const users = await listUsers(result.data)
return c.json(users)
})3.3 响应设计
- 响应 schema 稳定(不会每次返回不同结构)
- 字段命名一致(snake_case 或 camelCase 统一)
- 分页接口统一(
page/offset、limit、total、next_cursor) - 时间格式统一(推荐 ISO 8601:
2026-06-21T10:00:00Z) - 空结果处理一致(空数组
[]vs 空对象{}vs null,统一选一个) - 大响应考虑压缩(gzip/brotli)
Hono 示例:统一响应格式
import { Hono } from 'hono'
const app = new Hono()
// 统一的成功响应格式
interface ApiResponse<T> {
data: T
meta?: {
pagination?: {
page: number
limit: number
total: number
hasMore: boolean
}
}
}
// 列表接口
app.get('/api/v1/users', async (c) => {
const page = 1
const limit = 20
const { users, total } = await listUsers({ page, limit })
const response: ApiResponse<User[]> = {
data: users,
meta: {
pagination: {
page,
limit,
total,
hasMore: page * limit < total
}
}
}
return c.json(response)
})
// 空结果:返回空数组,不是 null
app.get('/api/v1/users/:id/orders', async (c) => {
const orders = await getOrdersByUserId(c.req.param('id'))
// 即使没有订单,也返回空数组
return c.json({
data: orders || [] // 不是 null
})
})
// 时间格式:ISO 8601
interface User {
id: string
name: string
createdAt: string // "2026-06-21T10:00:00Z"
updatedAt: string // "2026-06-21T10:00:00Z"
}3.4 状态码与错误
- HTTP 状态码使用正确
- 200 成功、201 创建成功、204 无内容
- 400 客户端错误、401 未认证、403 无权限、404 未找到
- 409 冲突、422 校验失败、429 限流
- 500 服务端错误、502 网关错误、503 不可用
- 错误响应结构统一(
{ code, message, details }) - 错误码机器可读(
USER_NOT_FOUND),不是纯自然语言 - 错误信息对用户友好(不暴露内部堆栈)
- 校验错误能指出具体字段和原因
Hono 示例:统一错误处理
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// 统一错误响应格式
interface ErrorResponse {
code: string
message: string
details?: unknown
}
// 业务错误码
const ErrorCodes = {
// 客户端错误
VALIDATION_ERROR: 'VALIDATION_ERROR',
USER_NOT_FOUND: 'USER_NOT_FOUND',
INSUFFICIENT_PERMISSION: 'INSUFFICIENT_PERMISSION',
// 业务错误
ORDER_ALREADY_PAID: 'ORDER_ALREADY_PAID',
INSUFFICIENT_STOCK: 'INSUFFICIENT_STOCK',
// 服务端错误
INTERNAL_ERROR: 'INTERNAL_ERROR',
DATABASE_ERROR: 'DATABASE_ERROR'
} as const
// 统一错误处理中间件
app.onError((err, c) => {
console.error('Unhandled error:', err)
// HTTP 异常
if (err instanceof HTTPException) {
const response: ErrorResponse = {
code: err.message,
message: getErrorMessage(err.status),
details: err.name
}
return c.json(response, err.status)
}
// 业务异常
if (err instanceof BusinessError) {
const response: ErrorResponse = {
code: err.code,
message: err.message,
details: err.details
}
return c.json(response, err.status)
}
// 未知异常
const response: ErrorResponse = {
code: 'INTERNAL_ERROR',
message: '服务内部错误',
details: process.env.NODE_ENV === 'development' ? err.message : undefined
}
return c.json(response, 500)
})
// 业务异常类
class BusinessError extends Error {
code: string
status: number
details?: unknown
constructor(code: string, message: string, status = 400, details?: unknown) {
super(message)
this.code = code
this.status = status
this.details = details
}
}
// 使用示例
app.get('/api/v1/users/:id', async (c) => {
const user = await findUser(c.req.param('id'))
if (!user) {
throw new BusinessError(
ErrorCodes.USER_NOT_FOUND,
'用户不存在',
404
)
}
return c.json({ data: user })
})3.5 版本与兼容
- API 版本策略明确(URL 路径
/v1/...、Header、Query) - 非破坏性变更优先(加字段 OK,删字段/改语义要三思)
- 必须破坏性变更时:
- 新版本并行运行一段时间
- 旧版本有废弃时间表(sunset header)
- 通知所有调用方
- 字段扩展兼容(调用方应该忽略不认识的字段)
3.6 幂等与重试
- GET/PUT/DELETE 幂等(多次调用结果相同)
- POST 非幂等时,提供幂等键(
Idempotency-Keyheader) - 重试策略文档化(哪些接口可以安全重试)
- 网络超时的处理明确(请求到底成功了没有?)
Hono 示例:幂等键
import { Hono } from 'hono'
const app = new Hono()
// 幂等键:防止重复提交
app.post('/api/v1/orders', async (c) => {
const idempotencyKey = c.req.header('Idempotency-Key')
if (!idempotencyKey) {
return c.json({
code: 'IDEMPOTENCY_KEY_REQUIRED',
message: '请提供 Idempotency-Key 请求头'
}, 400)
}
// 检查是否已处理过
const existing = await c.env.KV.get(`idempotency:${idempotencyKey}`, 'json')
if (existing) {
// 返回之前的结果
return c.json(existing.response, existing.status)
}
// 处理请求
const body = await c.req.json()
const order = await createOrder(body)
// 保存结果
await c.env.KV.put(
`idempotency:${idempotencyKey}`,
JSON.stringify({
response: order,
status: 201
}),
{ expirationTtl: 86400 } // 24 小时过期
)
return c.json(order, 201)
})3.7 安全
- 认证机制明确(JWT、API Key、OAuth)
- 敏感数据不在 URL 里(用 Authorization header)
- HTTPS 强制
- 输入校验到位(防 SQL 注入、XSS、命令注入)
- 限流策略明确(每用户每分钟多少次)
- 审计日志记录关键操作
3.8 性能与可扩展
- 响应时间目标明确(P50/P95/P99)
- 缓存策略明确(Cache-Control、ETag)
- 大数据量接口有分页(不能一次返回 10 万条)
- 列表接口支持过滤和排序
- 慢查询考虑异步化(返回 202,客户端轮询结果)
3.9 文档与示例
- OpenAPI/Swagger 文档完整
- 每个接口都有描述(做什么、什么时候用)
- 有请求和响应示例(curl 命令、代码片段)
- 错误码列表和含义
- 变更日志(changelog)记录每次改动
Hono 示例:OpenAPI 集成
import { Hono } from 'hono'
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const app = new OpenAPIHono()
// 定义路由和 schema
const route = createRoute({
method: 'get',
path: '/api/v1/users/:id',
request: {
params: z.object({
id: z.string().uuid()
})
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
data: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
})
})
}
},
description: '获取用户成功'
},
404: {
content: {
'application/json': {
schema: z.object({
code: z.string(),
message: z.string()
})
}
},
description: '用户不存在'
}
}
})
// 实现路由
app.openapi(route, async (c) => {
const { id } = c.req.valid('param')
const user = await findUser(id)
if (!user) {
return c.json({
code: 'USER_NOT_FOUND',
message: '用户不存在'
}, 404)
}
return c.json({ data: user }, 200)
})
// 自动生成 OpenAPI 文档
app.doc('/doc', {
openapi: '3.0.0',
info: {
title: 'API',
version: '1.0.0'
}
})4. 评审会上容易漏的点
- 分页:所有列表接口都要问「数据多了怎么办?」
- 过滤与排序:问「调用方需要怎么筛选和排序数据?」
- 幂等:问「网络超时了,客户端能安全重试吗?」
- 错误信息:问「客户端拿到这个错误,能自己处理吗?」
- 版本兼容:问「老版本客户端升级前会坏吗?」
5. 通过标准
- 接口契约清晰、完整、无歧义
- 命名一致、符合团队约定
- 错误处理完整(客户端能据此行动)
- 安全审查通过
- 性能估算合理
- 文档齐全
- 已通知所有调用方(如适用)
6. 反模式
以下是常见的设计反模式,评审时看到要打回:
- URL 里塞业务逻辑:
/users/searchAndExport?format=pdf - 返回字符串而非结构化数据:
{ "result": "success, id=123" } - 200 返回所有错误:业务错误也用 200 +
{ "error": "..." } - 一次返回全部数据:没有分页,列表接口返回 10 万条
- 静默失败:删除接口找不到资源也返回 200
- 不一致的命名:同一个概念,不同接口叫法不同
7. 维护建议
API 评审清单需要随团队实践更新:
- 新引入 API 规范(如 GraphQL、gRPC)时,补充对应检查项
- 出现 API 相关的事故后,把根因对应的检查项加进来
- 团队规模扩大时,强化版本兼容和文档要求
- 每半年复审一次,删除过时的检查项