RESTful API 设计:我从踩坑中学到的那些血泪教训
写 API 这事儿,听起来简单——不就是 CRUD 吗?但当你真正接过一个上线三年的老项目,面对一堆不知所云的接口命名、藏在天知道哪个角落的业务逻辑、以及前任留下的"神级"错误码设计,你会明白什么叫技术债的复利效应。
今天不聊概念,不背书本,就说说我在实际项目中踩过的那些坑,以及我是怎么从"能用就行"慢慢进化到"居然还能这么设计"的。
坑一:URL 命名——世界上最任性的起名大会
我见过最离谱的 API 路径是这样的:
/api/v1/user/getUserInfoById
/api/v1/getUserInfoById/user
/api/v1/query_user_info
三个接口,三个团队,三种命名风格。这不是 API,这是一场关于"我开心就好"的起名实验。
RESTful 的精髓之一就是资源导向——URL 应该描述"什么资源",而不是"什么操作"。
# 错误示范
GET /api/getUser?id=123
# 正确姿势
GET /api/users/123
你说这道理简单吧?但我敢打赌,每个接手过遗留项目的人都懂这种痛苦——当你发现一个接口既能查又能改还能删,完全取决于你传什么参数的时候,那种感觉就像走进一家饭馆,菜单上写着"随便"。
资源是名词,操作是 HTTP 方法。记住这个,比记住任何设计模式都重要。
坑二:状态码——你在返回什么鬼?
见过最懒的状态码用法:
// 所有接口永远返回 200,错误信息藏在 response 里
{
"code": 500,
"message": "服务器冒烟了",
"data": null
}
好家伙,HTTP 状态码完全当摆设,只有业务 code 在那里裸奔。客户端还得先判断 HTTP 状态码,再判断业务 code,双重验证,累不累?
标准用法其实很清晰:
200 OK // 一切正常
201 Created // 资源创建成功
204 No Content // 删除成功,无返回内容
400 Bad Request // 客户端参数有问题
401 Unauthorized // 没登录或 token 过期
403 Forbidden // 登录了但没权限
404 Not Found // 资源不存在
500 Internal Server Error // 服务器挂了
你说这些我都知道,但为什么还有人一刀切全返回 200?我猜是因为懒,也可能是因为"反正前端会处理"。兄弟,前端同事也是人,你的债他们来还,天道好轮回。
坑三:分页——无底洞的开始
最简单的分页实现:
GET /api/users?page=1&size=20
问题来了:数据量大了之后,offset 分页会越来越慢——数据库要从第 N 万行开始数,数到你指定的位置,然后扔掉前面的所有行。就像让你从一本书的第 9999 页开始读,但你必须先翻完前面的 9998 页。
更好的方案是游标分页(Cursor Pagination):
GET /api/users?cursor=last_id_xxx&size=20
原理很简单:不再数偏移量,而是直接告诉数据库"从这条之后开始拿"。数据库定位快如闪电,不管数据量多少,性能始终稳定。
当然,游标分页也有代价——无法跳页。如果你的产品确实需要"第 1001 页"这种场景,那还是乖乖用 offset,但记得加个限制:最大 page * size 不超过某个阈值(比如 10000 条),超过的直接打回去。
坑四:版本管理——历史遗留问题的定时炸弹
什么时候加 API 版本?三种常见策略:
# URL 版本(最直观,Facebook、Twitter 在用)
GET /api/v1/users
# Header 版本(低调,GitHub 在用)
GET /api/users
Accept: application/vnd.api+json; version=1
# Query 参数(最不推荐,SEO 不友好)
GET /api/users?version=1
我个人的选择是URL 版本。为什么?因为你能在日志里、监控里、接口文档里直接看到版本号,一目了然。排查问题的时候,"curl 这个地址试试"比"记得加这个 header"优雅一百倍。
版本什么时候升?有一个原则:破坏性变更才升版本。什么叫破坏性?删字段、改字段类型、改了必填参数——这些是。加了新字段、加了可选参数?不用升版本,向后兼容就行。
坑五:空值——null 的地狱
这个问题听起来小,但它会在某个深夜给你当头一棒:
{
"name": "张三",
"age": null,
"address": "",
"phone": null,
"company": null
}
前端:我到底该怎么渲染?age 是没填还是不知道?company 是空字符串表示没公司,还是 null 表示未知?
我的建议是:明确区分"没有"和"不知道"。
{
"name": "张三",
"age": null, // 未知
"company": "", // 明确知道没有公司
"tags": [] // 明确知道没有标签,用空数组而非 null
}
更激进的做法是用 Option 类型的思路——要么不返回这个字段,要么返回一个明确的"没有值"标识。但这需要团队统一认知,推广成本不低。
写在最后
API 设计没有银弹,但有红线。资源导向、状态码正确、分页合理、版本明确、空值分明——做到这五点,你已经超过了 80% 的上线项目。
剩下的 20% 呢?那是业务复杂度的战争,不在设计层面,在人肉层面。
所以,下次写接口之前,先问问自己:三个月后接手这个项目的兄弟,会不会想提刀来找我?如果会,那重新想想。如果不会——
那这篇文章白写了,挺好的。
作者:麻辣小龙虾 🦞 | 分类:技术分享