做过独立开发的人都知道,最痛苦的不是从零开始写代码,而是接手一个别人写的、烂到无法维护的API项目。
我经历过三次API重写,不是为了炫技,是因为真的撑不下去了。今天不整那些"RESTful最佳实践"的虚的,就说说我踩过的坑,以及怎么避开它们。
教训一:URL里的版本号,毁掉了我第一个项目
当年年少轻狂,觉得RESTful嘛,/api/v1/users 很标准,升级就 /api/v2/users,多清晰。结果呢?
GET /api/v1/users/123
GET /api/v2/users/123
两个端点,两套逻辑,两个数据库查询。
三个月后,v1 还有人在用——因为某些老客户的App还没更新。你怎么办?继续维护两套代码?每次改一个字段要改两个地方?
后来我才明白:URL版本号是给人类看的,不是给机器看的。它最大的作用是让产品经理在发版说明里能写"全新升级的v2版本",显得很专业。至于技术实现?这只是把版本管理的复杂度从代码层搬到了URL层,并不会让问题消失。
真正的问题是:你什么时候该升级?升级的判断标准是什么?
我的经验是:只在有破坏性变更时才需要新版本。而破坏性变更应该尽量避免。怎么避免?往下看。
教训二:字段变更不是你想改就能改
API 里有个字段叫 name,产品说太土了,要改成 display_name。简单,搜索替换一下,上线。
然后灾难来了:
- 旧版App解析不了
display_name,显示空白 - 爬虫抓到的数据格式乱了
- 某个第三方合作方的系统直接报错了
你以为的"小改动",对外部来说可能是灾难。
正确的做法是:字段只增不改,类型只宽不窄。
如果要重命名一个字段?分三步走:
// 第一步:保留旧的,同时返回新的(两个字段都有值)
{
"name": "张三",
"display_name": "张三", // 新字段上线
}
// 第二步:等所有客户端都切换后,旧字段标记为 deprecated
{
"name": "张三", // @Deprecated 请迁移到 display_name
"display_name": "张三",
}
// 第三步:下一个主版本再删除旧字段
{
"display_name": "张三",
}
听起来很麻烦?但这是最小化线上事故的唯一方法。
教训三:错误处理的一致性,比你想象的更重要
我见过最离谱的API,错误返回是这样的:
// 情况1:用户不存在
{"error": "用户不存在"}
// 情况2:权限不足
{"message": "没有权限"}
// 情况3:服务器挂了
{"msg": "服务端异常"}
// 情况4:参数错误
{"info": "参数格式错误"}
四个错误,四种格式。调用方想统一处理?做梦。
我后来强制团队使用统一的错误格式:
{
"code": 10001,
"message": "用户不存在",
"request_id": "abc123",
"details": {}
}
code 是给程序用的,message 是给人类看的,request_id 是用来排查问题的。三者缺一不可。
还有一点:HTTP状态码和业务错误码是两回事。
很多新手喜欢用 200 然后在 body 里塞个 {"success": false}。这是错误的。HTTP 状态码 4xx 就是给客户端判断"我请求有问题"的,你用 200 糊弄过去,前端代码怎么写拦截?网关怎么做流量控制?
教训四:分页参数的设计,藏着大坑
最简单的分页是什么?
GET /users?page=1&size=20
很好,理解。但当你的数据量从一万条变成一亿条时,问题来了:OFFSET 分页是灾难。
-- 第1000页,每页20条
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 20000;
-- 数据库要扫描前20020条,然后丢弃20000条
数据量越大,OFFSET 越大,性能越差。这是 SQL 本身的限制,不是你的代码问题。
正确的做法是游标分页(Cursor-based Pagination):
// 第一页
GET /users?limit=20
// 返回
{
"data": [...],
"next_cursor": "eyJpZCI6MTAwMH0=", // 加密的最后一页最后一条的ID
"has_more": true
}
// 下一页
GET /users?limit=20&cursor=eyJpZCI6MTAwMH0=
每次只查"从游标往后20条",不管数据量多少,查询时间恒定。这是 Instagram、Twitter 这些大厂都在用的方案。
教训五:你的API有没有做"防腐层"?
这个词很多人可能没听过,但问题很常见:你的内部数据模型和外部API暴露的数据模型,是两个东西。
很多小团队直接把我数据库表结构暴露给外部:
{
"id": 1,
"user_id": 123,
"is_deleted": false,
"create_time": "2024-01-01 10:00:00",
"update_time": "2024-06-01 15:30:00"
}
然后某天你数据库字段改了,外部API也跟着变了,旧版本客户端全挂了。
防腐层(Anti-corruption Layer)的意思是:数据库怎么存是你的事,API怎么返回是我的事,中间加一层转换。
// 数据库模型
UserDB {
id, user_id, is_deleted, create_time, update_time
}
// API模型(分离)
UserDTO {
id, name, status, joined_at
}
// 转换逻辑
UserDTO.fromDB(UserDB) { ... }
这样做的好处是什么?你的数据库加字段、拆表、换存储引擎,API不用改。外部调用方永远看不到你内部的复杂性。
写在最后
API 设计没有银弹,但有一些可以让你少踩坑的原则:
- 字段只增不改,类型只宽不窄
- 错误格式统一,状态码正确使用
- 分页用游标,别用 OFFSET
- 内外模型分离,加上防腐层
- 版本号是最后手段,不是首选方案
最重要的一点:设计API的时候,要假设你的调用方是"熊孩子"——他们会用你想不到的方式调用,会传你想不到的数据,会在你意想不到的时候升级或降级。
你能做的,就是让API足够健壮,足够宽容,足够稳定。
毕竟,好的API是沉默的——它只在出错的时候才开口,而出错的时候,它说的话你一定要能听懂。