干这行这么多年,我见过太多项目在API设计这块翻车。不是接口返回格式乱成一锅粥,就是版本管理让人摸不着头脑,还有那该死的错误处理,简直是灾难现场。今天咱们就来扒一扒API设计里那些不得不说的事情,保证让你看完直拍大腿——原来这里有这么多门道!
一、URL设计:别把接口写成密码
先说说URL设计这事儿。我见过最离谱的接口是这样的:
/api/v1/user/123/action/getInfoByIdAndType?type=2&from=app
看到这种接口,我只想问一句:你是故意的吧?
好的URL设计应该遵循这几个原则:
- 名词而非动词:用 /users 而不是 /getUsers,用 /orders 而不是 /fetchOrders
- 层级清晰:资源嵌套要有意义 /users/123/orders 表示用户123的订单列表
- 避免冗余:不需要每个接口都加 /api 前缀,路由本身就应该语义清晰
- 统一风格:要么全小写+横杠,要么全小写+下划线,别一会这样一会那样
我现在的习惯是:所有资源用复数名词,嵌套表示从属关系,过滤条件用查询参数。比如:
GET /articles?category=tech&status=published&page=1&per_page=20
清爽,一眼就能看懂是做啥。
二、HTTP方法:别只会用GET和POST
这是重灾区。很多后端程序员,不管什么操作一律POST走天下。你去问他为什么,他说我只会这个。
拜托,HTTP定义了这么多方法,不是摆设:
- GET - 读取资源,安全,不会改变状态
- POST - 创建资源,非幂等
- PUT - 完整更新资源,幂等
- PATCH - 部分更新资源
- DELETE - 删除资源,幂等
有人要问了,PUT和PATCH啥区别?简单说:PUT是全量替换,PATCH是局部更新。比如你有个用户对象:
// 原始数据
{ "name": "张三", "email": "zhangsan@example.com", "age": 25 }
// PUT 更新(需要传全量)
PUT /users/123
{ "name": "张三", "email": "zhangsan_new@example.com", "age": 26 }
// PATCH 更新(只传要改的)
PATCH /users/123
{ "email": "zhangsan_new@example.com" }
用对了方法,前端开发会感谢你的,debug的时候也能少骂几句。
三、状态码:别总返回200然后在body里写error
这个问题太普遍了。我见过无数接口,HTTP状态码永远是200,但body里写着:
{ "code": 500, "message": "服务器内部错误", "data": null }
我就想问问,既然error,为啥状态码不用500?
正确的做法是让HTTP状态码本身就能说明问题:
- 200 - 成功
- 201 - 资源创建成功(比如POST后)
- 400 - 请求参数有问题,客户端的错
- 401 - 未认证,请先登录
- 403 - 已认证但没权限
- 404 - 资源不存在
- 422 - 请求格式正确但语义错误(比如必填字段缺失)
- 429 - 请求太频繁,悠着点
- 500 - 服务器出问题了
有人说我这是RESTful设计,太严格了。朋友,这不是严格,这是基本素养。就像你不能把错误信息返回200一样,状态码和内容保持一致是天经地义的事情。
四、错误处理:给开发者一条活路
错误信息的设计,直接决定了你API的可用性。我见过最离谱的错误返回是这样的:
{ "error": "操作失败" }
操作失败?啥操作?为啥失败?程序员看到这种错误,只能对着屏幕发呆。
好的错误响应应该包含:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数验证失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确",
"value": "not-an-email"
},
{
"field": "age",
"message": "年龄必须在18-150之间",
"value": "16"
}
],
"request_id": "req_abc123"
}
}
这样开发者拿到错误信息,就知道具体是哪个字段出了问题,该怎么修复。而且加上request_id,方便排查问题。
另外,错误信息要分层次:面向开发者的详细错误(比如字段名、约束条件),和面向用户的友好提示(不用告诉他们技术细节)。
五、版本管理:没有版本控制的API是裸奔
API上线之后,不可避免地要迭代升级。但如果你改了接口,旧版本的客户端可能就全炸了。所以版本管理是必须的。
常见的版本管理方式有几种:
方式一:URL路径版本
/api/v1/users
/api/v2/users
这是最直观的方式,Netflix、GitHub都在用。优点是一眼就能看出调用的是哪个版本。
方式二:Header版本
GET /api/users
Accept: application/vnd.myapi.v2+json
这种方式更符合REST规范,但不够直观。多数开发者看到这种header,第一反应是懵。
方式三:查询参数版本
/api/users?version=2
不推荐。这种方式容易被忽略,而且会被缓存影响。
我的建议是用方式一,简单直接。版本号用v1、v2这样的大版本号,不要精确到v1.1、v1.2。破坏性变更才升主版本,小的优化用向后兼容的方式去做。
六、分页:别一次性把数据全返回了
这条可能大家都知道,但架不住还是有人犯。有人写列表接口,数据库里有10万条数据,他返回一个10万条的数组。前端拿到直接卡死。
分页是必须的,而且是服务端分页,不是前端截断。常见的有两种方式:
Offset分页
GET /articles?page=2&per_page=20
// 返回
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 1000,
"total_pages": 50
}
}
简单易用,但有个问题:数据量大的时候,翻到后面会变慢,因为要跳过大堆数据。
Cursor分页(游标分页)
GET /articles?cursor=eyJpZCI6MTAwfQ&per_page=20
// 返回
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true
}
}
适合大数据量,不管翻到第几页,性能都稳定。缺点是不能随机跳页。Twitter、Instagram都在用这种方式。
选哪种?看场景。数据量小、需要随机跳页的,用offset。数据量大、只做列表翻页的,用cursor。
七、写在最后
API设计这事儿,说简单也简单,说难也难。简单在于那些原则你可能都听过,难在于真正写代码的时候能不能守住这些原则。
我见过太多项目,一开始图快,随便定义接口,上线之后要改才发现骑虎难下。改吧,要通知所有调用方同步更新;不改吧,留一堆技术债天天被人骂。
所以我的建议是:接口设计的时候多花点心思,写好文档,定义好规范。虽然前期慢一点,但后期维护会轻松很多。毕竟,写代码一时爽,接口乱了一生埋。
好了,今天就聊到这儿。如果你有什么API设计的心得体会,欢迎交流。下次再扒点别的干货。