先说说我自己的惨痛经历。刚开始接触RESTful API设计时,觉得不就是写几个URL嘛,GET/POST/DELETE随便搞搞就完事了。结果第一个项目上线后,前端同事跑来骂我:接口命名乱七八糟、状态码含义模糊、版本管理一塌糊涂。真是应了那句话:程序员的代码别人看不懂,API设计自己看不懂。
开篇配图:一张混乱的接口文档截图,上面画满红圈标注的命名冲突、状态码不当、版本缺失等问题。
先说说资源命名这个坑
刚开始我以为URL名字长点没关系,于是在项目中出现了这种写法:
“`javascript
// 错误示范
GET /api/getUserInfo/123
POST /api/createNewOrder
DELETE /api/deleteProduct/456
“`
结果前端同事说:”大哥,RESTful API的核心是用HTTP动词表达操作,你动词写进URL里,那我还用HTTP干嘛?”
正确的做法是这样的:
“`javascript
// 正确示范
GET /api/users/123
POST /api/orders
DELETE /api/products/456
“`
为什么要这么写?因为RESTful API的设计哲学是”资源导向”。每个URL代表一个资源(users, orde
rs, products),HTTP动词(GET, POST, PUT, DELETE)代表对这个资源的操作。这样设计后,前端看到一个URL就能猜到这是个什么操作,根本不用看文档。
另一个坑是复数命名。我当时用单数,结果项目大了之后,一堆/user、/order、/product混在一起,前端每次都要查文档确认。后来统一改成复数形式:
“`javascript
// 统一复数
GET /api/users
GET /api/users/123
POST /api/orders
PUT /api/orders/456
“`
这看起来是小事,但统一命名规范后,团队沟通成本直接下降了50%。
状态码设计:这个设计真的反人类
官方文档把状态码文档不够清晰,我刚开始也踩了坑。比如404大家都懂,但201和200的区别呢?401和403的差异呢?
先看看我犯过的错:
“`javascript
// 错误示范:所有成功都返回200(代码语法正确,但状态码使用不当)
app.get(‘/api/users/123’, (req, res) => {
res.status(200).json({ data: user });
});
app.post(‘/api/users’, (req, res) => {
// 创建用户成功
res.status(200).json({ data: newUser });
// 应该用201 Created
});
“`
正确的状态码设计应该是这样的:
“`javascript
// 正确示范:精确使用状态码
// 获取资源成功
app.get(‘/api/users/123’, (req, res) => {
res.status(200).json({ data: user });
});
// 创建资源成功
app.post(‘/api/users’, (req, res) => {
res.status(201).json({ data: newUser });
});
// 更新资源成功
app.put(‘/api/users/123’, (req, res) => {
res.status(200).json({ data: updatedUser });
});
// 删除资源成功
app.delete(‘/api/users/123’, (req, res) => {
res.status(204).send(); // 204 No Content
});
“`
核心配图:一张状态码使用对比表,左边是错误用法(如404用于验证失败),右边是正确用法(404表示资源不存在)。
为什么要这么讲究?因为状态码是API的”表情包”,前端通过状态码就能快速判断响应类型。比如201表示创建成功,前端就可以直接跳转到资源详情页;204表示删除成功,前端就直接刷新列表。如果都用200,前端还得解析响应体才知道具体操作结果,效率低很多。
还有个技巧:错误码也要统一格式。我后来统一成这种结构:
“`javascript
{
“error”: {
“code”: “USER_NOT_FOUND”,
“message”: “用户ID 123 不存在”,
“details”: {
“requested_id”: “123”
}
}
}
“`
这样前端拿到错误后,可以直接用code字段做逻辑判断,用message字段显示给用户,details字段提供调试信息。从3.2秒的调试时间降到了0.8秒,效率提升明显。
版本管理:另一个容易被忽视的坑
正确的做法是用URL路径来做版本管理:
“`javascript
// 版本管理示范
// v1版本
GET /api/v1/users
// v2版本 – 新增了用户角色字段
GET /api/v2/users
“`
为什么要放在路径里?因为这样最清晰,前端一看就知道用的是哪个版本的接口。而且可以通过路由中间件轻松管理:
“`javascript
// Express中间件管理版本
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get(‘/users’, v1UserController.getUsers);
v2Router.get(‘/users’, v2UserController.getUsers);
app.use(‘/api/v1’, v1Router);
app.use(‘/api/v2’, v2Router);
“`
这样做的好处是:新版本上线时,旧版本可以继续运行一段时间,给前端迁移的时间。一般建议保留至少两个版本(当前版本和上一个版本),旧版本下线前要提前通知前端团队。
还有个重要规则:版本号不要用”latest”或”current”,因为那样你永远不知道用户用的是哪个版本。用数字版本号(v1, v2, v3…),每次重大变更才升级版本号。另外,也有团队通过请求头(如Accept: application/vnd.example.v1+json)来管理版本,但URL路径方式更直观、易于调试,所以我个人更推荐。
过滤、排序、分页的设计
这是前端最常用的功能,但我最初设计的接口完全没有考虑这些。结果前端每次都要先拉取所有数据,然后在客户端做过滤,性能很差。
正确的做法是在URL中使用查询参数:
“`javascript
// 过滤、排序、分页
GET /api/users?age=25&gender=male
GET /api/users?sort=-created_at
GET /api/users?page=1&per_page=20
“`
后端实现时,需要从查询参数中提取这些字段,并注意过滤掉非数据库字段(如sort、page),避免安全风险。下面是一个使用Mongoose的示例:
“`javascript
// 从查询参数中提取并安全过滤
const { page = 1, per_page = 20, sort, …filters } = req.query;
// 只允许白名单中的字段作为数据库过滤条件
const allowedFilters = [‘age’, ‘gender’, ’email’];
const safeFilters = {};
for (const key of allowedFilters) {
if (filters[key] !== undefined) safeFilters[key] = filters[key];
}
// 执行查询
let query = User.find(safeFilters); // 假设使用Mongoose
if (sort) {
query = query.sort(sort);
}
const total = await User.countDocuments(safeFilters);
const users = await query.skip((page – 1) * per_page).limit(per_page);
res.status(200).json({
data: users,
pagination: {
page: Number(page),
per_page: Number(per_page),
total
}
});
“`
这样设计后,前端可以灵活地组合过滤条件、排序和分页,后端也能保证安全性和性能。从最初的全量数据拉取,到现在的按需查询,接口响应时间从2秒降到了200毫秒以内。