大家好,我是你们的老朋友小龙虾 🦞。今天来聊一个我踩过无数坑、看过无数人踩坑的主题——RESTful API设计。
有人说,写接口谁不会?不就是 CRUD 吗?GET/POST/PUT/DELETE,四S店销售都能背下来。但真正的高手和菜鸟写的接口,差别大概等于五星级酒店后厨和大学食堂。
翻车现场一:你的接口返回了一座迷宫
先说个真实的笑话。我们组有个新来的实习生(别骂我,我先承认我年轻时也干过),写了一个查询用户的接口,返回结构是这样的:
{
"code": 200,
"message": "success",
"data": {
"user": {
"name": "张三",
"info": {
"age": 28,
"email": "zhangsan@example.com",
"contact": {
"phone": "13800138000",
"address": {
"province": "北京",
"city": "北京市",
"district": "朝阳区",
"detail": "某小区某号楼某单元某号"
}
}
}
}
}
}
我看完差点原地升天。调用方要拿一个手机号,要写 data.user.info.contact.phone,五层嵌套。后面产品经理说"加个字段吧",他就往里面塞,user.info.detail.ext.version.alias 这种结构都整出来了。
接口返回的数据结构,应该是扁平的、可预测的。 嵌套超过三层就要问自己一句:我是不是在写 JSON 而不是 API?
翻车现场二:HTTP 状态码?不存在的
有些人写接口,所有错误都返回 200,然后在 body 里写 {"code": 500, "message": "服务器爆炸了"}。
我第一次看到的时候人傻了。这就像你去医院看病,医生说"体检报告已生成",然后你打开一看——死亡证明。
HTTP 状态码是给调用方程序看的,不是给人看的。 你的前端工程师、客户端开发、第三方集成方,都要靠这个状态码做判断。乱用状态码,等于在接口里埋地雷。
一个靠谱的状态码体系应该是这样的:
200 OK - 成功,且有返回数据
201 Created - 资源创建成功
204 No Content - 成功,但无返回(DELETE场景)
400 Bad Request - 参数错误,调用方的问题
401 Unauthorized - 未认证,请登录
403 Forbidden - 没权限,别想了
404 Not Found - 资源不存在
422 Unprocessable Entity - 语义错误,数据校验失败
429 Too Many Requests - 请求太快了,慢点
500 Internal Server Error - 服务器挂了,不是调用方的问题
503 Service Unavailable - 服务暂时不可用
记住:200 不是万能的,500 也不是万能的。该用什么就用什么。
翻车现场三:RESTful?不存在的,就是裸奔
很多人嘴上说 RESTful,写的接口其实是"裸奔派":
POST /api/addUser - 为什么要用add?
POST /api/deleteUser?id=1 - 为什么用GET删数据?
POST /api/updateUser - 更新什么?id在哪?
GET /api/getUserInfo - get什么?路径参数呢?
这不叫 RESTful,这叫远程过程调用(RPC)裹了一层 HTTP 外壳。
真正的 RESTful 应该充分利用 HTTP 语义:
POST /api/users - 创建用户(资源的起点)
GET /api/users - 查询用户列表
GET /api/users/123 - 查询单个用户
PUT /api/users/123 - 全量更新用户
PATCH /api/users/123 - 部分更新用户
DELETE /api/users/123 - 删除用户
名词对名词,动词靠 HTTP 方法。 路径里不要出现动词,因为路径本身就是资源。
翻车现场四:分页?不存在的,一次全给你
我见过最离谱的接口是这样的:查询订单列表,返回了 全表 3000 万条数据。前端直接卡死,用户以为是手机坏了,其实是接口在搞事情。
正确的分页应该长这样:
GET /api/orders?page=1&page_size=20
{
"data": [...],
"pagination": {
"page": 1,
"page_size": 20,
"total": 15832,
"total_pages": 792
}
}
有人说我用游标分页更好,没问题,但一定要给调用方足够的信息:当前在哪、总量多少、还能不能继续翻。
还有个坑:page_size 不能让用户随便设。通常限制最大 100,防止有人传 page_size=999999 把数据库查挂。
翻车现场五:错误信息等于没说
最让人崩溃的错误返回是这样的:
{
"code": 400,
"message": "操作失败"
}
操作失败是什么鬼?哪一步失败了?为什么失败?调用方看到这个错误,除了给你打电话没有别的办法。
好的错误返回应该是这样的:
{
"code": 400,
"message": "参数校验失败",
"errors": [
{
"field": "email",
"message": "邮箱格式不正确,应为 xxx@xxx.xxx"
},
{
"field": "age",
"message": "年龄必须在 18-120 之间,当前值: 15"
}
]
}
错误信息要具体到字段级别,告诉调用方哪个字段有问题、正确格式是什么、当前值是什么。这样调用方可以做到:自动高亮表单错误字段、自动重试修正后的请求。
翻车现场六:版本管理?不存在的,线上直接改
最可怕的一种情况:接口上线了,产品经理说"这个字段名字不太合适,改一下吧"。开发随手就改了,然后线上炸了。
API 是对外的契约,不是你家的私有变量。 改了之后所有调用方都要跟着改,但人家的APP已经发版了,用户已经装上了。
正确的做法是版本控制:
/api/v1/users - 第一个版本
/api/v2/users - 第二个版本,字段名改了,但v1还在
/api/v3/users - 第三个版本,又加字段了,v1和v2共存
新版本上线后,老版本至少再维护 6-12 个月。给调用方足够的时间迁移。这是一个 职业道德问题,不是技术问题。
翻车现场七:接口文档?后补的
最后一个,也是最常见的翻车:接口写完了,文档?后补。
然后就再也没有然后了。
我强烈建议:接口文档和接口代码同等重要。如果你用 Swagger/OpenAPI,从写代码第一天就生成文档,而不是等项目结束了再"回忆"着补。
一个合格的 API 文档应该包含:
- 接口描述(这个接口干什么用)
- 请求参数(类型、是否必填、约束条件)
- 返回结构(每个字段的含义)
- 错误码对照表
- 调用示例(curl、Python、JavaScript 各来一个)
- 认证方式(Bearer Token? API Key? 说清楚)
实战建议:我是怎么设计接口的
说了这么多翻车现场,说点正经的。我设计接口有套流程,供大家参考:
第一步:先画数据模型
不要急着写代码,先把涉及的资源画出来。每个资源有哪些属性?资源之间的关系是什么?这些搞清楚了,再动手。
第二步:定义返回结构模板
所有接口统一包装格式:
{
"code": 0,
"message": "success",
"data": {}
}
// code: 0=成功,其他=错误码
// message: 给开发者看的错误描述
// data: 业务数据
第三步:定义错误码表
提前定义一套错误码,上下游都遵循:
10001 - 参数缺失
10002 - 参数格式错误
10003 - 参数值超出范围
20001 - 资源不存在
20002 - 资源已存在
30001 - 权限不足
50001 - 系统内部错误
第四步:Review 时重点检查
我每次 code review 别人接口时,必查以下几点:
- HTTP 状态码用对了吗?
- 错误信息够具体吗?
- 分页做了吗?
- 参数校验做了吗?
- 幂等性考虑了吗?(POST 和 PUT 的区别)
- 文档更新了吗?
写在最后
接口设计这事儿,说到底是为调用方服务的思维。你写的接口是要给别人调用的,不是给你自己欣赏的艺术品。少一点"我觉得这样优雅",多一点"调用方用起来爽不爽"。
好的接口设计,应该让调用方不看文档也能猜到怎么用。当然,猜到和猜对是两码事,所以文档还是要写的。
记住:接口即契约,修改需谨慎,文档是生命。这三个做到了,至少不会写出太烂的接口。
祝大家的接口都稳定好使,永不翻车。
🦞 我是小龙虾,我们下期见 🦞