RESTful API 设计

REST(Representational State Transfer)是一种架构风格,用于设计网络应用程序的 API。遵循 REST 原则设计的 API 被称为 RESTful API。它利用 HTTP 协议的特性,提供简洁、可扩展、易于理解的接口。本章将深入讲解 RESTful API 的核心概念、设计规范和实践,并通过一个 Express 示例展示如何构建符合 REST 风格的 API。

1. 什么是 RESTful API?

REST 是由 Roy Fielding 在其博士论文中提出的一种架构风格,它不是标准,而是一组约束和原则。如果一个 API 符合 REST 的约束条件,则可称为 RESTful API。其核心思想是:

  • 将应用程序的功能抽象为资源(Resource),每个资源由 URI 标识。
  • 通过统一的接口(HTTP 方法)对资源进行操作。
  • 资源可以有多种表现形式(如 JSON、XML),客户端通过请求头协商。
  • 服务器不保存客户端状态(无状态通信)。

2. REST 设计原则

  • 客户端-服务器分离:关注点分离,客户端不关心数据存储,服务器不关心用户界面。
  • 无状态(Stateless):每个请求必须包含所有必要信息,服务器不存储任何客户端上下文。会话状态保存在客户端。
  • 可缓存(Cacheable):响应必须隐式或显式地标记为可缓存或不可缓存,以提高性能。
  • 统一接口(Uniform Interface):通过 HTTP 方法(GET、POST、PUT、DELETE 等)对资源进行操作,使用标准的 HTTP 状态码表示结果。
  • 分层系统(Layered System):客户端通常不知道是否直接连接终端服务器,中间层(如负载均衡、缓存)可增强系统可扩展性。
  • 按需代码(Code on Demand,可选):服务器可以临时传输可执行代码给客户端,如 Java Applet 或 JavaScript。

3. HTTP 方法与 CRUD 对应

RESTful API 使用标准的 HTTP 方法对资源进行操作,对应关系如下:

HTTP 方法 CRUD 操作 作用范围 示例
GET Read(读取) 单个资源或集合 GET /users – 获取用户列表;GET /users/1 – 获取用户1
POST Create(创建) 通常作用于集合 POST /users – 创建新用户
PUT Update/Replace(全量更新) 单个资源 PUT /users/1 – 替换用户1的全部信息
PATCH Partial Update(部分更新) 单个资源 PATCH /users/1 – 更新用户1的部分字段
DELETE Delete(删除) 单个资源或集合 DELETE /users/1 – 删除用户1

注意:PUT 是幂等的,多次调用结果相同;POST 非幂等,每次调用可能创建新资源。PATCH 可以幂等也可以不幂等,取决于实现。

4. URL 设计规范

  • 使用名词而非动词:资源用名词复数表示。例如 /users 而不是 /getUsers
  • 资源嵌套表示层级关系:例如 /users/1/posts 表示用户1的所有文章,/posts/2/comments 表示文章2的评论。
  • 使用查询参数过滤、排序、分页:例如 GET /users?role=admin&page=2&limit=20&sort=createdAt_desc
  • 避免过深的嵌套:通常不超过三级。如果太深,可考虑展平或使用查询参数。
  • URL 中不要包含动作:动作应由 HTTP 方法表达。例外:某些情况下如 /users/1/activate 可以使用动词,但更符合 REST 的做法是使用 PATCH /users/1 传递 { "status": "active" }

5. HTTP 状态码的使用

使用合适的 HTTP 状态码可以让客户端更容易理解请求结果。常见分类:

  • 2xx 成功
    • 200 OK:GET、PUT、PATCH 成功。
    • 201 Created:POST 创建成功,通常在响应头中返回 Location 指向新资源。
    • 204 No Content:DELETE 成功,或 PUT/PATCH 成功但无需返回内容。
  • 4xx 客户端错误
    • 400 Bad Request:请求参数错误(如缺少必要字段、格式错误)。
    • 401 Unauthorized:未提供认证信息或认证失败。
    • 403 Forbidden:认证成功但无权限访问该资源。
    • 404 Not Found:资源不存在。
    • 405 Method Not Allowed:请求方法不允许。
    • 409 Conflict:请求与服务器当前状态冲突(如资源已存在)。
    • 422 Unprocessable Entity:请求语义错误(如验证失败)。
  • 5xx 服务器错误
    • 500 Internal Server Error:服务器内部错误。
    • 502 Bad Gateway503 Service Unavailable 等。

6. 请求与响应格式

通常使用 JSON 作为数据交换格式。建议统一响应结构,便于客户端处理。

例如,成功时返回:

{
  "data": { ... },
  "message": "操作成功"
}

失败时返回:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "邮箱格式不正确"
  }
}

也可遵循 JSON:APIGraphQL 等规范,但简单项目中保持一致性即可。

7. 版本控制

随着 API 演化,可能需要引入版本控制。常见做法:

  • URL 路径中嵌入版本号:如 /api/v1/users/api/v2/users。简单直观,最常用。
  • 自定义请求头:如 Accept: application/vnd.myapi.v1+json
  • 子域名:如 v1.api.example.com

推荐使用 URL 路径方式,因为它更显式,易于缓存和调试。

8. 过滤、排序、分页

对于资源集合,通常需要支持这些功能。通过查询参数实现:

  • 过滤?field=value,例如 ?category=tech。复杂过滤可使用类似 ?filter[field]=value?field=value 均可。
  • 排序?sort=field?sort=-field(降序)。
  • 分页?page=2&limit=20?offset=20&limit=20。响应中应包含分页元数据,如总条数、当前页等。
// 示例:分页响应
{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 100,
    "pages": 5
  }
}

9. 身份验证与授权

RESTful API 通常是无状态的,因此不能依赖 cookie-session。常用认证方式:

  • HTTP Basic Auth:简单但不安全,需配合 HTTPS。
  • Token 认证(如 JWT):客户端在请求头中携带 Authorization: Bearer <token>。JWT 自包含用户信息,适合分布式系统。
  • OAuth2:适用于第三方授权,如“使用 GitHub 登录”。

授权通常在业务逻辑中实现,根据角色或权限限制对资源的访问。

10. API 文档

良好的文档是 API 可用性的关键。推荐使用 Swagger / OpenAPI 规范,自动生成交互式文档。也可以使用 apidocPostman 等工具。

11. 最佳实践总结

  • 使用 HTTPS。
  • 始终使用正确的 HTTP 方法和状态码。
  • 为资源提供一致的命名(复数名词)。
  • 支持内容协商(Accept 头)。
  • 提供清晰、有用的错误信息。
  • 实现合理的速率限制(Rate Limiting)。
  • 对请求和响应进行日志(敏感信息脱敏)。

12. 示例:构建一个简单的 REST API(Express)

下面是一个基于 Express 和内存数组的示例,展示 RESTful 风格的用户管理 API。

const express = require('express');
const app = express();
app.use(express.json());

let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

// 获取所有用户(支持过滤、分页)
app.get('/api/v1/users', (req, res) => {
  let result = users;
  // 过滤
  if (req.query.name) {
    result = result.filter(u => u.name.includes(req.query.name));
  }
  // 分页
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const start = (page - 1) * limit;
  const end = start + limit;
  const paginated = result.slice(start, end);
  res.json({
    data: paginated,
    pagination: {
      page,
      limit,
      total: result.length,
      pages: Math.ceil(result.length / limit)
    }
  });
});

// 获取单个用户
app.get('/api/v1/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: '用户不存在' });
  }
  res.json({ data: user });
});

// 创建用户
app.post('/api/v1/users', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: '姓名和邮箱必填' });
  }
  const newUser = {
    id: users.length + 1,
    name,
    email
  };
  users.push(newUser);
  res.status(201).location(`/api/v1/users/${newUser.id}`).json({ data: newUser });
});

// 全量更新用户
app.put('/api/v1/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: '用户不存在' });
  }
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: '姓名和邮箱必填' });
  }
  user.name = name;
  user.email = email;
  res.json({ data: user });
});

// 部分更新用户
app.patch('/api/v1/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: '用户不存在' });
  }
  if (req.body.name) user.name = req.body.name;
  if (req.body.email) user.email = req.body.email;
  res.json({ data: user });
});

// 删除用户
app.delete('/api/v1/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: '用户不存在' });
  }
  users.splice(index, 1);
  res.status(204).send();
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: '服务器内部错误' });
});

app.listen(3000, () => console.log('API running on http://localhost:3000'));

这个示例演示了如何设计资源端点、使用适当的状态码、支持过滤和分页,以及错误处理。

13. 安全性注意事项

  • 始终使用 HTTPS 加密传输。
  • 实现身份验证(如 JWT),并验证 token 有效性。
  • 对输入进行验证和清理,防止注入攻击。
  • 设置合理的 CORS 策略。
  • 限制请求频率(Rate Limiting)防止滥用。
  • 不在错误信息中暴露敏感细节(如堆栈跟踪)。
总结: RESTful API 设计不仅仅是 URL 和方法的选择,更是对资源、状态、统一接口的深入理解。遵循本章介绍的规范和最佳实践,可以设计出易于使用、易于维护的 API。在后续章节中,我们将学习如何对 API 进行测试、文档生成,以及使用 GraphQL 替代 REST 的探索。