RESTful API 设计规范与最佳实践:从理论到实战
摘要
RESTful API已成为现代Web服务的标准架构风格。本文将从资源命名、HTTP方法、状态码、版本控制、安全等方面,结合Node.js+Express实战,带你掌握RESTful API的设计精髓。
1. 引言
你是否遇到过这样的API:/getUser?id=1、/deleteUser?id=2、/createUser?这种RPC风格的接口不仅混乱,而且难以维护。RESTful API通过资源导向和标准HTTP方法,提供了统一、可预测的接口设计。本文不仅会列出规范,还会通过一个简单的任务管理系统,手把手教你实现RESTful API。
2. 核心原则
2.1 资源导向
URL代表资源(名词),而非操作(动词)。例如:
- 错误:
/getUser、/createUser - 正确:
/users、/users/123
2.2 使用标准HTTP方法
| 方法 | 操作 | 示例 | |——|——|——| | GET | 获取资源 | GET /users、GET /users/123 | | POST | 创建资源 | POST /users | | PUT | 完全替换资源 | PUT /users/123 | | PATCH | 部分更新资源 | PATCH /users/123 | | DELETE | 删除资源 | DELETE /users/123 |
2.3 状态码表达结果
- 2xx 成功:200 OK, 201 Created, 204 No Content
- 3xx 重定向:301 Moved Permanently, 304 Not Modified
- 4xx 客户端错误:400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity
- 5xx 服务器错误:500 Internal Server Error, 502 Bad Gateway
3. 实战:构建任务管理API
我们将使用Node.js + Express + 内存存储来演示。
3.1 项目初始化
mkdir task-api && cd task-api
npm init -y
npm install express
3.2 基础服务器
// server.js
const express = require('express');
const app = express();
app.use(express.json());
let tasks = [];
let idCounter = 1;
app.listen(3000, () => console.log('API running on port 3000'));
3.3 资源命名与端点设计
| 端点 | 方法 | 描述 | |——|——|——| | /tasks | GET | 获取所有任务 | | /tasks | POST | 创建新任务 | | /tasks/:id | GET | 获取单个任务 | | /tasks/:id | PUT | 完全替换任务 | | /tasks/:id | PATCH | 部分更新任务 | | /tasks/:id | DELETE | 删除任务 |
3.4 实现CRUD
// 获取所有任务
app.get('/tasks', (req, res) => {
res.json(tasks);
});
// 创建任务
app.post('/tasks', (req, res) => {
const { title, completed } = req.body;
if (!title || typeof title !== 'string') {
return res.status(400).json({ error: 'Title is required and must be a string' });
}
const task = {
id: idCounter++,
title,
completed: completed || false,
createdAt: new Date().toISOString()
};
tasks.push(task);
res.status(201).json(task);
});
// 获取单个任务
app.get('/tasks/:id', (req, res) => {
const id = parseInt(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
// 完全替换任务
app.put('/tasks/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return res.status(404).json({ error: 'Task not found' });
const { title, completed } = req.body;
if (!title || typeof title !== 'string') {
return res.status(400).json({ error: 'Title is required' });
}
tasks[index] = {
id,
title,
completed: !!completed,
createdAt: tasks[index].createdAt,
updatedAt: new Date().toISOString()
};
res.json(tasks[index]);
});
// 部分更新任务
app.patch('/tasks/:id', (req, res) => {
const id = parseInt(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) return res.status(404).json({ error: 'Task not found' });
const { title, completed } = req.body;
if (title !== undefined) {
if (typeof title !== 'string') return res.status(400).json({ error: 'Title must be a string' });
task.title = title;
}
if (completed !== undefined) {
if (typeof completed !== 'boolean') return res.status(400).json({ error: 'Completed must be a boolean' });
task.completed = completed;
}
task.updatedAt = new Date().toISOString();
res.json(task);
});
// 删除任务
app.delete('/tasks/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return res.status(404).json({ error: 'Task not found' });
tasks.splice(index, 1);
res.status(204).send();
});
💡 注意:PUT应替换整个资源,因此需要提供所有必填字段;PATCH只更新提供的字段。
3.5 测试API
使用curl测试:
# 创建任务
curl -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"学习REST"}'
# 获取所有任务
curl http://localhost:3000/tasks
# 获取单个任务
curl http://localhost:3000/tasks/1
# 部分更新
curl -X PATCH http://localhost:3000/tasks/1 -H "Content-Type: application/json" -d '{"completed":true}'
# 删除任务
curl -X DELETE http://localhost:3000/tasks/1
4. 高级实践
4.1 过滤、排序与分页
使用查询参数:
// GET /tasks?completed=true&sort=-createdAt&page=1&limit=10
app.get('/tasks', (req, res) => {
let result = [...tasks];
// 过滤
if (req.query.completed !== undefined) {
const completed = req.query.completed === 'true';
result = result.filter(t => t.completed === completed);
}
// 排序
if (req.query.sort) {
const fields = req.query.sort.split(',');
fields.forEach(field => {
const order = field.startsWith('-') ? -1 : 1;
const key = field.replace(/^-/, '');
result.sort((a, b) => (a[key] > b[key] ? order : -order));
});
}
// 分页
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const start = (page - 1) * limit;
const paginated = result.slice(start, start + limit);
res.json({
data: paginated,
total: result.length,
page,
limit
});
});
4.2 版本控制
推荐使用URL路径版本:
app.use('/v1', v1Router);
app.use('/v2', v2Router);
或者请求头版本(更优雅但不易发现):
app.use((req, res, next) => {
const version = req.headers['accept-version'];
req.apiVersion = version || '1';
next();
});
4.3 错误处理
统一错误响应格式:
// 全局错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
});
});
4.4 认证与授权
使用JWT示例:
const jwt = require('jsonwebtoken');
// 中间件验证
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(403).json({ error: 'Invalid token' });
}
}
app.get('/tasks', authenticate, (req, res) => {
// 只返回当前用户的任务
const userTasks = tasks.filter(t => t.userId === req.user.id);
res.json(userTasks);
});
4.5 幂等性
PUT和DELETE应该是幂等的。例如,多次DELETE同一个资源应返回相同结果(404)。
5. 常见陷阱
- 使用动词:不要用
/getTasks,用GET /tasks - 忽略状态码:总是返回适当的状态码,不要所有请求都返回200
- 嵌套过深:避免
/users/1/tasks/2/comments,考虑扁平化或使用查询参数 - 忽略HATEOAS:虽然不是强制,但返回相关链接可以提高API的可发现性
6. 总结
本文从理论到实战,带你构建了符合RESTful原则的任务管理API。关键点:
- 资源命名使用复数名词
- 利用HTTP方法表达操作
- 正确使用状态码
- 实现过滤、排序、分页
- 版本控制与安全
下一步,你可以考虑添加HATEOAS链接,或者使用OpenAPI规范文档化API。