API 设计——为什么你的接口总是被吐槽 / API Design for Clear and Evolvable Interfaces
📅 创建时间:2026-05-08 🏷️ 标签:#RESTful #GraphQL #OpenAPI #接口设计 #版本管理 #API文档 📚 前置知识:[[00-backend-overview]] [[07-auth-security]](认证鉴权) 📚 相关知识:[[18-observability]](接口日志)
场景:前端工程师和产品经理同时找你吵架
┌─────────────────────────────────────────────────────────────┐
│ │
│ 前端:我需要按状态筛选订单,你返回的字段不够。 │
│ 后端:加一个参数不就行了? │
│ 前端:但你返回的结构变了,我得改很多地方... │
│ │
│ 产品经理:iOS 和 Android 的接口不一样? │
│ 后端:嗯,历史原因... │
│ 产品经理:维护两套接口很累的,能统一吗? │
│ │
│ 新人:我不知道这个接口怎么用,有文档吗? │
│ 后端:Swagger 有...可能没更新... │
│ │
└─────────────────────────────────────────────────────────────┘好的 API 设计 = 好的用户体验 + 低维护成本。
第1节:RESTful 规范——不只是把 URL 改成名词
常见的错误 RESTful
❌ 错误示范:
POST /api/addUser # 用了动词
POST /api/user/add # 资源命名不一致
GET /api/getUser?id=1 # 动词在 URL 里
GET /api/user/1/orders # 用嵌套表示关系
POST /api/deleteOrder # 动作在 URL 里
✅ 正确示范:
POST /api/users # 创建用户
GET /api/users # 获取用户列表
GET /api/users/1 # 获取单个用户
PUT /api/users/1 # 更新用户(完整替换)
PATCH /api/users/1 # 更新用户(部分更新)
DELETE /api/users/1 # 删除用户
GET /api/users/1/orders # 获取用户的订单HTTP 方法的语义
┌─────────────────────────────────────────────────────────────┐
│ HTTP 方法语义 │
├─────────────────────────────────────────────────────────────┤
│ │
│ GET(查询): │
│ → 幂等,多次执行结果相同 │
│ → 不修改数据 │
│ → 可缓存 │
│ │
│ POST(创建): │
│ → 非幂等,执行多次产生多个资源 │
│ → 返回 201 Created + Location header │
│ │
│ PUT(完整替换): │
│ → 幂等,多次执行结果相同 │
│ → 发送完整资源,即使只改一个字段 │
│ → 缺失字段可能被视为空 │
│ │
│ PATCH(部分更新): │
│ → 非幂等(通常) │
│ → 只发送需要修改的字段 │
│ → 推荐用 JSON Merge Patch 或 JSON Patch │
│ │
│ DELETE(删除): │
│ → 幂等,多次删除结果相同(第二次返回 404) │
│ → 返回 204 No Content │
│ │
└─────────────────────────────────────────────────────────────┘RESTful 响应状态码
┌─────────────────────────────────────────────────────────────┐
│ 常用 HTTP 状态码 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 2xx 成功: │
│ 200 OK - GET/PUT/PATCH 成功 │
│ 201 Created - POST 创建成功(带 Location) │
│ 204 No Content - DELETE 成功(无返回体) │
│ │
│ 4xx 客户端错误: │
│ 400 Bad Request - 参数错误、校验失败 │
│ 401 Unauthorized - 未认证(没登录) │
│ 403 Forbidden - 无权限(登录了但没权限) │
│ 404 Not Found - 资源不存在 │
│ 409 Conflict - 资源冲突(如重复创建) │
│ 422 Unprocessable Entity - 业务校验失败 │
│ 429 Too Many Requests - 限流 │
│ │
│ 5xx 服务端错误: │
│ 500 Internal Server Error - 服务器异常 │
│ 502 Bad Gateway - 网关错误 │
│ 503 Service Unavailable - 服务不可用 │
│ 504 Gateway Timeout - 网关超时 │
│ │
└─────────────────────────────────────────────────────────────┘第2节:RESTful API 实战设计
用户订单 API 设计
yaml
# OpenAPI 3.0 规范
openapi: 3.0.0
info:
title: 订单服务 API
version: 1.0.0
description: 订单创建、查询、取消
paths:
/users/{userId}/orders:
get:
summary: 获取用户的订单列表
parameters:
- name: userId
in: path
required: true
schema:
type: integer
- name: status
in: query
description: 订单状态筛选
schema:
type: string
enum: [pending, paid, shipped, completed, cancelled]
- name: page
in: query
schema:
type: integer
default: 1
- name: pageSize
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Order'
pagination:
$ref: '#/components/schemas/Pagination'
post:
summary: 创建订单
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- items
- addressId
properties:
items:
type: array
items:
type: object
properties:
productId:
type: integer
quantity:
type: integer
addressId:
type: integer
responses:
'201':
description: 创建成功
headers:
Location:
schema:
type: string
description: 新订单的 URL
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
$ref: '#/components/responses/BadRequest'
'422':
$ref: '#/components/responses/Unprocessable'
/orders/{orderId}:
get:
summary: 获取订单详情
parameters:
- name: orderId
in: path
required: true
schema:
type: integer
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/OrderDetail'
'404':
$ref: '#/components/responses/NotFound'
patch:
summary: 更新订单(部分更新)
parameters:
- name: orderId
in: path
required: true
schema:
type: integer
requestBody:
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [cancelled]
cancelReason:
type: string
responses:
'200':
description: 更新成功
'409':
description: 状态冲突,无法取消已完成的订单
components:
schemas:
Order:
type: object
properties:
id:
type: integer
userId:
type: integer
status:
type: string
totalAmount:
type: number
format: decimal
createdAt:
type: string
format: date-time
OrderDetail:
allOf:
- $ref: '#/components/schemas/Order'
- type: object
properties:
items:
type: array
items:
type: object
properties:
productId:
type: integer
productName:
type: string
quantity:
type: integer
price:
type: number
address:
$ref: '#/components/schemas/Address'
Pagination:
type: object
properties:
page:
type: integer
pageSize:
type: integer
total:
type: integer
totalPages:
type: integer第3节:GraphQL vs RESTful——什么时候该用 GraphQL
问题:移动端需要不同字段
┌─────────────────────────────────────────────────────────────┐
│ │
│ 场景:iOS 需要订单列表(字段少) │
│ Android 需要订单详情(字段多) │
│ Web 需要额外字段(物流信息) │
│ │
│ REST 方案: │
│ ❌ 接口爆炸:/orders(列表) /orders/detail /orders/lite│
│ ❌ 字段冗余:列表也返回所有字段 │
│ ✅ 简单直接,易理解 │
│ │
│ GraphQL 方案: │
│ ✅ 单个端点,客户端按需取字段 │
│ ✅ 一次请求获取多资源 │
│ ❌ 学习成本,复杂度高 │
│ │
└─────────────────────────────────────────────────────────────┘GraphQL 示例
graphql
# 单个端点:POST /graphql
# 客户端请求(iOS:只需要基础字段)
query {
orders(status: "pending") {
id
totalAmount
createdAt
}
}
# 响应
{
"data": {
"orders": [
{"id": 1, "totalAmount": 99.99, "createdAt": "2026-05-08"},
{"id": 2, "totalAmount": 199.99, "createdAt": "2026-05-08"}
]
}
}
# 客户端请求(Android:需要更多字段)
query {
orders {
id
totalAmount
items {
productName
quantity
}
address {
city
detail
}
}
}REST vs GraphQL 选择指南
┌─────────────────────────────────────────────────────────────┐
│ REST vs GraphQL 选择 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 选 REST: │
│ ✅ API 简单,资源明确 │
│ ✅ 团队熟悉 HTTP + JSON │
│ ✅ 需要 HTTP 缓存(CDN / 浏览器缓存) │
│ ✅ 需要清晰的接口文档(Swagger) │
│ ✅ 移动端性能要求高(减少数据传输) │
│ │
│ 选 GraphQL: │
│ ✅ 多端需求差异大(iOS/Android/Web 字段不同) │
│ ✅ 需要一次请求获取多个相关资源 │
│ ✅ 前端团队希望自主迭代,不用等后端加字段 │
│ ✅ 数据关系复杂,需要深度查询 │
│ │
│ 实际建议: │
│ → 大部分场景 RESTful 足够 │
│ → 只有在多端差异化需求严重时才考虑 GraphQL │
│ │
└─────────────────────────────────────────────────────────────┘第4节:API 版本管理——如何平滑升级
三种版本策略
┌─────────────────────────────────────────────────────────────┐
│ API 版本管理策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方案 1:URL 路径版本(最常用) │
│ GET /api/v1/users │
│ GET /api/v2/users ← 新版本 │
│ ✅ 显式清晰 │
│ ❌ 需要维护多套接口 │
│ │
│ 方案 2:Header 版本(推荐) │
│ GET /api/users │
│ API-Version: 2024-01-01 │
│ ✅ URL 干净 │
│ ❌ 不直观,需要看 Header 才能知道版本 │
│ │
│ 方案 3:Query 参数版本(简单项目) │
│ GET /api/users?version=2 │
│ ✅ 最简单 │
│ ❌ 容易忘记传版本参数 │
│ │
└─────────────────────────────────────────────────────────────┘平滑升级策略
python
# 方案:双版本运行,逐步迁移
@app.route('/api/users', methods=['GET'])
def get_users():
# 读取请求的版本
api_version = request.headers.get('API-Version', 'v1')
if api_version.startswith('v2') or api_version >= '2024-01-01':
return get_users_v2()
else:
return get_users_v1()
def get_users_v1():
"""旧版本:返回扁平结构"""
users = User.query.all()
return jsonify([{
'id': u.id,
'name': u.name,
'email': u.email
}])
def get_users_v2():
"""新版本:返回嵌套结构 + 新字段"""
users = User.query.all()
return jsonify([{
'id': u.id,
'profile': {
'name': u.name,
'email': u.email,
'avatar': u.avatar_url
},
'metadata': {
'createdAt': u.created_at,
'role': u.role
}
}])
# 监控各版本的使用比例
@app.after_request
def track_version(response):
api_version = request.headers.get('API-Version', 'v1')
metrics.increment(f'api_version.{api_version}')
return response第5节:接口设计最佳实践
统一响应格式
python
# 统一响应格式
class Response:
@staticmethod
def success(data=None, message="操作成功", code=0):
return jsonify({
"code": code,
"message": message,
"data": data,
"timestamp": int(time.time())
})
@staticmethod
def error(message, code=1, errors=None):
return jsonify({
"code": code,
"message": message,
"errors": errors,
"timestamp": int(time.time())
}), Response.http_status_for(code)
@staticmethod
def http_status_for(code):
if code == 0:
return 200
elif code in [1, 10]: # 通用错误、业务错误
return 400
elif code == 2: # 未认证
return 401
elif code == 3: # 无权限
return 403
elif code == 4: # 未找到
return 404
elif code == 5: # 限流
return 429
else:
return 500
# 分页响应
class PaginatedResponse:
@staticmethod
def success(items, total, page, page_size):
return Response.success({
"items": items,
"pagination": {
"total": total,
"page": page,
"pageSize": page_size,
"totalPages": (total + page_size - 1) // page_size
}
})接口安全 Checklist
┌─────────────────────────────────────────────────────────────┐
│ 接口安全 Checklist │
├─────────────────────────────────────────────────────────────┤
│ │
│ 认证授权: │
│ ✅ 所有接口需要认证(除登录/注册) │
│ ✅ 敏感操作需要二次验证 │
│ ✅ 权限校验(用户只能操作自己的资源) │
│ │
│ 输入校验: │
│ ✅ 参数类型校验(int/string/array) │
│ ✅ 参数范围校验(pageSize <= 100) │
│ ✅ 字符串长度校验 │
│ ✅ SQL / XSS 注入防护 │
│ │
│ 敏感数据: │
│ ✅ 脱敏展示(手机号 138****5678) │
│ ✅ 日志不记录敏感信息 │
│ ✅ 接口返回最小必要字段 │
│ │
│ 限流熔断: │
│ ✅ 接口限流([[05-middleware]]) │
│ ✅ 慢接口熔断(超过 5 秒返回错误) │
│ │
└─────────────────────────────────────────────────────────────┘升华:好 API 的标准
┌─────────────────────────────────────────────────────────────┐
│ 好 API 的标准 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 可见即可得 │
│ → 文档和代码一致 │
│ → Swagger/OpenAPI 实时生成文档 │
│ │
│ 2. 自描述 │
│ → 字段命名清晰 │
│ → 错误信息明确 │
│ → 状态码语义正确 │
│ │
│ 3. 稳定演进 │
│ → 版本管理清晰 │
│ → 向后兼容 │
│ → 渐进迁移 │
│ │
│ 4. 高效沟通 │
│ → 接口变更提前通知 │
│ → 提供 SDK / 示例代码 │
│ │
│ 一句话: │
│ API 是产品,不是实现细节。要像设计产品一样设计 API。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ OpenAPI 规范的完整语法
✅ GraphQL Schema 的详细定义
✅ 各语言的分页实现方式
必须理解:
🔴 RESTful 的 HTTP 方法语义(GET/POST/PUT/PATCH/DELETE)
🔴 PUT vs PATCH 的区别
🔴 GraphQL 适用场景(不是所有场景都适合)
🔴 API 版本管理策略的选择
🔴 统一响应格式的重要性学习状态:🟡 开始学习