做后端开发这么多年,写过的 API 没有一千也有八百。从最开始的「能跑就行」,到后来的「这个接口设计得真烂我自己都看不下去」,再到现在的「有点东西」,中间踩过的坑够我写一本血泪史。今天不整虚的,直接上干货,说说那些真正有用的 API 设计套路。
一、先搞清楚你是在给谁写接口
很多新手拿到需求就开始写,写完了发现移动端要的数据和 PC 端不一样,然后就开始疯狂打补丁。一个接口走天下?迟早把自己走死。我的经验是:按客户端维度拆分接口,而不是按业务逻辑堆在一起。
举个例子,有个电商系统,最开始只有一个商家后台,接口都是给 PC 端用的。后来上了 APP,然后又上了小程序,还有供应商入驻的独立后台。结果呢?一套接口打天下,每次改一个端的需求,其他端跟着遭殃。这种耦合简直让人崩溃。
正确的做法是:API Gateway 后面按客户端部署独立的服务。BFF 模式(Backend For Frontend)不是银弹,但在多端适配这件事上,确实能救命。
二、URL 设计:名词为王,别整动词
这个问题我见过无数次了:
POST /api/getUserInfo
POST /api/addNewProduct
POST /api/deleteOrderById
GET /api/updateUserData
兄弟,你这不叫 RESTful,你这叫「用 HTTP 方法的壳子包装 RPC 的灵魂」。REST 的精髓是什么?用名词表示资源,用 HTTP 方法表示操作。
正确的打开方式:
GET /api/users/123 # 获取用户信息
POST /api/users # 创建用户
PUT /api/users/123 # 更新用户
DELETE /api/users/123 # 删除用户
PATCH /api/users/123 # 部分更新用户
PATCH 和 PUT 的区别很多人搞不清楚。简单说:PUT 是全量替换,PATCH 是部分更新。比如更新用户头像,PATCH 就比 PUT 优雅得多——你不需要传一整坨用户对象。
三、状态码:别一成功就返回 200
我见过最离谱的接口是:不管发生什么,返回永远是 200,然后 success 字段 true 或 false。这完全浪费了 HTTP 状态码的意义。
状态码就是接口的语言,你的合作伙伴(前端、移动端、其他服务)都在听这门语言。你不能说「成功了」,然后在消息体里悄悄说「其实失败了」。这不是欺骗是什么?
我的状态码规范:
200 OK # 查询成功、更新成功
201 Created # 资源创建成功,响应头带上 Location
204 No Content # 删除成功,无响应体
400 Bad Request # 参数校验失败,附上错误详情
401 Unauthorized # 没登录或 token 过期
403 Forbidden # 登录了但没权限
404 Not Found # 资源不存在
409 Conflict # 资源冲突,比如用户名已存在
422 Unprocessable # 语义错误,参数格式对但逻辑不对
429 Too Many Request # 请求过于频繁
500 Internal Error # 服务器炸了
重点说下 201 和 204。创建资源返回 201 是不成文的规定,同时在响应头加上 Location: /api/users/456,告诉调用者新资源在哪。删除操作返回 204 是因为删除操作通常没有响应体,你总不能返回一个「删除成功」吧?
四、错误响应:给前端一条活路
错误响应这件事,做得好不好直接决定前端的开发体验。我见过太多接口返回这种鬼东西:
{
"code": 500,
"message": "服务器内部错误",
"data": null
}
500?服务器内部错误?这信息量等于零。前端同学看到这个只能对着屏幕发呆,连下手改的地方都没有。
我的错误响应结构:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "指定的用户不存在",
"details": [
{
"field": "user_id",
"message": "用户 ID 格式正确但数据库中不存在"
}
],
"trace_id": "a1b2c3d4e5f6"
}
}
code 是给程序看的,用大写下划线命名,客户端好做 switch-case。message 是给用户看的,可以是中文。details 字段放具体哪个字段出错及原因,方便前端精准渲染到表单上。trace_id 是链路追踪 ID,出问题时一搜就知道是哪台机器、哪个请求的锅。
五、分页:别一次性全量返回
「这个接口怎么这么慢?」「你猜它返回了多少数据?」「不知道,反正订单表有几千万条。」
全量返回这种操作,在测试环境看起来没问题,一上生产就是灾难。分页是必须的,但分页的实现方式也有讲究。
常见的分页方式有两种:offset 分页和cursor 分页(也叫游标分页)。
offset 分页:
GET /api/orders?page=1&page_size=20
简单粗暴,但有性能问题。数据库执行 OFFSET 10000 LIMIT 20 时,得先扫描前 10020 行然后丢弃前 10000 行,数据量大的时候慢得离谱。
cursor 分页:
GET /api/orders?cursor=eyJpZCI6MTAwMH0&limit=20
基于上一页最后一条的 ID 做条件查询,WHERE id > last_id LIMIT 20,不管翻到第几页,性能都稳定。但 cursor 分页不支持跳页,用户想直接跳到第 50 页?不好意思,做不到。
我的建议:数据量小用 offset,数据量大或列表有实时性要求用 cursor。比如订单列表、消息列表这种,用 cursor 分页体验反而更好——用户往下滑,订单一直在变,用 offset 分页翻到第 5 页可能已经看到过期订单了。
六、版本管理:向前兼容是基本礼仪
接口上线后不可能不改,但改了不能影响已有的调用方。版本管理就是干这个的。
两种常见方式:
// URL 路径版本(GitHub、Stripe 在用)
GET /api/v1/users
GET /api/v2/users
// Header 版本(AWS 在用)
GET /api/users
Accept: application/vnd.api+json; version=2
我更推荐 URL 路径版本,原因是看得见摸得着。调用方可以明确知道自己用的是哪个版本,出问题时好排查。Header 版本的好处是 URL 干净,但调试的时候得记着带 header,相对麻烦。
另外,接口废弃要给出明确的过渡期。Stripe 的做法值得学习:废弃接口会返回 Deprecation: true 响应头,并给出 Sunset 响应头说明具体废弃时间。调用方能看到预警,有充足时间迁移。
七、安全:别等被刷才知道后悔
API 安全是个大话题,说几个最容易忽略的点:
1. 限流要做得细
不只是限制总请求量,还要按接口分组。比如查询接口可以限流 100 次/分钟,但写入接口只能 10 次/分钟。防止有人疯狂调用写入接口把你的数据库打爆。
2. 敏感数据要脱敏
日志里打满手机号、身份证号的接口我见过不少。存储要加密,传输要 HTTPS,返回给前端之前要脱敏。手机号中间四位打星号,身份证号只显示前三位后四位,这是基本操作。
3. CORS 别配置成 *
开发阶段图省事 CORS 设成 *,上线也不改。这种我只能说胆子大。被恶意网站用你的 API 发起 CSRF 攻击,用户数据泄露了哭都来不及。
总结
API 设计这件事,说到底是在给自己和调用方省麻烦。一个设计得好的接口,调用方用起来顺手,自己维护起来也轻松。改需求、加字段、排查问题,都能从容应对。
记住几个核心原则:资源导向、状态码语义化、错误响应有价值、分页保平安、版本管理要规范、安全时刻记心间。做到这几点,不敢说接口设计得多优雅,至少不会被人背后骂。
接口是后端的脸面,且写且珍惜。