RESTful API设计避坑指南:我用尸体铺出来的经验
写API这件事吧,看起来简单,不就是增删改查吗?但你要是真这么想,等上线之后就会发现你的接口被各种奇奇怪怪的调用方式折磨得死去活来。今天我就来聊聊那些年我在API设计上踩过的坑,以及怎么绕过去。
一、状态码:别再用200表示一切了
这是最容易翻车的地方。我见过太多API,所有响应都返回200,然后在里面塞个code字段说"0表示成功,1表示失败"。兄弟,你这是在HTTP协议的脸上蹦迪啊。
HTTP状态码是干嘛用的?是让调用方在底层就知道请求结果的东西。如果你返回200但业务逻辑失败了,代理服务器、CDN、各种中间件都会以为一切正常,然后缓存这个"成功"的失败响应。
来,上点实在的:
// 正确的做法
GET /users/123
200 OK → 用户存在,返回数据
404 Not Found → 用户不存在
401 Unauthorized → 没登录或token过期
403 Forbidden → 登录了但没权限
429 Too Many Requests → 触发限流了
// 业务层面的错误用4xx或5xx
400 Bad Request → 参数校验失败
422 Unprocessable Entity → 语义正确但业务规则不允许
500 Internal Server Error → 服务端出问题了
记住:状态码是你的第一道语言,代码是你的第二道语言。别让状态码失语。
二、命名:URL是给人看的,不是给机器看的
我见过这样的API:
POST /api/v1/user/add
POST /api/v1/user/update
POST /api/v1/user/del
好家伙,三个接口一个动词,区别在于最后一个字母。这是考验调用者的拼写能力吗?
REST的精髓是什么?资源+动作=URL。名词复数,动词用HTTP方法:
GET /users → 获取用户列表
POST /users → 创建用户
GET /users/123 → 获取某个用户
PUT /users/123 → 完整更新用户
PATCH /users/123 → 部分更新用户
DELETE /users/123 → 删除用户
但这里有个坑:很多人把PATCH和PUT混用。区别在于:PUT是全量替换,PATCH是部分更新。如果你要更新用户的昵称,用PATCH /users/123,body里只传nickname就行,不用把整个用户对象都传过来。
另外,URL命名有几个原则:
- 小写+中划线:/user-orders,不要/userOrders或/user_orders
- 用复数名词:/users不要/user,除非是单个资源但场景确实需要单数
- 避免动词:用GET/POST/PUT/DELETE表达动作,URL里不要出现get、create这种词
- 层级不要过深:/users/123/orders/456这种可以,但/users/123/orders/456/items就有点过分了,这时候该考虑用查询参数或者拆接口了
三、版本控制:给你的API留条活路
你的API迟早要变。加字段、改字段、删字段——这些都是雷。最好的办法是从一开始就做版本控制。
https://api.example.com/v1/users
https://api.example.com/v2/users
常见的版本控制方式有三种:
1. URL路径版本(最直观)
GET /v1/users
GET /v2/users
优点:直观,调试方便。缺点:版本多了URL会变。
2. Header版本(更"规范"但麻烦)
GET /users
Accept: application/vnd.example.v2+json
优点:不污染URL。缺点:调试的时候得手动改Header,有点反人类。
3. Query参数版本(不推荐)
GET /users?version=2
这种方式的问题是版本参数容易被缓存系统忽略,而且不RESTful。除非你有特别的理由,否则别用。
我的建议?用URL路径版本。简单、直观、调试友好。Google、GitHub、Stripe都是这么干的。
四、分页:没有分页的列表接口是灾难
如果你的/users接口返回十万条数据,你觉得会发生什么?
轻则接口超时,重则数据库被打爆,更重则你的服务器直接原地升天。所以,列表接口必须分页,而且要支持游标分页(Cursor Pagination)而不是传统的偏移分页(Offset Pagination)。
为什么?偏移分页有这个经典问题:
// 用户在看第100页,每页20条
GET /users?page=100&per_page=20
// 此时另一个用户被删除了
// 用户翻到第101页
GET /users?page=101&per_page=20
// 结果:同一行数据出现了两次,或者某条数据彻底消失了
这就是"分页漂移"问题。游标分页就不一样了,它基于最后一条数据的ID来定位,数据库新增或删除记录不会影响已有页面的数据。
// 游标分页示例
GET /users?limit=20
GET /users?limit=20&cursor=eyJpZCI6MTAwMH0
// 返回格式
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTAyMH0",
"has_more": true
}
}
limit建议默认20,最大不超过100。别让调用方一口吃成胖子。
五、错误处理:给调用方一条活路
错误响应是API的门面。很多人的错误响应是这样的:
{
"message": "操作失败"
}
好,调用方看到这个错误,能干嘛?啥也干不了。他不知道是参数问题、网络问题、还是服务端崩了。所以,错误响应要有结构,要有料。
{
"error": {
"code": "INVALID_PARAMETER",
"message": "手机号格式不正确",
"details": {
"field": "phone",
"reason": "格式应为138xxxxxxx",
"received_value": "12345"
},
"request_id": "req_abc123xyz"
}
}
这样调用方至少知道:问题出在哪个字段、为什么出错、怎么改。而且request_id很重要,出了生产问题,你靠这个ID去日志系统里查链路,比对"操作失败"四个字强一万倍。
另外,错误响应要有一致性。你不能有时候返回code有时候返回error_type有时候返回err_code。定好规范,全站统一。
六、安全:基础中的基础
这块本来不想写,但见过太多裸奔的API,还是提一嘴:
- 强制HTTPS:别跟我扯什么内网不需要,302跳转可以被劫持
- 认证用JWT或OAuth2:别把token放URL里,URL会被日志记下来
- 限流必须做:没限流的API等于没有护城河,分分钟被人刷爆
- CORS要配置正确:生产环境的CORS配置别用星号
- 敏感数据要脱敏:日志里别出现密码、token、完整手机号或身份证号
写在最后
好的API设计就像好的代码,不需要注释就能读懂。调用方拿到接口文档,应该有一种"哇,这个接口怎么设计得这么 intuitive"的感觉,而不是"这啥玩意儿,联系你们开发吧"。
当然,罗马不是一天建成的。你不需要一开始就设计出完美的API,但至少要避开那些明显的坑:状态码别乱用、命名要规范、版本要控制、分页必须做、错误响应要有料。
剩下的,慢慢迭代就好。API是活的,会随着业务一起成长。
祝你的接口稳如老狗,调用方笑口常开。