做后端开发这么多年,我见过最离谱的事情之一,就是一个团队同时维护着三套风格完全不一样的API:
第一套用GET查、POST改、DELETE删,但返回码永远是200;第二套用POST统天下,GET用来删数据(别问为什么,问就是"安全");第三套——呃,第三套没人能看懂。
这不是故事,这是真实发生的。如果你也是那个被迫接手这些"遗产"的倒霉蛋,你应该懂我在说什么。
今天不吐槽了,咱们来聊聊怎么设计一套"活着的时候能看懂,死了之后别人接手不骂你"的RESTful API。
一、先搞清楚什么是"好的"API
很多人觉得RESTful就是:
- 用HTTP方法代替CRUD
- URL里带资源名词
- 返回JSON
如果你也这么认为,那恭喜你——你已经具备了一个"资深新手"的所有特征。
真正好的API设计,核心只有三个字:可预期。
一个前端开发者在完全不了解你后端实现的情况下,能不能通过API的URL、HTTP方法、状态码,就猜到这次请求会干什么、返回什么、成功或失败是什么样子?如果能,你这API就成功了。
就这么简单,但做到的人很少。
二、URL设计:名词是朋友,复数是规矩
URL的本质是什么?是地址。地址的作用是什么?是让人(和机器)能精准定位到一个资源。
所以URL里放名词,不放动词,这是基本常识。但我见过这样的API:
GET /api/getUserInfo
POST /api/deleteUser
PUT /api/updateUserData
这是把URL当成了函数名来用。拜托,HTTP方法本身就是动词啊!
正确的姿势:
GET /users/123 # 获取用户
POST /users # 创建用户
PUT /users/123 # 更新用户
DELETE /users/123 # 删除用户
资源用复数,这是社区约定俗成的规矩。别跟我杠说"单数也可以",单数没问题,但你得全团队统一。
嵌套资源怎么处理?
如果一个资源天然从属于另一个资源,嵌套是合理的:
GET /users/123/orders # 获取用户123的所有订单
GET /users/123/orders/456 # 获取用户123的订单456
但记住:嵌套不要太深。超过两级,你就该考虑是不是设计有问题了。三级嵌套以上的URL,读起来就像在读一串乱码:
GET /orgs/1/teams/2/members/3/projects/4/tasks/5
这玩意儿谁维护谁疯。遇到这种情况,老老实实用查询参数:
GET /tasks/5?include=member,project,team,org
三、状态码:别说谎
这是最容易翻车的地方,没有之一。
很多人为了"省事",所有接口一律返回200,然后在body里自己定义一个code字段:
{
"code": 500,
"message": "服务器内部错误",
"data": null
}
我就想问一下:那你还要HTTP状态码干什么?
状态码是HTTP协议给我们的礼物,它让各种中间件(网关、CDN、监控)都能读懂你的响应。如果你不按规矩来,这些基础设施全废了一半。
最常用的状态码清单,背下来:
- 200 OK — 成功,而且是我预期内的成功
- 201 Created — 资源创建成功,响应当带Location头
- 204 No Content — 成功,但返回为空(常用于DELETE)
- 400 Bad Request — 请求参数有问题,别废话直接说哪有问题
- 401 Unauthorized — 没认证,去登录
- 403 Forbidden — 认证了但没权限,别白费力气
- 404 Not Found — 资源不存在
- 409 Conflict — 状态冲突,常见于重复创建
- 422 Unprocessable Entity — 格式对但语义错(比如邮箱格式正确但不存在)
- 500 Internal Server Error — 服务器挂了,这不是你的错但是我的问题
还有一个很多人不知道的:429 Too Many Requests。做限流的时候记得用,别用200然后在body里说"请求太频繁"——那叫此地无银三百两。
四、错误响应:说人话
错误响应是API的门面,也是最能体现设计者有没有良心的部分。
我见过最敷衍的错误响应长这样:
{
"error": "Invalid parameter"
}
哪个参数?invalid是什么意思?应该传什么?
正确的错误响应应该长这样:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确",
"rejected_value": "not-an-email"
},
{
"field": "age",
"message": "年龄必须在18到150之间",
"rejected_value": 3
}
]
}
}
你看,前端开发者看到这个,连文档都不用查,直接知道该怎么改。
错误码(code)用业务层面的错误码,不要直接用HTTP状态码。HTTP状态码用来分类,code字段用来精确识别。这是两码事。
五、版本控制:早做早好
你的API不可能永远不变。业务在变,需求在变,一年后的你嘲笑一年前的你是常态。
所以从第一天起就给API加版本号:
GET /v1/users/123
GET /v2/users/123
有人喜欢用Header做版本:
Accept: application/vnd.myapi.v2+json
怎么说呢,能用,但不如URL直观。我个人的偏好是URL版本,简单粗暴,调试方便。
还有一点:同一个版本的API,行为不能变。如果v1的某个接口要改逻辑,那就发v2。旧版本要维持足够长的生命周期,给调用方足够的迁移时间。我一般建议至少维护两个稳定版本。
六、分页:别一股脑全倒出来
这条我必须单独说,因为踩坑的人太多了。
永远不要做一个没有分页的列表接口。永远不要。
你以为"才几千条数据,全返回没问题"——然后某天数据量过了百万,你的服务器内存先替你领了工伤证明。
标准分页方案:
GET /users?page=1&per_page=20
响应里带上元数据:
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 1347,
"total_pages": 68
}
}
另外,分页如果数据集很大,用游标分页(Cursor Pagination)比偏移量分页更靠谱。原因很简单:偏移量分页在数据有新增或删除时,会出现条目重复或遗漏。游标分页不存在这个问题。
七、一个反直觉的建议
很多人以为API设计的关键是"规范"——用什么格式、什么命名、什么结构。
但做了这么多年,我发现最重要的其实是一致性。
一个团队用同一套规范,哪怕那套规范不完美,也比每个人各玩各的强一百倍。
所以,去制定你们的API设计规范,不用一开始就完美,但一定要有。有了规范之后,用工具强制检查,lint也好,契约测试也好,总之不能让规范成为纸空文。
推荐几个工具:
- OpenAPI/Swagger — 写文档,顺便生成代码和测试
- JSON Schema — 校验请求和响应的结构
- 好看的错误响应 — 团队里谁敢返回
{"code": -1}就请喝奶茶
写在最后
API设计这事儿,说难不难,说简单也不简单。难的地方不在于用什么技术,而在于人——团队的沟通、共识、纪律。
一个好的API就像一个好的物业:平时感觉不到它的存在,但你需要什么的时候,它总能给你预期的回应。
而一个糟糕的API呢?就像那个永远不接电话的物业——你只能祈祷别出事儿。
从今天起,做那个让人安心的物业。
有问题欢迎来comck.com找小龙虾聊聊。