强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 13 章 · REST API 设计

第 13 章 · REST API 设计

13.1 REST 基本原则

REST(Representational State Transfer)是一种 API 设计风格,核心原则:

原则说明
资源导向每个 URL 代表一个资源
统一接口使用标准 HTTP 方法(GET/POST/PUT/DELETE)
无状态每个请求包含所有必要信息
分层系统客户端不感知中间层
可缓存响应应标记是否可缓存

HTTP 方法语义

方法语义幂等安全示例
GET获取资源GET /api/users/1
POST创建资源POST /api/users
PUT替换资源PUT /api/users/1
PATCH部分更新PATCH /api/users/1
DELETE删除资源DELETE /api/users/1

幂等:多次请求结果相同
安全:不会修改服务器资源

URL 设计规范

✅ 好的设计:
GET    /api/users              — 获取用户列表
GET    /api/users/123          — 获取单个用户
POST   /api/users              — 创建用户
PUT    /api/users/123          — 更新用户
DELETE /api/users/123          — 删除用户
GET    /api/users/123/posts    — 获取用户的帖子

❌ 不好的设计:
GET    /api/getUsers           — 不要用动词
POST   /api/createUser
POST   /api/users/delete/123
GET    /api/user-list

13.2 完整 CRUD 实现

// routes/users.js
const { Router } = require('express');
const router = Router();

// 模拟数据库
let users = [
  { id: 1, name: 'Alice', email: '[email protected]', role: 'admin' },
  { id: 2, name: 'Bob', email: '[email protected]', role: 'user' },
];
let nextId = 3;

// GET /api/users — 获取列表(支持分页、筛选、排序)
router.get('/', (req, res) => {
  let result = [...users];

  // 筛选
  const { role, search, sort, order = 'asc', page = 1, limit = 10 } = req.query;
  if (role) result = result.filter(u => u.role === role);
  if (search) {
    const q = search.toLowerCase();
    result = result.filter(u =>
      u.name.toLowerCase().includes(q) ||
      u.email.toLowerCase().includes(q)
    );
  }

  // 排序
  if (sort) {
    result.sort((a, b) => {
      const cmp = a[sort] < b[sort] ? -1 : a[sort] > b[sort] ? 1 : 0;
      return order === 'desc' ? -cmp : cmp;
    });
  }

  // 分页
  const total = result.length;
  const pageNum = Math.max(1, Number(page));
  const limitNum = Math.min(100, Math.max(1, Number(limit)));
  const start = (pageNum - 1) * limitNum;
  result = result.slice(start, start + limitNum);

  res.json({
    data: result,
    pagination: {
      page: pageNum,
      limit: limitNum,
      total,
      totalPages: Math.ceil(total / limitNum),
    },
  });
});

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

// POST /api/users — 创建资源
router.post('/', (req, res) => {
  const { name, email, role = 'user' } = req.body;

  // 验证
  const errors = [];
  if (!name || name.length < 2) errors.push('名称至少 2 个字符');
  if (!email || !email.includes('@')) errors.push('邮箱格式不正确');
  if (errors.length > 0) {
    return res.status(400).json({ errors });
  }

  // 检查重复
  if (users.some(u => u.email === email)) {
    return res.status(409).json({ error: '邮箱已存在' });
  }

  const user = { id: nextId++, name, email, role };
  users.push(user);

  res
    .status(201)
    .header('Location', `/api/users/${user.id}`)
    .json({ data: user });
});

// PUT /api/users/:id — 全量更新
router.put('/:id', (req, res) => {
  const index = users.findIndex(u => u.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: '用户不存在' });
  }

  const { name, email, role } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: '名称和邮箱为必填字段' });
  }

  users[index] = { ...users[index], name, email, role };
  res.json({ data: users[index] });
});

// PATCH /api/users/:id — 部分更新
router.patch('/:id', (req, res) => {
  const index = users.findIndex(u => u.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: '用户不存在' });
  }

  const allowedFields = ['name', 'email', 'role'];
  const updates = {};
  for (const key of allowedFields) {
    if (req.body[key] !== undefined) {
      updates[key] = req.body[key];
    }
  }

  users[index] = { ...users[index], ...updates };
  res.json({ data: users[index] });
});

// DELETE /api/users/:id — 删除资源
router.delete('/:id', (req, res) => {
  const index = users.findIndex(u => u.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: '用户不存在' });
  }

  users.splice(index, 1);
  res.status(204).send();
});

module.exports = router;

13.3 HTTP 状态码

状态码含义使用场景
200 OK请求成功GET、PUT、PATCH 成功
201 Created资源已创建POST 成功
204 No Content无响应体DELETE 成功
400 Bad Request请求格式错误参数验证失败
401 Unauthorized未认证缺少或无效的认证令牌
403 Forbidden无权限认证通过但权限不足
404 Not Found资源不存在URL 或资源 ID 不存在
409 Conflict资源冲突重复创建、版本冲突
422 Unprocessable语义错误验证失败
429 Too Many Requests请求过多触发速率限制
500 Internal Error服务器错误未捕获的异常

13.4 API 版本控制

// 方式 1:URL 路径版本(推荐)
app.use('/api/v1/users', require('./routes/v1/users'));
app.use('/api/v2/users', require('./routes/v2/users'));

// 方式 2:请求头版本
app.use((req, res, next) => {
  const version = req.headers['accept-version'] || 'v1';
  req.apiVersion = version;
  next();
});

// 方式 3:查询参数版本
// GET /api/users?version=1

13.5 统一响应格式

// 成功响应
{
  "success": true,
  "data": { ... },
  "meta": {
    "page": 1,
    "limit": 10,
    "total": 100
  }
}

// 错误响应
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}
// 统一响应辅助函数
function success(res, data, meta = {}, status = 200) {
  res.status(status).json({ success: true, data, meta });
}

function error(res, message, status = 500, code = 'INTERNAL_ERROR', details = []) {
  res.status(status).json({
    success: false,
    error: { code, message, details },
  });
}

// 使用
router.get('/', (req, res) => {
  const { data, pagination } = getUsers(req.query);
  success(res, data, { pagination });
});

router.post('/', (req, res) => {
  const { valid, errors } = validateUser(req.body);
  if (!valid) {
    return error(res, '参数验证失败', 422, 'VALIDATION_ERROR', errors);
  }
  const user = createUser(req.body);
  success(res, user, {}, 201);
});

13.6 分页实现

// 游标分页(推荐用于大数据集)
router.get('/api/users', async (req, res) => {
  const { cursor, limit = 20 } = req.query;
  const limitNum = Math.min(100, Number(limit));

  const query = cursor
    ? { id: { $gt: Number(cursor) } }
    : {};

  const users = await db.users.find(query).limit(limitNum).sort({ id: 1 });
  const nextCursor = users.length === limitNum ? users[users.length - 1].id : null;

  res.json({
    data: users,
    pagination: {
      nextCursor,
      hasMore: users.length === limitNum,
    },
  });
});

// 使用方式
// GET /api/users?limit=20           → 第一页
// GET /api/users?limit=20&cursor=42 → 下一页

13.7 请求验证

// 使用 Joi 或 Zod 进行验证
const { z } = require('zod');

const UserSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['user', 'admin']).default('user'),
});

// 验证中间件
function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        errors: result.error.issues.map(i => ({
          field: i.path.join('.'),
          message: i.message,
        })),
      });
    }
    req.body = result.data;
    next();
  };
}

router.post('/api/users', validate(UserSchema), (req, res) => {
  // req.body 已验证和清理
  res.json({ created: req.body });
});

注意事项

⚠️ GET 请求不应有副作用:GET 请求应该只读取数据,不修改服务器状态。

⚠️ POST 不幂等,PUT 幂等:多次 POST 创建多条记录,多次 PUT 更新同一条记录。

⚠️ 返回适当的状态码:不要所有响应都返回 200,使用准确的 HTTP 状态码。

⚠️ 敏感数据不要放在 URL 中:查询参数和路径可能被日志记录,敏感信息应放在请求头或体中。

业务场景

  1. 移动端后端 API:为 App 提供 RESTful 数据接口
  2. 微服务间通信:服务间通过 HTTP API 交互
  3. 第三方开放平台:对外暴露标准化 API
  4. 管理后台 API:为前端管理界面提供 CRUD 接口

扩展阅读


上一章第 12 章 · Express 框架 下一章第 14 章 · 数据库 — MySQL、PostgreSQL、MongoDB 和 ORM。