正在加载,请稍候…

REST API 设计最佳实践:URL 结构、版本控制与错误处理

学习顶级科技公司使用的 REST API 设计最佳实践,涵盖 URL 命名约定、HTTP 方法、版本控制策略、错误响应、认证和文档。

REST API 设计最佳实践:URL 结构、版本控制与错误处理

REST API 设计最佳实践:URL 结构、版本控制与错误处理

设计良好的 REST API 用起来令人愉悦,而设计糟糕的则成为维护噩梦。以下最佳实践源自 Stripe、GitHub、Twilio 等受开发者喜爱的 API 所使用的模式。

URL 设计原则

REST API 设计最佳实践:URL 结构、版本控制与错误处理 插图

使用名词,而非动词

❌ 错误 — URL 中包含动词
GET  /getUsers
POST /createUser
PUT  /updateUser/123
GET  /deleteUser/123

✅ 正确 — 使用名词,HTTP 方法作为动词
GET    /users           (列出用户)
POST   /users           (创建用户)
GET    /users/123       (获取用户 123)
PUT    /users/123       (替换用户 123)
PATCH  /users/123       (部分更新用户 123)
DELETE /users/123       (删除用户 123)

集合使用复数名词

/users        而不是 /user
/products     而不是 /product
/orders       而不是 /order
/categories   而不是 /category

嵌套资源表示关系

/users/123/orders           属于用户 123 的订单
/users/123/orders/456       用户 123 的订单 456
/posts/789/comments         帖子 789 的评论
/posts/789/comments/101     帖子 789 的评论 101

但保持嵌套浅层(最多 2-3 层):
❌ /users/123/orders/456/items/789/reviews    太深!
✅ /order-items/789/reviews                   展平它

使用查询参数进行过滤、排序、分页

GET /products?category=electronics&inStock=true
GET /users?sort=createdAt&order=desc
GET /orders?status=pending&page=2&limit=20
GET /products?minPrice=10&maxPrice=100&search=wireless

HTTP 方法 — 正确使用它们

方法 用途 幂等? 请求体?
GET 检索数据
POST 创建资源
PUT 替换资源(完整)
PATCH 更新资源(部分)
DELETE 删除资源 有时

PUT 与 PATCH 的区别

// PUT — 替换整个资源(必须发送所有字段)
PUT /users/123
{
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "role": "admin",
  "phone": "+1-555-0100"
}

// PATCH — 仅更新指定字段
PATCH /users/123
{
  "phone": "+1-555-9999"
}

API 版本控制

REST API 设计最佳实践:URL 结构、版本控制与错误处理 插图

选项 1:URL 路径(推荐用于公共 API)

GET /v1/users
GET /v2/users    (包含破坏性变更的新版本)
// Express 版本控制
app.use('/v1', v1Router);
app.use('/v2', v2Router);

// v1Router
v1Router.get('/users', getUsersV1);

// v2Router — v2 可能有不同的响应格式
v2Router.get('/users', getUsersV2);

选项 2:Accept 头部(更符合 REST 风格)

GET /users
Accept: application/vnd.myapi.v2+json

选项 3:自定义头部

GET /users
API-Version: 2

建议:URL 版本控制最明确,且最容易在浏览器中测试。公共 API 推荐使用。

响应格式一致性

// ✅ 单个资源
GET /users/123
{
  "data": {
    "id": "123",
    "type": "user",
    "attributes": {
      "name": "Alice Johnson",
      "email": "alice@example.com",
      "createdAt": "2026-01-15T10:30:00Z"
    }
  }
}

// ✅ 集合
GET /users?page=1&limit=20
{
  "data": [
    { "id": "123", "name": "Alice Johnson", "email": "alice@example.com" },
    { "id": "124", "name": "Bob Smith", "email": "bob@example.com" }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 847,
    "totalPages": 43
  },
  "links": {
    "self": "/users?page=1&limit=20",
    "next": "/users?page=2&limit=20",
    "last": "/users?page=43&limit=20"
  }
}

错误处理

一致的错误格式

// ✅ Stripe 风格的错误响应
{
  "error": {
    "type": "validation_error",
    "message": "Invalid request parameters",
    "code": "VALIDATION_FAILED",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "age",
        "message": "Must be at least 18",
        "code": "MIN_VALUE"
      }
    ],
    "requestId": "req_abc123"
  }
}

HTTP 状态码

// Express 错误处理示例
export function errorHandler(err, req, res, next) {
  // 4xx — 客户端错误
  if (err instanceof ValidationError) {
    return res.status(422).json({
      error: { type: 'validation_error', message: err.message, details: err.details }
    });
  }
  
  if (err instanceof NotFoundError) {
    return res.status(404).json({
      error: { type: 'not_found', message: err.message, code: 'RESOURCE_NOT_FOUND' }
    });
  }
  
  if (err instanceof UnauthorizedError) {
    return res.status(401).json({
      error: { type: 'authentication_error', message: 'Invalid or expired token' }
    });
  }
  
  if (err instanceof ForbiddenError) {
    return res.status(403).json({
      error: { type: 'authorization_error', message: 'Insufficient permissions' }
    });
  }
  
  if (err.code === '23505') { // PostgreSQL 唯一约束冲突
    return res.status(409).json({
      error: { type: 'conflict', message: 'Resource already exists' }
    });
  }
  
  // 5xx — 服务器错误
  console.error('Unhandled error:', err);
  res.status(500).json({
    error: {
      type: 'server_error',
      message: 'An unexpected error occurred',
      requestId: req.id, // 用于调试
    }
  });
}

REST API 设计最佳实践:URL 结构、版本控制与错误处理 插图

认证

Bearer Token (JWT) 模式

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// 在 Express 中:
const token = req.headers.authorization?.replace('Bearer ', '');

API Key 模式(用于服务间通信)

X-API-Key: sk_live_abc123...
// 或者作为查询参数(安全性较低,敏感操作避免使用):
GET /data?api_key=sk_live_abc123

过滤与搜索

// 按精确值过滤
GET /products?category=electronics

// 按范围过滤
GET /products?minPrice=10&maxPrice=500
GET /orders?startDate=2026-01-01&endDate=2026-03-31

// 全文搜索
GET /articles?q=javascript+async+await

// 多个值(逗号分隔或重复参数)
GET /products?category=electronics,clothing
GET /products?category=electronics&category=clothing

// 嵌套字段过滤
GET /users?address.city=SanFrancisco

使用 OpenAPI/Swagger 的 API 文档

# openapi.yaml
openapi: 3.0.3
info:
  title: My API
  version: 1.0.0

paths:
  /users:
    get:
      summary: 列出用户
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
      responses:
        '200':
          description: 用户列表
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'

常见 API 设计错误

错误 修正
GET /deleteUser/123 DELETE /users/123
错误时返回 200 使用正确的 4xx/5xx
没有分页 添加 page/limit 参数
字段名不一致(camelCase vs snake_case) 选择一种并坚持使用
没有版本控制 从一开始就添加 /v1/ 前缀
在响应中返回密码 永远不要包含敏感字段
本意是 PATCH 却用了 PUT 理解两者的区别

发布前检查清单

  • URL 使用名词而非动词
  • 整个 API 使用一致的 camelCase(或 snake_case)
  • 所有列表端点支持分页
  • 一致的错误响应格式
  • URL 中包含 API 版本(/v1/)
  • 认证方式已文档化
  • 响应中包含速率限制头部
  • 已生成 OpenAPI/Swagger 文档

→ 使用 URL 解析器 工具解析和检查 URL。