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: 'alice@example.com', role: 'admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', 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 中:查询参数和路径可能被日志记录,敏感信息应放在请求头或体中。
业务场景
- 移动端后端 API:为 App 提供 RESTful 数据接口
- 微服务间通信:服务间通过 HTTP API 交互
- 第三方开放平台:对外暴露标准化 API
- 管理后台 API:为前端管理界面提供 CRUD 接口
扩展阅读
上一章:第 12 章 · Express 框架 下一章:第 14 章 · 数据库 — MySQL、PostgreSQL、MongoDB 和 ORM。