Engineering Architecture

API 设计的艺术:从混乱到优雅的进化之路

RESTful 还是 GraphQL?版本控制怎么做?错误响应如何设计?分享我设计 50+ API 的实战经验,以及那些踩过的坑和学到的教训。

Ioodu · · Updated: Feb 10, 2026 · 22 min read
#API Design #REST #GraphQL #Backend #Architecture

好的 API 是什么样的

五年前,我设计的第一个 API 是这样的:

GET /getUserData?id=123
POST /saveNewUser
DELETE /deleteUserById

当时我觉得挺好的——功能实现了,前端能调用,测试通过了。

直到半年后,另一个团队需要集成我们的 API,他们发来的问题清单让我汗颜:

  • “这个接口返回的数据结构为什么不一致?”
  • “错误码为什么是 200 但 body 里有 error?”
  • “分页怎么实现?”
  • “这个字段是必填的吗?”

那一刻我意识到:写代码实现功能只是 20%,设计好 API 才是那 80%。

这篇文章分享我过去五年设计 50+ API 的经验教训,以及一套可落地的设计原则。

RESTful API 设计原则

1. 资源的命名

错误示范

GET /getUsers
POST /createUser
PUT /updateUser
DELETE /deleteUser

正确示范

GET    /users          # 获取用户列表
GET    /users/{id}     # 获取单个用户
POST   /users          # 创建用户
PUT    /users/{id}     # 全量更新用户
PATCH  /users/{id}     # 部分更新用户
DELETE /users/{id}     # 删除用户

原则

  • 使用名词(users, orders, products),不是动词
  • 使用复数形式
  • 使用层级结构表达关系:/users/{id}/orders

2. HTTP 方法的正确使用

方法幂等性用途示例
GET获取资源GET /users/123
POST创建资源POST /users
PUT全量更新PUT /users/123
PATCH部分更新PATCH /users/123
DELETE删除资源DELETE /users/123

常见误区

  • 用 POST 做所有操作(包括更新和删除)
  • 用 GET 做删除操作(浏览器预加载会误删!)
  • 用 PUT 做部分更新(应该用 PATCH)

3. 状态码的语义

错误示范

HTTP/1.1 200 OK
{
  "success": false,
  "error": "User not found"
}

正确示范

HTTP/1.1 404 Not Found
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User with id 123 does not exist",
    "details": {
      "userId": "123"
    }
  }
}

常用状态码

  • 200 OK - 成功
  • 201 Created - 创建成功
  • 204 No Content - 成功但无返回体(如 DELETE)
  • 400 Bad Request - 请求参数错误
  • 401 Unauthorized - 未认证
  • 403 Forbidden - 无权限
  • 404 Not Found - 资源不存在
  • 409 Conflict - 资源冲突(如重复创建)
  • 422 Unprocessable Entity - 业务逻辑错误
  • 429 Too Many Requests - 限流
  • 500 Internal Server Error - 服务器错误

请求与响应设计

1. 请求参数

路径参数 vs 查询参数 vs 请求体

# 路径参数:标识资源
GET /users/{userId}

# 查询参数:过滤、排序、分页
GET /users?role=admin&sort=-createdAt&page=2&limit=20

# 请求体:创建/更新资源
POST /users
{
  "name": "John Doe",
  "email": "john@example.com"
}

2. 响应格式

统一包装

{
  "data": {
    "id": "123",
    "name": "John Doe",
    "email": "john@example.com"
  },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-03-21T10:30:00Z"
  }
}

列表响应

{
  "data": [
    { "id": "1", "name": "User 1" },
    { "id": "2", "name": "User 2" }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": false
  }
}

3. 字段命名规范

使用 camelCase

{
  "userId": "123",
  "createdAt": "2026-03-21T10:30:00Z",
  "isActive": true
}

时间格式:使用 ISO 8601

{
  "createdAt": "2026-03-21T10:30:00Z",  // UTC
  "localTime": "2026-03-21T18:30:00+08:00"  // 带时区
}

错误处理设计

1. 错误响应结构

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Email format is invalid"
      },
      {
        "field": "age",
        "code": "OUT_OF_RANGE",
        "message": "Age must be between 18 and 100"
      }
    ],
    "requestId": "req_abc123",
    "timestamp": "2026-03-21T10:30:00Z",
    "documentation": "https://api.example.com/docs/errors/VALIDATION_ERROR"
  }
}

2. 错误码设计

格式{DOMAIN}_{ERROR_TYPE}

USER_NOT_FOUND
USER_ALREADY_EXISTS
ORDER_INSUFFICIENT_INVENTORY
AUTH_INVALID_TOKEN
AUTH_TOKEN_EXPIRED
RATE_LIMIT_EXCEEDED

文档化:每个错误码都要有对应的文档说明

3. 错误日志

后端日志

logger.error({
  requestId: 'req_abc123',
  userId: 'user_456',
  errorCode: 'VALIDATION_ERROR',
  errorMessage: 'Email format is invalid',
  stack: error.stack,
  path: '/api/users',
  method: 'POST',
  body: { name: 'John', email: 'invalid-email' },
  timestamp: '2026-03-21T10:30:00Z'
});

分页设计

1. Offset-based 分页

适用:数据量小、实时性要求不高

GET /users?page=2&limit=20

响应

{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 1000,
    "totalPages": 50
  }
}

缺点

  • 深度分页性能差(OFFSET 100000
  • 数据实时变化时可能重复或遗漏

2. Cursor-based 分页

适用:数据量大、实时性要求高(如消息流)

GET /messages?cursor=eyJpZCI6MTIzNDV9&limit=20

响应

{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6MTIzNjV9",
    "prevCursor": "eyJpZCI6MTIzMjV9",
    "hasNext": true,
    "hasPrev": true
  }
}

优点

  • 性能稳定
  • 数据变化不影响结果

版本控制策略

1. URL 路径版本

GET /v1/users
GET /v2/users

优点:清晰、易于缓存 缺点:URL 变更

2. Header 版本

GET /users
Accept: application/vnd.api+json;version=2

优点:URL 不变 缺点:不够直观、缓存复杂

3. 我的推荐

新项目:直接 URL 版本 /v1/ 成熟项目:Header 版本,逐步迁移

版本策略

  • 主版本号(v1, v2):破坏性变更
  • 次版本号(v1.1):新功能,向后兼容
  • 补丁版本:不体现在 API 中

认证与授权

1. JWT 最佳实践

Token 结构

// Access Token(短期,15分钟)
{
  "sub": "user_123",
  "iss": "api.example.com",
  "iat": 1711012200,
  "exp": 1711013100,
  "scope": ["read:users", "write:orders"],
  "jti": "token_unique_id"
}

// Refresh Token(长期,7天)
{
  "sub": "user_123",
  "type": "refresh",
  "exp": 1711617000,
  "jti": "refresh_token_id"
}

安全建议

  • Access Token 短期有效(15-60 分钟)
  • Refresh Token 轮换(每次刷新都生成新的)
  • 使用 HTTPS only
  • Token 存储在 httpOnly cookie 或 secure storage

2. OAuth 2.0 + PKCE

适用场景:第三方应用接入

# 1. 授权请求
GET /oauth/authorize?
  response_type=code&
  client_id=my_app&
  redirect_uri=https://myapp.com/callback&
  scope=read write&
  state=random_state&
  code_challenge=base64url(sha256(code_verifier))

# 2. 换取 Token
POST /oauth/token
{
  "grant_type": "authorization_code",
  "code": "auth_code",
  "redirect_uri": "https://myapp.com/callback",
  "client_id": "my_app",
  "code_verifier": "random_string"
}

API 文档

1. OpenAPI (Swagger) 规范

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserList'

2. 文档工具推荐

工具特点适用场景
Swagger UI可视化、可交互开发调试
Redoc美观、只读对外文档
Postman集合分享、Mock团队协作
Stoplight设计优先API 设计阶段

性能优化

1. 响应压缩

Accept-Encoding: gzip, deflate, br

建议:> 1KB 的响应都开启压缩

2. 缓存策略

# 客户端缓存
Cache-Control: max-age=3600, private

# CDN 缓存
Cache-Control: max-age=86400, public
ETag: "abc123"
Last-Modified: Mon, 21 Mar 2026 10:00:00 GMT

缓存层级

  1. 浏览器缓存(客户端)
  2. CDN 缓存(边缘节点)
  3. 应用缓存(Redis/Memcached)
  4. 数据库缓存(Query Cache)

3. 限流(Rate Limiting)

# 响应头
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1711015800

# 超限返回
429 Too Many Requests
Retry-After: 60

限流算法

  • 固定窗口:简单,但可能有 burst
  • 滑动窗口:更平滑
  • 令牌桶:允许一定 burst

GraphQL:REST 的替代方案

什么时候选 GraphQL

适合

  • 前端需求多变,经常需要不同字段组合
  • 移动端,需要精简响应
  • 聚合多个数据源

不适合

  • 简单 CRUD
  • 文件上传/下载
  • 团队缺乏 GraphQL 经验

GraphQL 设计原则

type User {
  id: ID!
  name: String!
  email: String!
  orders: [Order!]!
  createdAt: DateTime!
}

type Query {
  user(id: ID!): User
  users(
    filter: UserFilter
    sort: SortInput
    pagination: PaginationInput
  ): UserConnection!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

N+1 问题解决

DataLoader

const userLoader = new DataLoader(async (userIds) => {
  // 批量查询:SELECT * FROM users WHERE id IN (...)
  const users = await db.users.findMany({
    where: { id: { in: userIds } }
  });
  return userIds.map(id => users.find(u => u.id === id));
});

// 使用
const user = await userLoader.load(userId);

测试策略

1. 契约测试(Contract Testing)

Pact 示例

// Consumer 测试
const pact = new Pact({
  consumer: 'Frontend',
  provider: 'UserAPI'
});

await pact
  .interaction()
  .given('user exists')
  .uponReceiving('get user by id')
  .withRequest({
    method: 'GET',
    path: '/users/123'
  })
  .willRespondWith({
    status: 200,
    body: {
      id: '123',
      name: 'John Doe'
    }
  });

2. API 自动化测试

describe('Users API', () => {
  it('should create a user', async () => {
    const response = await request(app)
      .post('/api/v1/users')
      .send({
        name: 'John Doe',
        email: 'john@example.com'
      })
      .expect(201);

    expect(response.body.data).toMatchObject({
      name: 'John Doe',
      email: 'john@example.com'
    });
    expect(response.body.data.id).toBeDefined();
  });

  it('should return 400 for invalid email', async () => {
    await request(app)
      .post('/api/v1/users')
      .send({
        name: 'John',
        email: 'invalid-email'
      })
      .expect(400);
  });
});

监控与可观测性

1. 关键指标

黄金三指标

  • Latency:P50, P95, P99 响应时间
  • Error Rate:4xx, 5xx 错误率
  • Throughput:QPS/TPS

业务指标

  • 接口调用量
  • 用户活跃度
  • 错误分布

2. 分布式追踪

// OpenTelemetry 示例
const span = tracer.startSpan('process_order');
try {
  span.setAttribute('order.id', orderId);
  span.setAttribute('user.id', userId);

  await processOrder(orderId);

  span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
  span.recordException(error);
  span.setStatus({
    code: SpanStatusCode.ERROR,
    message: error.message
  });
  throw error;
} finally {
  span.end();
}

总结:API 设计 checklist

设计阶段

  • 资源命名符合 RESTful 规范
  • HTTP 方法和状态码使用正确
  • 错误响应结构统一
  • 分页策略合适
  • 认证授权机制明确
  • 版本策略确定

开发阶段

  • OpenAPI 文档完整
  • 输入验证完备
  • 错误处理全面
  • 单元测试覆盖
  • 集成测试通过
  • 性能测试达标

部署阶段

  • 监控告警配置
  • 日志收集配置
  • 限流熔断配置
  • 缓存策略生效
  • 文档站点部署

运维阶段

  • 错误率监控
  • 性能指标监控
  • 用户反馈收集
  • 版本兼容性检查
  • 定期安全审计

推荐资源

书籍

  • 《RESTful Web APIs》Leonard Richardson
  • 《API Design Patterns》JJ Geewax
  • 《GraphQL in Action》Samer Buna

工具

规范


你的 API 设计遇到过什么坑?欢迎在评论区分享。

本文总结了 5 年的 API 设计血泪史,希望能帮你少走一些弯路。

---

评论