Development
API 和接口设计
指导稳定的 API 和接口设计。在设计 API、模块边界或任何公共接口时使用。
设计稳定、文档完善的接口,使其难以被误用。
当一个 API 有足够多的用户时,你系统的所有可观测行为都会被某人依赖。
每一种公共行为——包括未记录的怪癖——一旦被用户依赖,就成为了事实上的契约。
为一个同时只存在一个版本的世界设计——扩展而非分叉。
先定义接口再实现:
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>;
}
interface APIError {
error: {
code: string; // 机器可读:"VALIDATION_ERROR"
message: string; // 人类可读:"Email is required"
details?: unknown;
};
}
// 状态码映射
// 400 → 客户端发送了无效数据
// 401 → 未认证
// 403 → 已认证但未授权
// 404 → 资源未找到
// 422 → 验证失败
// 500 → 服务器错误(绝不暴露内部细节)
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);
});
// 好:添加可选字段
interface CreateTaskInput {
title: string;
description?: string;
priority?: 'low' | 'medium' | 'high'; // 后来添加的,可选
}
// 差:更改现有字段类型
interface CreateTaskInput {
title: string;
priority: number; // 从 string 改为 number —— 破坏消费者
}
| 模式 | 约定 | 示例 |
|------|------|------|
| REST 端点 | 复数名词,无动词 | GET /api/tasks |
| 查询参数 | camelCase | ?sortBy=createdAt |
| 响应字段 | camelCase | { createdAt, updatedAt } |
| 布尔字段 | is/has/can 前缀 | isComplete |
| 枚举值 | UPPER_SNAKE | "IN_PROGRESS" |
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 /api/tasks/123
{ "title": "Updated title" }
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}`;
}
}
type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };
function getTask(id: TaskId): Promise<Task> { ... }
设计 API 后: