好的 API 应该像一位优秀的管家:** predictable(可预测)、helpful(有帮助)、unobtrusive(不碍事)**。它让使用者能够直觉地猜到如何操作,减少认知负担,同时提供足够的灵活性应对复杂场景。
从本质上说,API(Application Programming Interface)是系统之间沟通的契约。这份契约的质量直接决定了:
| 原则 | 含义 | 实践要点 |
|---|---|---|
| 一致性 | 相似的操作用相似的方式表达 | 统一的命名规范、参数风格、返回格式 |
| 正交性 | 功能之间不重叠,组合产生能力 | 避免冗余参数,每个参数有明确职责 |
| 最小惊讶 | 行为符合直觉,不制造意外 | 遵循业界惯例,文档清晰说明边界情况 |
| 可扩展 | 能优雅地增加新功能 | 版本控制、向后兼容设计 |
| 安全默认 | 默认配置是安全的 | 权限校验、限流、防注入 |
REST(Representational State Transfer)的核心思想是将一切抽象为资源(Resource),通过标准的 HTTP 方法对资源进行操作。
✅ GET /users # 用户列表(复数名词)
✅ GET /users/{id} # 单个用户
✅ GET /users/{id}/orders # 用户的订单(嵌套资源)
✅ POST /users # 创建用户
✅ PUT /users/{id} # 全量更新
✅ PATCH /users/{id} # 部分更新
✅ DELETE /users/{id} # 删除用户
❌ GET /getUsers # 动词开头,不符合 REST 风格
❌ GET /user/list # 混合风格
❌ POST /users/create # 冗余动词
users 而非 useruser-profiles 而非 UserProfilesorder-items 而非 orderItems 或 order_itemsAccept 头协商格式,而非 /users.json/users/{id}/orders/{orderId}/items 已是极限| 方法 | 幂等性 | 安全性 | 用途 | 响应码 |
|---|---|---|---|---|
| GET | 是 | 是 | 获取资源 | 200 OK, 404 Not Found |
| POST | 否 | 否 | 创建资源 | 201 Created, 400 Bad Request |
| PUT | 是 | 否 | 全量更新 | 200 OK, 204 No Content |
| PATCH | 否 | 否 | 部分更新 | 200 OK, 409 Conflict |
| DELETE | 是 | 否 | 删除资源 | 204 No Content, 404 Not Found |
幂等性(Idempotency):多次执行产生相同结果。如 DELETE /users/123 执行一次和十次,最终都是用户 123 不存在。
安全性(Safety):不修改服务器状态。GET 是安全的,POST/PUT/PATCH/DELETE 不是。
200 OK - 请求成功,返回资源
201 Created - 创建成功,Location 头指向新资源
202 Accepted - 已接受请求,异步处理中
204 No Content - 成功但无返回体(如 DELETE)
301 Moved Permanently - 资源永久迁移,更新书签
302 Found - 临时重定向(HTTP/1.0 遗留)
303 See Other - 重定向到 GET(POST 后常用)
304 Not Modified - 缓存有效,使用本地副本
307 Temporary Redirect - 临时重定向,保持方法
308 Permanent Redirect - 永久重定向,保持方法
400 Bad Request - 请求格式错误,无法解析
401 Unauthorized - 未认证(需登录)
403 Forbidden - 已认证但无权限
404 Not Found - 资源不存在
409 Conflict - 资源冲突(如重复创建)
422 Unprocessable Entity - 语义错误(如验证失败)
429 Too Many Requests - 请求过于频繁,限流触发
500 Internal Server Error - 服务端内部错误
502 Bad Gateway - 网关/代理收到无效响应
503 Service Unavailable - 服务暂时不可用(维护或过载)
504 Gateway Timeout - 上游服务超时
| 场景 | 方式 | 示例 |
|---|---|---|
| 资源定位 | 路径参数 | /users/{id} |
| 过滤/搜索 | 查询参数 | /users?role=admin&status=active |
| 请求体数据 | Body | POST/PUT/PATCH 的 JSON |
| 元信息 | Header | Authorization, Accept, Content-Type |
{
"data": {
"id": "12345",
"name": "张三",
"email": "zhangsan@example.com",
"createdAt": "2024-01-15T08:30:00Z"
},
"meta": {
"requestId": "req-abc-123",
"timestamp": "2024-01-15T08:30:01Z"
}
}
错误响应统一格式:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"details": {
"userId": "12345"
},
"help": "https://docs.example.com/errors/USER_NOT_FOUND"
}
}
GET /users?offset=20&limit=10
响应:
{
"data": [...],
"pagination": {
"offset": 20,
"limit": 10,
"total": 1000,
"hasMore": true
}
}
优点:简单直观,支持跳页
缺点:大数据量时性能差,数据变动导致重复/遗漏
GET /users?cursor=eyJpZCI6MTAwfQ==&limit=10
响应:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTEwfQ==",
"prevCursor": "eyJpZCI6OTB9",
"hasMore": true
}
}
优点:性能稳定,适合实时数据流
缺点:不支持跳页,游标不透明
{
"userName": "张三", // ❌ 驼峰(Java 风格,不推荐)
"user_name": "张三", // ✅ 蛇形(Python/Ruby 风格,推荐)
"user-name": "张三" // ❌ 连字符(JSON 键不常用)
}
推荐:使用 snake_case(蛇形命名),这是 JSON API 规范(jsonapi.org)和大多数 Web 框架的默认约定。
{
"created_at": "2024-01-15T08:30:00Z", // ✅ ISO 8601 UTC
"created_at": "2024-01-15 08:30:00", // ❌ 无时区信息
"created_at": 1705312200 // ❌ Unix 时间戳(可读性差)
}
{
"middle_name": null, // ✅ 明确字段存在但值为空
"middle_name": "", // ❌ 空字符串语义不清
// ❌ 省略字段(调用方不确定是 null 还是未返回)
}
{
"amount": 100.50, // ❌ 浮点数表示金额(精度问题)
"amount": "100.50", // ✅ 金额用字符串(或分整数)
"count": 100, // ✅ 整数计数
"id": "123456789012345" // ✅ 大整数用字符串(JavaScript 精度限制)
}
GET /users/123
Accept: application/json # 请求 JSON
Accept: application/xml # 请求 XML(如需要)
Accept: application/vnd.api+json # JSON API 规范格式
响应:
Content-Type: application/json
| 方式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URL 路径 | /v1/users, /v2/users |
直观,易于缓存 | URL 混乱,破坏 REST 资源唯一性 |
| 请求头 | Accept: application/vnd.api.v1+json |
资源 URL 不变 | 不够直观,调试困难 |
| 查询参数 | /users?version=1 |
简单 | 不规范,易被忽略 |
| Host 分离 | api-v1.example.com |
完全隔离 | 运维成本高 |
推荐:URL 路径版本(/v1/)+ 请求头协商(Accept),兼顾直观性和灵活性。
阶段 1:新增字段(向后兼容)
GET /v1/users/123
{ "id": "123", "name": "张三", "email": "zs@example.com" }
→ 新增 phone 字段
{ "id": "123", "name": "张三", "email": "zs@example.com", "phone": null }
阶段 2:字段变更(需新版本)
GET /v2/users/123
{ "id": "123", "full_name": "张三", "contact": { "email": "...", "phone": "..." } }
阶段 3:废弃旧版本
GET /v1/users/123
响应头: Sunset: Sat, 01 Jun 2024 00:00:00 GMT
响应体: { "warning": "v1 API 将于 2024-06-01 废弃" }
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| API Key | 服务端调用、内部服务 | 简单 | 安全性低,无法细粒度授权 |
| JWT (Bearer) | 移动端、SPA | 无状态,可携带声明 | Token 过大,无法主动失效 |
| OAuth 2.0 | 第三方接入 | 标准授权流程 | 实现复杂 |
| mTLS | 高安全内部服务 | 双向认证 | 证书管理成本高 |
请求头:
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Token 结构:
{
"header": { "alg": "RS256", "typ": "JWT" },
"payload": {
"sub": "user-123", // 主题(用户ID)
"iss": "auth.example.com", // 签发者
"aud": "api.example.com", // 接收者
"iat": 1705312200, // 签发时间
"exp": 1705398600, // 过期时间(建议 15-60 分钟)
"scope": "read:users write:orders" // 权限范围
}
}
安全建议:
基于角色的访问控制(RBAC):
角色: admin, editor, viewer
权限: users:read, users:write, orders:delete
基于属性的访问控制(ABAC):
用户属性: department=finance, level=manager
资源属性: owner=user-123, sensitivity=high
环境属性: time=09:00-18:00, ip=10.0.0.0/8
# 多层防御策略
# 1. 格式校验(JSON Schema)
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 }
},
"required": ["email"]
}
# 2. 业务校验
if not user_exists(email):
raise ValidationError("用户不存在")
# 3. 防注入
# 使用参数化查询,绝不拼接 SQL
# ORM: User.objects.filter(email=email) ✅
# 拼接: f"SELECT * FROM users WHERE email='{email}'" ❌
策略 1:固定窗口
限制: 1000 请求/小时
问题: 窗口边界突发(59 分 1000 请求,00 分又 1000 请求)
策略 2:滑动窗口
限制: 1000 请求/小时
实现: 记录每个请求时间,统计最近 1 小时
优点: 平滑,无边界问题
缺点: 存储成本高
策略 3:令牌桶
容量: 1000 令牌
速率: 10 令牌/秒
突发: 最多 1000 请求
优点: 允许突发,长期平滑
响应头告知限流状态:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1705398600
Retry-After: 3600
Access-Control-Allow-Origin: https://app.example.com # 明确域名,非 *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true # 允许 Cookie
Access-Control-Max-Age: 86400 # 预检缓存 1 天
Strict-Transport-Security: max-age=31536000; includeSubDomains # HSTS
X-Content-Type-Options: nosniff # 禁止 MIME 嗅探
X-Frame-Options: DENY # 禁止嵌入 iframe
Content-Security-Policy: default-src 'self' # CSP
客户端缓存:
Cache-Control: max-age=3600, public # 1 小时公共缓存
Cache-Control: max-age=0, must-revalidate # 每次验证
ETag: "abc123" # 资源版本标识
Last-Modified: Wed, 15 Jan 2024 08:30:00 GMT # 修改时间
条件请求:
If-None-Match: "abc123" → 304 Not Modified(未变化)
If-Modified-Since: ... → 304 Not Modified
请求:Accept-Encoding: gzip, deflate, br
响应:Content-Encoding: gzip
压缩级别权衡:
gzip -1 (最快,压缩率低) → 实时性要求高
gzip -6 (默认,平衡) → 通用场景
gzip -9 (最慢,压缩率高) → 静态资源
❌ 100 次请求:
POST /orders/1/cancel
POST /orders/2/cancel
...
✅ 1 次批量请求:
POST /batch
{
"operations": [
{ "method": "POST", "path": "/orders/1/cancel" },
{ "method": "POST", "path": "/orders/2/cancel" }
]
}
GET /users/123?fields=id,name,email # 只返回指定字段
GET /users/123?expand=department,manager # 展开关联资源
openapi: 3.0.0
info:
title: 用户服务 API
version: 1.0.0
paths:
/users/{id}:
get:
summary: 获取用户信息
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: 用户不存在
# 好的 SDK 设计
client = ApiClient(api_key="your-key", timeout=30)
user = client.users.get("123") # 简洁直观
try:
user.update(name="新名字") # 异常处理清晰
except UserNotFoundError as e:
logger.error(f"用户不存在: {e.user_id}")
# 避免的设计
client.make_request("GET", "/users/123") # 太底层,暴露细节
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 请求量 (QPS) | 每秒请求数 | 基线 ±20% |
| 延迟 (Latency) | P50/P95/P99 响应时间 | P99 > 500ms |
| 错误率 | 5xx 比例 | > 0.1% |
| 饱和度 | CPU/内存/连接池使用率 | > 80% |
请求头传递 Trace ID:
X-Request-ID: req-abc-123
X-Trace-ID: trace-xyz-789
X-Span-ID: span-001
日志格式:
[2024-01-15 08:30:01] [trace-xyz-789] [span-001] INFO userservice - 用户查询成功: user_id=123
GET /health
{
"status": "healthy", // healthy / degraded / unhealthy
"version": "1.2.3",
"checks": {
"database": { "status": "pass", "responseTime": "5ms" },
"cache": { "status": "pass", "responseTime": "2ms" },
"external-api": { "status": "warn", "responseTime": "500ms" }
}
}
| 反模式 | 问题 | 正确做法 |
|---|---|---|
| 隧道化 POST | 所有操作都用 POST,丧失 HTTP 语义 | 正确使用 GET/POST/PUT/DELETE |
| 200 包裹错误 | HTTP 200 但 body 里放 error | 使用正确的 HTTP 状态码 |
| 超级端点 | 一个接口做太多事 | 遵循单一职责,拆分端点 |
| 返回 HTML | API 返回 HTML 错误页 | 始终返回 JSON/XML |
| 无版本控制 | 直接修改现有接口 | 版本化,渐进式废弃 |
| 暴露内部 ID | 返回数据库自增 ID | 使用 UUID 或业务编码 |
❌ 问题:offset=999990, limit=10
数据库需要扫描 100 万行才能返回 10 条
✅ 解决:
1. 限制最大 offset(如 10000)
2. 大数据量强制游标分页
3. 提供搜索/筛选替代全量遍历
❌ 问题:
GET /users → 返回 100 个用户 ID
然后调用 100 次 GET /users/{id}/orders
✅ 解决:
1. 批量接口:GET /users?ids=1,2,3...&expand=orders
2. GraphQL:客户端指定需要的关联数据
3. 后端优化:JOIN 查询或缓存预热
# 客户端精确指定需要的字段
query {
user(id: "123") {
name
email
orders(first: 5) {
total
items {
product { name price }
quantity
}
}
}
}
适用场景:移动端(减少数据传输)、复杂关联查询、快速迭代的前端
不适用:简单 CRUD、文件上传、需要强缓存的场景
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User);
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
适用场景:内部微服务通信、高性能要求、多语言环境
不适用:浏览器直接调用(需 gRPC-Web 转换)、需要人类可读调试
| 场景 | 推荐方案 |
|---|---|
| 对外公开 API | REST + OpenAPI |
| 移动端/前端 | REST 或 GraphQL |
| 内部微服务 | gRPC 或 REST |
| 实时数据流 | WebSocket / SSE |
| 文件传输 | 专用下载接口,非 JSON |
# 使用 Pact 进行消费者驱动契约测试
# 消费者端测试
pact.given("用户存在").upon_receiving("查询用户").with_request(
method="GET",
path="/users/123"
).will_respond_with(
status=200,
body={"id": "123", "name": "张三"}
)
# 生成的契约文件供提供方验证
模拟故障场景:
- 数据库延迟增加 500ms
- 外部服务返回 503
- 网络丢包 10%
- CPU 满载
验证:
- 降级策略是否生效
- 超时设置是否合理
- 重试是否导致雪崩
核心原则:API 是产品,不是实现细节。好的 API 设计需要站在调用方视角,提供一致、可靠、易用的接口,同时保证系统的安全性和可维护性。