为什么你的 API 总是不如别人家的?——从设计混乱到让人拍案叫绝的实战经验
干这行这么多年,我见过太多团队在 API 设计上翻车。有些是新手不懂,有些是老手躺平,还有一种是"我觉得这样挺好"的迷之自信。今天不整虚的,直接掰开了揉碎了聊,什么才是真正好的 API 设计。
一、先把"RESTful"这三个字搞清楚再装
十个工程师里有八个张嘴就是"我们用的是 RESTful API",但你让他解释一下什么是 HATEOAS,十个里有九个会卡壳。REST 不是玄学,它有一整套约束条件,你违背了其中任何一条,都不好意思说自己做的是 REST API。
先说资源抽象。REST 的核心是"资源",不是"动作"。我见过太多接口这样写:
POST /api/getUserInfo
POST /api/deleteUser
POST /api/updateUserData
兄弟,你这是在写 RPC 呢。正确的姿势应该是:
GET /api/users/{id} # 获取用户
DELETE /api/users/{id} # 删除用户
PATCH /api/users/{id} # 部分更新用户
记住一个原则:用名词表示资源,用 HTTP 方法表示动作。这是最最基本的,你要是连这个都搞不清楚,后面都是白搭。
二、状态码不是随便糊弄的
有些人返回 200 表示一切 OK,返回 400 表示"你参数错了",返回 500 表示"我程序崩了"——这种粗糙的分法简直是 API 界的乡镇企业水平。
来,我给你捋一捋常用状态码的正确打开方式:
2xx:成功系列
200 OK(标准成功)、201 Created(资源创建成功)、204 No Content(成功但没内容返回,常用于 DELETE)4xx:客户端错误系列
400 Bad Request(参数校验失败)、401 Unauthorized(没登录)、403 Forbidden(没权限)、404 Not Found(资源不存在)、409 Conflict(状态冲突,比如重复创建)、422 Unprocessable Entity(语义错误,参数格式对但业务上不对)、429 Too Many Requests(请求太频繁)5xx:服务端错误系列
500 Internal Server Error(一般性错误)、502 Bad Gateway(上游服务挂了)、503 Service Unavailable(服务不可用)、504 Gateway Timeout(上游超时)
重点说两个容易翻车的:
401 vs 403:没认证用 401,没权限用 403。很多人混着用,这俩区别大了去了。401 的意思是"你倒是先登录啊",403 的意思是"你登录了但这事不归你管"。
400 vs 422:400 是"你这参数我压根不认识",422 是"参数格式没问题但值不对"。比如必填字段你传空字符串,用 400;你传了个负数年龄,用 422。
三、错误响应body,比你想象的更重要
很多团队的错误响应是这个德行:
{"message": "操作失败"}
就一个 message,完了。调用方看到这东西一脸懵逼——到底是参数问题?权限问题?还是服务端抽风?这种设计等于把调试成本全甩给了前端同学,缺德。
推荐一个经过实战检验的 error body 格式:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"details": [
{
"field": "user_id",
"message": "该用户ID不存在于系统中"
}
],
"trace_id": "abc123def456"
}
}
这个格式牛在哪?
code 是给你程序员看的错误码,机器友好,方便前端做分支判断。message 是给人看的描述。details 告诉你具体哪个字段出了问题。trace_id 是链路追踪 ID,出了问题直接查日志,不用在那猜。
还有一点:错误响应尽量用 JSON,不要用 XML。都 2026 年了,还在那儿 application/xml 的要么是祖传代码,要么是跟潮流有仇。
四、分页不是 page 和 size 那么简单
分页这个问题,入门级选手会这样写:
GET /api/users?page=1&size=20
进阶选手知道这种写法的坑——数据有变更时翻页会重复或遗漏。更好的方案是基于游标的分页:
GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20
返回的时候同时给你下一页的 cursor:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true
}
}
这种方案无论数据怎么增删改查,翻页永远准确。当然,如果你数据基本不变,用页码分页也没问题。但如果你做的是动态feed流、消息列表这类东西,必须用 cursor 分页,不然用户刷着刷着看到重复内容,能把你们产品骂自闭。
五、版本管理——早动手早解脱
很多团队一开始不设计版本,觉得"我接口就这么定了,不用改"。结果业务一发展,原有字段不能改、不能删,新增字段又不知道放哪,最后搞出一坨四不像。
API 版本有两种主流做法:
URL 路径版本:
GET /api/v1/users
GET /api/v2/users
Header 版本:
GET /api/users
Accept: application/vnd.myapi.v2+json
我的经验是——能用 URL 版本就用 URL 版本。简单粗暴,一目了然,调试方便。Header 版本看着优雅,但实际开发中你会浪费大量时间在各种配置上,而且出了 bug 排查起来也费劲。
版本什么时候升?记住几个原则:
1. 字段可以新增,但不能修改语义或删除
2. 返回的 JSON 结构不能乱改(新增字段可以)
3. 必须废弃旧版本时,给足过渡期(建议至少 6 个月)
六、幂等性——这个概念被严重低估
幂等性是什么?就是你同一个请求执行一次和执行一百次,效果是一样的。这玩意儿在支付、退款、重试等场景下是性命攸关的。
举两个例子:
创建订单——不是幂等的,每次调用都该创建一个新订单。
取消订单——是幂等的,你取消一次和取消一百次,订单都是取消状态。
HTTP 规范里,GET、PUT、DELETE 是天然幂等的,POST 和 PATCH 不是。所以:
PUT /api/users/{id} # 幂等,完整替换
PATCH /api/users/{id} # 非幂等,部分更新
DELETE /api/users/{id} # 幂等
如果你的 POST 接口需要支持幂等(比如防止重复提交),可以在客户端生成一个唯一 ID(通常是 UUID),放到 Header 里:
POST /api/orders
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
服务端根据这个 key 做去重,保证同一个 key 的请求只执行一次。这在第三方支付回调场景特别有用——网络抖动导致回调重试,你总不能给用户重复充值吧?
七、写在最后
API 设计这事儿,说难不难,说简单也不简单。难的地方不在于记住多少规则,而在于如何在业务迭代和架构优雅之间找平衡。
有些人追求完美设计,结果接口画图很漂亮,实际业务一跑傻眼了。有些人完全放飞自我,接口怎么方便怎么来,最后债务滚到还不起。
我的建议是:保持克制,每一次过度设计都是未来的坑。先用最简单的方案,能 hold 住就先用着,等业务真的证明需要扩展了再重构。提前优化是万恶之源,这条准则在 API 设计领域同样适用。
当然,有些基础原则是不能妥协的——状态码要用对、错误信息要完整、版本管理要提前想好。这些你要是糊弄,迟早会有人教你做人。
行了,今天就唠到这儿。希望你下次设计 API 的时候,能少走点弯路。共勉。