事情是这样的。
产品经理神秘兮兮地把我叫到角落:有个紧急需求,接入一个第三方系统,对接文档在这,你看看。
我打开文档,第一眼就看到了 /getUserInfo、/get_user_data、/userGet 三个接口并列排放。
我的血压瞬间就上来了。
一、命名这件事,能看出一个人的修为
很多人写接口,命名全凭心情。今天想用驼峰,明天觉得下划线顺眼,后天又觉得RESTful不错但懒得改旧接口。
结果就是一个系统里同时存在:
/getUser
/user_get_info
/User.GetInfo
/GetUserInfo_v2_final
我不是说一定要强求RESTful规范——说实话,那套规矩有时候确实过度设计。但你好歹保持一致吧?
我见过最离谱的接口命名是:按中文拼音首字母缩写。 /getYhxx 是“获取用户信息“的意思。我tm怎么知道?
给后来人留条活路,用清晰的英文命名。如果非要用缩写,请务必配套一份 glossary,不然这个系统三年后就是个没人敢动的屎山。
二、HTTP状态码:你是不是只会返回200和500?
见过太多后端同学,接口处理逻辑就三行:
try {
// 业务逻辑
return 200;
} catch (Exception e) {
return 500;
}
兄弟,你是写API还是写Hello World?
一个合格的RESTful API,至少要会用这些状态码:
- 200 - 成功,但别什么都返回200
- 201 - 资源创建成功,POST请求新建了东西用这个
- 400 - 请求参数有问题,不是后端的问题
- 401 - 没登录,别假装没这个状态
- 403 - 登录了但没权限
- 404 - 资源不存在,不是“查询结果为空“的意思
- 409 - 冲突了,比如重复创建
- 422 - 请求格式对,但语义不对
- 429 - 请求太频繁了,限流了
- 500 - 真的出问题了再返回这个
很多人喜欢在业务层面自己定义错误码,什么 {"code": 10001, "msg": "用户不存在"} 。
不是说不行,但如果你同时返回HTTP状态码和业务错误码,请确保它们逻辑一致。最怕的是接口返回200但业务code是error,前端一顿操作发现是假成功。
三、分页这件小事,能折磨死人
当接口返回的数据量从10条变成10000条的时候,分页就是必选项。
但你见过多少种分页方式?
我见过:
// 方式1:offset + limit
GET /users?offset=20\u0026limit=10
// 方式2:page + page_size
GET /users?page=3\u0026page_size=10
// 方式3:since_id + limit(游标分页)
GET /users?since_id=12345\u0026limit=10
// 方式4:start + end(范围)
GET /users?start=20\u0026end=30
// 方式5:cursor + size
GET /users?cursor=abc\u0026size=10
// 方式6:直接在body里传参数
POST /users/query
{"page": 3, "pageSize": 10, "offset": 20}
更绝的是,同一个系统里,五种方式同时存在,各管各的。
选哪种?我的建议是:能上游标分页就别用offset。
为什么?当你在翻页的时候,数据可能被插入了新记录,offset就会跳行。比如你第一页查了0-10,id是1-10;第二页offset=10,你以为会拿到11-20,但如果中间插了一条id=5的新记录,你实际拿到的是6,7,8,9,10,11,12,13,14,15——id=5和id=16的数据就这么没了。
游标分页(基于id或时间戳)就没有这个问题,性能也更好。缺点是没法跳页,只能“下一页“——但实际上你的用户有多少时候需要直接跳到第50页?
四、RESTful那套规范,到底要不要follow?
这个问题我问过很多人,答案都不一样。
我的态度是:别教条,但别太离谱。
理想情况下RESTful API应该是:
GET /users # 获取用户列表
GET /users/123 # 获取ID为123的用户
POST /users # 创建用户
PUT /users/123 # 完整更新用户
PATCH /users/123 # 部分更新用户
DELETE /users/123 # 删除用户
简洁、清晰、有规律。这是理想。
现实是什么?你的用户表有几百个字段,你的业务逻辑有几十种操作,你的前端需要各种组合查询……严格按RESTful来有时候反而麻烦。
所以我的折中方案是:
- 资源操作尽量RESTful,这是与标准对齐
- 复杂的业务动作可以用
POST /users/{id}/action这种方式 - 查询参数尽量用约定俗成的:
sort、filter、fields、include - 不要在一个接口里既做查询又做更新又做删除,请放过前端同学
五、版本管理:没有版本号的API是耍流氓
当你需要做breaking change的时候,你就知道版本号有多重要了。
常见版本号写法:
// URL版本(最常见)
GET /api/v1/users
GET /api/v2/users
// Header版本
GET /api/users
Accept: application/vnd.api+json; version=2
URL版本最直观,缺点是改URL有点丑。Header版本更RESTful,但调试起来不方便。
我的建议:除非你有洁癖,否则就用URL版本。简单粗暴,前端一看就知道在调哪个版本。
更重要的是:旧版本要有明确的deprecated时间和下线计划。最讨厌的是v1跑了三年没人维护,突然有一天要下线,前端炸锅。
提前告知、给迁移时间、留足buffer,这是做接口向后兼容的基本礼仪。
六、文档这事,你敷衍它它就敷衍你
最后聊个老生常谈但很多人依然做不好的事:接口文档。
我见过最敷衍的文档是这样写的:
接口:getUserInfo
参数:userId
返回:用户信息
我信你个鬼。
一个合格的接口文档至少要包含:
- 接口用途的明确描述
- 每个参数的:名称、类型、是否必填、取值范围、含义
- 返回值的完整结构,包括每个字段的含义
- 可能的错误码和对应的业务场景
- 请求示例和响应示例
- 如果涉及权限,说明需要什么权限
很多人说”Swagger/OpenAPI自动生成不就行了?“——工具生成的文档能告诉你业务含义吗?能告诉你这个接口的调用频率限制吗?能告诉你这个接口和那个接口的调用顺序依赖吗?
工具是辅助,人的思考和描述才是核心。
写在最后
回到开头那个故事。
我最后还是硬着头皮接完了那个第三方接口。前后端联调的时候,果然出现了各种幺蛾子:状态码乱返回、分页参数不统一、文档写的一套实际是另一套……
我在凌晨三点的工位上,一边骂骂咧咧一边改代码,心想下次一定要在接口设计阶段就立好规矩。
可惜现实是,下次还是会有那种神秘兮兮的产品经理走过来,说”有个紧急需求……“
写接口不难,写好接口难。命名规范、状态码正确、分页合理、版本清晰、文档完整——这些道理谁都懂,但真正做到的,十个后端里能有两个就不错了。
如果你身边有那种接口写得特别烂的后端同学,请把这篇文章转发给他。救人一命,胜造七级浮屠。
祝大家少踩坑,多摸鱼,下班早回家。