RESTAPI设计 rest-api-design

RESTful API设计指南,包含资源命名、HTTP方法、状态码、版本控制、文档化等最佳实践,适用于创建和优化REST API。

后端开发 0 次安装 0 次浏览 更新于 3/4/2026

REST API 设计

概览

设计直观、一致且遵循行业最佳实践的资源导向架构的REST API。

何时使用

  • 设计新的RESTful APIs
  • 创建端点结构
  • 定义请求/响应格式
  • 实施API版本控制
  • 文档化API规范
  • 重构现有APIs

指令

1. 资源命名

✅ 好的资源名称(名词,复数)
GET    /api/users
GET    /api/users/123
GET    /api/users/123/orders
POST   /api/products
DELETE /api/products/456

❌ 坏的资源名称(动词,不一致)
GET    /api/getUsers
POST   /api/createProduct
GET    /api/user/123  (不一致的单数/复数)

2. HTTP方法和操作

# CRUD操作
GET    /api/users          # 列出所有用户(读取集合)
GET    /api/users/123      # 获取特定用户(读取单个)
POST   /api/users          # 创建新用户(创建)
PUT    /api/users/123      # 完全替换用户(更新)
PATCH  /api/users/123      # 部分更新用户(部分更新)
DELETE /api/users/123      # 删除用户(删除)

# 嵌套资源
GET    /api/users/123/orders       # 获取用户的订单
POST   /api/users/123/orders       # 为用户创建订单
GET    /api/users/123/orders/456   # 获取特定订单

3. 请求示例

创建资源

POST /api/users
Content-Type: application/json

{
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "role": "admin"
}

响应:201 创建
位置:/api/users/789
{
  "id": "789",
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "role": "admin",
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}

更新资源

PATCH /api/users/789
Content-Type: application/json

{
  "firstName": "Jonathan"
}

响应:200 OK
{
  "id": "789",
  "email": "john@example.com",
  "firstName": "Jonathan",
  "lastName": "Doe",
  "role": "admin",
  "updatedAt": "2025-01-15T11:00:00Z"
}

4. 查询参数

# 过滤
GET /api/products?category=electronics&inStock=true

# 排序
GET /api/users?sort=lastName,asc

# 分页
GET /api/users?page=2&limit=20

# 字段选择
GET /api/users?fields=id,email,firstName

# 搜索
GET /api/products?q=laptop

# 组合多个过滤器
GET /api/orders?status=pending&customer=123&sort=createdAt,desc&limit=50

5. 响应格式

成功响应

{
  "data": {
    "id": "123",
    "email": "user@example.com",
    "firstName": "John"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "version": "1.0"
  }
}

带分页的集合响应

{
  "data": [
    { "id": "1", "name": "产品1" },
    { "id": "2", "name": "产品2" }
  ],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 145,
    "totalPages": 8,
    "hasNext": true,
    "hasPrev": true
  },
  "links": {
    "self": "/api/products?page=2&limit=20",
    "first": "/api/products?page=1&limit=20",
    "prev": "/api/products?page=1&limit=20",
    "next": "/api/products?page=3&limit=20",
    "last": "/api/products?page=8&limit=20"
  }
}

错误响应

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "输入数据无效",
    "details": [
      {
        "field": "email",
        "message": "电子邮件格式无效"
      },
      {
        "field": "age",
        "message": "必须至少18岁"
      }
    ]
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "requestId": "abc-123-def"
  }
}

6. HTTP状态码

成功:
200 OK              - 成功的GET,PATCH,DELETE
201 Created         - 成功的POST(资源创建)
204 No Content      - 成功的DELETE(无响应体)

客户端错误:
400 Bad Request     - 请求格式/数据无效
401 Unauthorized    - 缺少或无效的身份验证
403 Forbidden       - 已认证但无权限
404 Not Found       - 资源不存在
409 Conflict        - 资源冲突(例如,重复的电子邮件)
422 Unprocessable   - 验证错误
429 Too Many Requests - 速率限制超出

服务器错误:
500 Internal Server Error - 通用服务器错误
503 Service Unavailable   - 临时不可用

7. API版本控制

# URL路径版本控制(推荐)
GET /api/v1/users
GET /api/v2/users

# 头部版本控制
GET /api/users
Accept: application/vnd.myapi.v1+json

# 查询参数(不推荐)
GET /api/users?version=1

8. 认证与安全

# JWT Bearer Token
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# API密钥
GET /api/users
X-API-Key: your-api-key-here

# 生产中始终使用HTTPS
https://api.example.com/v1/users

9. 速率限制头部

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995
X-RateLimit-Reset: 1642262400

10. OpenAPI文档

openapi: 3.0.0
info:
  title: 用户API
  version: 1.0.0
  description: 用户管理API

paths:
  /users:
    get:
      summary: 列出所有用户
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: 成功响应
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'

    post:
      summary: 创建新用户
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInput'
      responses:
        '201':
          description: 用户已创建
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: 输入无效
        '409':
          description: 电子邮件已存在

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
        createdAt:
          type: string
          format: date-time

    UserInput:
      type: object
      required:
        - email
        - firstName
        - lastName
      properties:
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string

最佳实践

✅ 要做

  • 使用名词作为资源,不要使用动词
  • 对集合使用复数名称
  • 保持命名约定的一致性
  • 返回适当的HTTP状态码
  • 为集合包含分页
  • 提供过滤和排序选项
  • 版本控制您的API
  • 使用OpenAPI进行详细文档化
  • 使用HTTPS
  • 实施速率限制
  • 提供清晰的错误消息
  • 使用ISO 8601日期格式

❌ 不要做

  • 在端点名称中使用动词
  • 返回200错误
  • 无必要地暴露内部ID
  • 过度嵌套资源(最多2级)
  • 使用不一致的命名
  • 忘记认证
  • 返回敏感数据
  • 在没有版本控制的情况下破坏向后兼容性

完整示例:Express.js

const express = require('express');
const app = express();

app.use(express.json());

// 带分页列出用户
app.get('/api/v1/users', async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const offset = (page - 1) * limit;

    const users = await User.findAndCountAll({
      limit,
      offset,
      attributes: ['id', 'email', 'firstName', 'lastName']
    });

    res.json({
      data: users.rows,
      pagination: {
        page,
        limit,
        total: users.count,
        totalPages: Math.ceil(users.count / limit)
      }
    });
  } catch (error) {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: '获取用户时发生错误'
      }
    });
  }
});

// 获取单个用户
app.get('/api/v1/users/:id', async (req, res) => {
  try {
    const user = await User.findByPk(req.params.id);

    if (!user) {
      return res.status(404).json({
        error: {
          code: 'NOT_FOUND',
          message: '用户未找到'
        }
      });
    }

    res.json({ data: user });
  } catch (error) {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: '发生错误'
      }
    });
  }
});

// 创建用户
app.post('/api/v1/users', async (req, res) => {
  try {
    const { email, firstName, lastName } = req.body;

    // 验证
    if (!email || !firstName || !lastName) {
      return res.status(400).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: '缺少必填字段',
          details: [
            !email && { field: 'email', message: '电子邮件是必填项' },
            !firstName && { field: 'firstName', message: '名字是必填项' },
            !lastName && { field: 'lastName', message: '姓氏是必填项' }
          ].filter(Boolean)
        }
      });
    }

    const user = await User.create({ email, firstName, lastName });

    res.status(201)
       .location(`/api/v1/users/${user.id}`)
       .json({ data: user });
  } catch (error) {
    if (error.name === 'SequelizeUniqueConstraintError') {
      return res.status(409).json({
        error: {
          code: 'CONFLICT',
          message: '电子邮件已存在'
        }
      });
    }
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: '发生错误'
      }
    });
  }
});

app.listen(3000);