Development
API 和接口设计
指导稳定的 API 和接口设计。在设计 API、模块边界或任何公共接口时使用。
设计稳定、文档完善的接口,使其难以被误用。
何时使用
- 设计新的 API 端点
- 定义团队之间的模块边界或契约
- 创建组件 prop 接口
- 建立影响 API 形状的数据库模式
- 修改现有的公共接口
核心原则
Hyrum 定律
当一个 API 有足够多的用户时,你系统的所有可观测行为都会被某人依赖。
每一种公共行为——包括未记录的怪癖——一旦被用户依赖,就成为了事实上的契约。
单版本规则
为一个同时只存在一个版本的世界设计——扩展而非分叉。
1. 契约优先
先定义接口再实现:
interface TaskAPI {
createTask(input: CreateTaskInput): Promise<Task>;
listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;
getTask(id: string): Promise<Task>;
updateTask(id: string, input: UpdateTaskInput): Promise<Task>;
deleteTask(id: string): Promise<void>;
}
2. 一致的错误语义
interface APIError {
error: {
code: string; // 机器可读:"VALIDATION_ERROR"
message: string; // 人类可读:"Email is required"
details?: unknown;
};
}
// 状态码映射
// 400 → 客户端发送了无效数据
// 401 → 未认证
// 403 → 已认证但未授权
// 404 → 资源未找到
// 422 → 验证失败
// 500 → 服务器错误(绝不暴露内部细节)
3. 在边界处验证
app.post('/api/tasks', async (req, res) => {
const result = CreateTaskSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid task data',
details: result.error.flatten(),
},
});
}
const task = await taskService.create(result.data);
return res.status(201).json(task);
});
4. 优先添加而非修改
// 好:添加可选字段
interface CreateTaskInput {
title: string;
description?: string;
priority?: 'low' | 'medium' | 'high'; // 后来添加的,可选
}
// 差:更改现有字段类型
interface CreateTaskInput {
title: string;
priority: number; // 从 string 改为 number —— 破坏消费者
}
5. 可预测的命名
| 模式 | 约定 | 示例 |
|---|---|---|
| REST 端点 | 复数名词,无动词 | GET /api/tasks |
| 查询参数 | camelCase | ?sortBy=createdAt |
| 响应字段 | camelCase | { createdAt, updatedAt } |
| 布尔字段 | is/has/can 前缀 | isComplete |
| 枚举值 | UPPER_SNAKE | "IN_PROGRESS" |
REST API 模式
资源设计
GET /api/tasks → 列出任务
POST /api/tasks → 创建任务
GET /api/tasks/:id → 获取单个任务
PATCH /api/tasks/:id → 更新任务(部分更新)
DELETE /api/tasks/:id → 删除任务
分页
// 请求
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc
// 响应
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"totalItems": 142,
"totalPages": 8
}
}
部分更新(PATCH)
// 只更改标题,其他保持不变
PATCH /api/tasks/123
{ "title": "Updated title" }
TypeScript 接口模式
可辨识联合(Discriminated Unions)
type TaskStatus =
| { type: 'pending' }
| { type: 'in_progress'; assignee: string; startedAt: Date }
| { type: 'completed'; completedAt: Date; completedBy: string }
| { type: 'cancelled'; reason: string; cancelledAt: Date };
function getStatusLabel(status: TaskStatus): string {
switch (status.type) {
case 'pending': return 'Pending';
case 'in_progress': return `In progress (${status.assignee})`;
case 'completed': return `Done on ${status.completedAt}`;
case 'cancelled': return `Cancelled: ${status.reason}`;
}
}
品牌类型(Branded Types)
type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };
function getTask(id: TaskId): Promise<Task> { ... }
验证
设计 API 后:
- 每个端点都有类型化的输入和输出模式
- 错误响应遵循单一一致的格式
- 验证仅在系统边界处进行
- 列表端点支持分页
- 新字段是增量且可选的(向后兼容)
- 命名在所有端点间遵循一致的约定