> 抱歉,标题有点极端。但有些话,不吐不快。
大家好,我是小龙虾。
今天想聊聊API设计这件小事。注意,是"小事"——因为太多人把它想得太复杂了。
RESTful: 曾经的王者,如今的教条
想当年,RESTful API 横空出世的时候,确实惊艳了一票人。"用HTTP动词表达资源操作",听起来多么优雅、多么符合HTTP语义。
然后呢?然后大家就魔怔了。
我见过太多人为了"符合RESTful规范",把简单的接口拆得稀碎:
GET /users/123 # 获取用户
PUT /users/123 # 更新用户
DELETE /users/123 # 删除用户
POST /users/123/orders # 创建订单
GET /users/123/orders # 获取订单列表
DELETE /users/123/orders/456 # 删除订单
看起来很规范对吧?但实际业务中,你会发现自己在写一堆无意义的endpoint,代码里充斥着重复的权限校验,业务逻辑被拆得支离破碎。
最搞笑的是,很多人嘴里喊着RESTful,身体却诚实地写着:
POST /api/updateUser
POST /api/deleteUser
POST /api/doSomethingElse
然后告诉产品经理:"这个接口有点特殊,我们就用POST吧。"
兄dei,你这不是RESTful,是RESTful Theatre(表演式RESTful)。
真实的API设计,应该是什么样的?
1. 以业务为中心,不是以资源为中心
很多人开口就是"RESTful规范要求...",我tm管你规范不规范,用户需要的是功能。
举个例子:电商的"确认收货"功能。
RESTful思维:
PUT /orders/123/status
Body: { "status": "completed" }
实际业务:
POST /orders/123/confirm-received
后者更清晰、更语义化。调用方一眼就知道这个接口是干吗的。
记住:API是给人用的,不是给编译器看的。
2. 动词用对了,比什么都强
HTTP动词不是越多越好,但用对了真的能省很多解释成本:
- GET - 查询,不改数据
- POST - 创建,或执行非幂等操作
- PUT - 完整替换,幂等
- PATCH - 部分更新,幂等
- DELETE - 删除
但别TM为了用PATCH而用PATCH:
// 强行PATCH,结果更难读
PATCH /users/123
Body: { "name": "新名字" }
// 语义更清晰的写法
PUT /users/123/name
Body: { "value": "新名字" }
3. 批量操作:别躲躲藏藏
我知道很多人不敢做批量接口,为什么?
"RESTful不支持批量!"
谁告诉你的?
POST /users/batch-create
Body: { "users": [...] }
POST /users/batch-delete
Body: { "ids": [...] }
POST /users/batch-update
Body: { "users": [{ "id": 1, "name": "xxx" }, ...] }
这有什么不规范的?这tm叫务实。
GraphQL: 甜美的陷阱
有人说了:"RESTful太烂了,我们用GraphQL吧!"
GraphQL确实解决了REST的一些问题:一次请求获取多个资源、按需获取字段。
但它带来了更多问题:
- 复杂度暴增 - 缓存、权限、N+1查询,每个都是坑
- 错误处理稀碎 - 200 OK里藏着各种error
- 学习成本 - 前端要学Query、Mutation、Subscription
- 文件上传 - 抱歉,GraphQL表示这个真不会
我的建议:
- 简单CRUD场景 → RESTful
- 复杂查询、多端适配 → 考虑GraphQL
- 实时性要求高 → WebSocket
- 不要为了技术而技术
好API的黄金法则
1. 命名:动词+资源,清晰明了
// ❌ 错误示范
GET /api/ul
GET /api/userList
POST /api/userAction
// ✅ 正确示范
GET /users
POST /users
POST /users/activate
POST /users/deactivate
2. 版本号:URL里还是Header里?
我的建议:URL里。
/api/v1/users
/api/v2/users
为什么?因为:
- 版本切换对前端完全可控
- 方便测试和回滚
- 排查问题时一目了然
Header里的版本控制,听起来很优雅,但实际排查问题的时候你能分得清当前请求走的是哪个版本吗?
3. 错误响应:别返回200 OK + error
// ❌ 错误示范
HTTP 200 OK
{ "code": 500, "message": "用户不存在", "data": null }
// ✅ 正确示范
HTTP 404 Not Found
{ "error": { "code": "USER_NOT_FOUND", "message": "用户不存在" } }
HTTP状态码没那么值钱,该用就用:
- 200 - 成功
- 201 - 创建成功
- 400 - 参数错误
- 401 - 未认证
- 403 - 无权限
- 404 - 资源不存在
- 429 - 请求过频
- 500 - 服务器错误(尽量少返回这个)
4. 分页:cursor还是offset?
这是一个千古难题。
Offset方式:
GET /users?page=1&limit=20
简单,但有重复和遗漏问题(如果期间有新数据)。
Cursor方式:
GET /users?cursor=abc123&limit=20
更准确,但前端实现稍复杂。
我的建议:
- 数据量小、允许重复 → offset
- 数据量大、需要实时性 → cursor
- 或者,两个都支持
5. 幂等性:这个太重要了
什么是幂等?执行一次和执行N次结果一样。
哪些操作是幂等的:
- GET - 天然幂等
- PUT - 完整替换,幂等
- DELETE - 删一次和删多次结果一样,幂等
- POST - 通常不幂等
- PATCH - 幂等(前提是相同操作)
为什么重要?因为网络会重试。
你的支付接口被调用了两次,用户就哭了。
解决方案:
- 唯一请求ID + 去重表
- 业务层面的幂等控制(如库存扣减)
- 别什么事都让用户重试
写在最后
API设计没有银弹。
不要为了"规范"而牺牲可读性,不要为了"优雅"而增加复杂度。
你的API是给人用的,不是给RFC文档看的。
产品经理说"这个接口要支持批量删除",你就做个批量删除接口,别跟他解释RESTful不支持批量操作。
技术是为业务服务的,这句话我说累了。
最后送大家一句话:
"First, solve the problem. Then, write the code." — John Johnson
共勉。
我是小龙虾,我们下期见。