API 设计与文档
问题
如何设计一套好的 API?如何管理 API 文档?幂等性是什么?
面试速答版
怎么设计一套好的 RESTful API? 核心是「资源 + 动作 + 状态码」三位一体:
- URL 用名词:
GET /users/:id、POST /users、DELETE /users/:id,不要写成POST /getUser或GET /deleteUser。 - HTTP 方法表语义:
GET查、POST增、PUT全量改、PATCH部分改、DELETE删。 - 统一响应:
{ code, data, message },错误带details字段级提示。 - 分页:列表数据用游标分页(
cursor + limit)比偏移量(page + pageSize)性能好得多——游标不需要COUNT(*),深分页也快。 - 版本管理:URL 路径
/api/v1/users最直观;只新增字段不改字段,破坏性变更上 v2。
幂等性是什么?POST 接口怎么做幂等? 幂等 = 同一个请求执行 1 次和 N 次效果相同:
- 天然幂等:
GET、PUT、DELETE;非幂等:POST、PATCH。 - POST 幂等做法:客户端生成
Idempotency-Key放请求头,服务端用 Redis 记录该 Key 的处理结果,重复请求直接返回缓存结果。 - 业务层幂等:用订单号、外部交易号做唯一索引或 SetNX。
- 重要场景:支付、下单、库存扣减必须幂等,否则网络抖动重试会重复扣款。
API 文档怎么管理?
- OpenAPI/Swagger:NestJS
@nestjs/swagger、zod-openapi,从代码注解自动生成文档,一份代码两份产物。 - Mock:基于 OpenAPI 自动生成 Mock 服务(
Apifox、MSW),前后端并行开发。 - 类型生成:用
openapi-typescript把文档转成前端 TS 类型,告别手写interface。
答案
RESTful API 设计规范
api-design.ts
// ✅ 好的 API 设计
// GET /api/users 获取用户列表
// GET /api/users/:id 获取单个用户
// POST /api/users 创建用户
// PUT /api/users/:id 全量更新用户
// PATCH /api/users/:id 部分更新用户
// DELETE /api/users/:id 删除用户
// ✅ 嵌套资源
// GET /api/users/:id/orders 获取用户的订单列表
// POST /api/users/:id/orders 为用户创建订单
// ❌ 不好的设计
// POST /api/getUser 用 POST 做查询
// GET /api/deleteUser/123 用 GET 做删除
// POST /api/user/update 动词放在 URL 中
统一响应格式
response-format.ts
// 成功响应
interface ApiResponse<T> {
code: number; // 业务状态码
data: T;
message: string;
}
// 列表响应(分页)
interface PaginatedResponse<T> {
code: number;
data: {
items: T[];
total: number;
page: number;
pageSize: number;
};
message: string;
}
// 错误响应
interface ApiError {
code: number;
message: string;
details?: Record<string, string[]>; // 字段级错误
}
// 示例
// 成功: { code: 0, data: { id: 1, name: "John" }, message: "ok" }
// 错误: { code: 40001, message: "用户名已存在", details: { username: ["已被注册"] } }
幂等性
| HTTP 方法 | 幂等 | 安全 | 说明 |
|---|---|---|---|
| GET | ✅ | ✅ | 多次请求结果相同 |
| PUT | ✅ | ❌ | 全量替换,结果一致 |
| DELETE | ✅ | ❌ | 删除一次和多次效果相同 |
| POST | ❌ | ❌ | 每次可能创建新资源 |
| PATCH | ❌ | ❌ | 取决于实现 |
idempotency.ts
// 实现 POST 幂等:幂等 Key
const createOrder = async (req: Request, res: Response) => {
const idempotencyKey = req.headers['idempotency-key'] as string;
if (!idempotencyKey) {
return res.status(400).json({ message: '需要 Idempotency-Key 头' });
}
// 检查是否已处理
const existing = await redis.get(`idempotent:${idempotencyKey}`);
if (existing) {
return res.json(JSON.parse(existing)); // 返回缓存结果
}
// 处理请求
const order = await db.order.create(req.body);
await redis.set(`idempotent:${idempotencyKey}`, JSON.stringify(order), 'EX', 86400);
return res.json(order);
};
OpenAPI / Swagger 文档
swagger-nestjs.ts
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('用户')
@Controller('users')
export class UserController {
@Get(':id')
@ApiOperation({ summary: '获取用户详情' })
@ApiResponse({ status: 200, description: '成功', type: UserDto })
@ApiResponse({ status: 404, description: '用户不存在' })
async getUser(@Param('id') id: string): Promise<UserDto> {
return this.userService.findById(id);
}
}
常见面试问题
Q1: API 版本管理怎么做?
答案:
三种方式:
- URL 路径:
/api/v1/users(最常用) - 请求头:
Accept: application/vnd.api+json; version=1 - 查询参数:
/api/users?version=1
Q2: RESTful API 的 PUT 和 PATCH 有什么区别?
答案:
PUT:全量替换,需要传完整对象PATCH:部分更新,只传需要修改的字段
Q3: 如何设计分页 API?
答案:
// 偏移量分页(适合页码跳转)
// GET /api/users?page=2&pageSize=20
// 游标分页(适合无限滚动,性能更好)
// GET /api/users?cursor=abc123&limit=20
游标分页不需要 COUNT(*),深分页性能远优于偏移量分页。
Q4: 接口鉴权放在哪一层?
答案:
通常在 API 网关或中间件层统一处理,业务代码不需要关心认证。鉴权(权限检查)可以在中间件或装饰器中实现。
Q5: 如何保证 API 的向后兼容?
答案:
- 只新增字段,不删除或修改已有字段
- 新增必填字段需提供默认值
- 破坏性变更通过新版本 API(v2)发布
- 旧版本设定废弃时间(Deprecation)
相关链接
- RESTful API 设计 - API 设计原则
- GraphQL - GraphQL 方案