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/offsetlimittotalnext_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 不可用
  • 错误响应结构统一(&#123; code, message, details &#125;
  • 错误码机器可读(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-Key header)
  • 重试策略文档化(哪些接口可以安全重试)
  • 网络超时的处理明确(请求到底成功了没有?)

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. 评审会上容易漏的点

  1. 分页:所有列表接口都要问「数据多了怎么办?」
  2. 过滤与排序:问「调用方需要怎么筛选和排序数据?」
  3. 幂等:问「网络超时了,客户端能安全重试吗?」
  4. 错误信息:问「客户端拿到这个错误,能自己处理吗?」
  5. 版本兼容:问「老版本客户端升级前会坏吗?」

5. 通过标准

  • 接口契约清晰、完整、无歧义
  • 命名一致、符合团队约定
  • 错误处理完整(客户端能据此行动)
  • 安全审查通过
  • 性能估算合理
  • 文档齐全
  • 已通知所有调用方(如适用)

6. 反模式

以下是常见的设计反模式,评审时看到要打回:

  • URL 里塞业务逻辑/users/searchAndExport?format=pdf
  • 返回字符串而非结构化数据&#123; "result": "success, id=123" &#125;
  • 200 返回所有错误:业务错误也用 200 + &#123; "error": "..." &#125;
  • 一次返回全部数据:没有分页,列表接口返回 10 万条
  • 静默失败:删除接口找不到资源也返回 200
  • 不一致的命名:同一个概念,不同接口叫法不同

7. 维护建议

API 评审清单需要随团队实践更新:

  • 新引入 API 规范(如 GraphQL、gRPC)时,补充对应检查项
  • 出现 API 相关的事故后,把根因对应的检查项加进来
  • 团队规模扩大时,强化版本兼容和文档要求
  • 每半年复审一次,删除过时的检查项