前言
写API这件事,看起来简单,不就是把数据扔出去吗?但当你真正面对生产环境,面对各种奇怪的调用方行为,面对半夜的告警,你才会意识到:API设计烂,后端火葬场。
今天不聊理论,就聊实战。我踩过的坑,比你写过的CRUD还多。
一、HTTP方法用对了吗?
见过太多人所有请求都走POST,问就是"方便"。兄弟,你是在设计API,不是在写漏洞利用代码。
HTTP方法不是摆设:
- GET:读取资源,禁止修改任何东西
- POST:创建资源
- PUT:全量更新(传完整对象)
- PATCH:部分更新(只传要改的字段)
- DELETE:删除资源
有个经典错误:有人用POST做删除操作,美其名曰"安全"。结果呢?搜索引擎爬虫没事爬一下,线上数据就没了。你说这是爬虫的错?不,这是你API设计的错。
// 正确示范
DELETE /users/123 // 删除用户
GET /users/123 // 获取用户信息
PATCH /users/123 // 更新部分字段
// 错误示范——所有操作都是POST
POST /deleteUser?id=123
POST /getUserInfo
POST /updateUser
二、状态码:别总返回200然后在body里塞error
这是国内最常见的烂设计:HTTP状态码永远是200,但body里写着{"code": 500, "message": "服务器错误"}。你是在逗我?
HTTP状态码是有意义的,用它们:
- 200:成功
- 201:资源创建成功
- 400:客户端请求有问题(比如参数校验失败)
- 401:未认证
- 403:已认证但没权限
- 404:资源不存在
- 500:服务器内部错误
有人说:"返回错误状态码,前端不好处理啊。"前端不好处理是前端的问题,不是你乱用状态码的理由。
// 好的设计:正确的HTTP状态码
HTTP/1.1 400 Bad Request
{"code": 40001, "message": "手机号格式不正确", "field": "phone"}
// 烂的设计:永远200
HTTP/1.1 200 OK
{"code": -1, "message": "手机号格式不正确"}
三、分页:无限滚动的代价
很多人在设计列表接口的时候,习惯用offset+limit,但当数据量大的时候,你就知道什么叫灾难。
// 经典烂设计
GET /articles?page=1\&page_size=100
// 问题在哪?
// 第1000页 = OFFSET 9900 LIMIT 100,数据库要扫描9900行再返回100行
// 数据有新增删除时,翻页会看到重复或遗漏数据
更好的方案是游标分页(Cursor Pagination):
// 基于游标的分页
GET /articles?cursor=eyJpZCI6MTIzfQ\&limit=20
// 返回
{
"data": [...],
"next_cursor": "eyJpZCI6MTAzfQ",
"has_more": true
}
// 优点:
// 1. 不管翻到第几页,性能一致
// 2. 数据有变化不会重复或遗漏
// 3. 适合移动端无限滚动场景
当然,游标分页也有代价:无法跳转到任意页。这个要根据自己的业务场景取舍。
四、版本管理:你的API升级策略是什么?
总有一天,你需要breaking change。但上线第一天就把现有接口改了?调用方会提着刀来找你。
常见的版本管理策略:
// URL版本(GitHub、Stripe在用)
GET /v1/users
GET /v2/users
// Header版本
GET /users
API-Version: 2023-01-01
// Query参数版本(不推荐)
GET /users?version=2
我的建议是:URL版本最直观,但也最容易被用户骂(因为看起来不优雅)。Header版本更符合REST精神,但调试不方便。
不管用哪种,关键是:旧版本要有足够的生命周期,给调用方足够的时间迁移。我见过有人上线新版本,两周后就下掉了旧版本,然后被客户投诉到工信部。
五、幂等性:你考虑过吗?
网络是不稳定的。超时、重试、用户手抖多点了一下——你的API能被调用多次而不产生副作用吗?
这些操作必须是幂等的:
- GET:天然幂等
- PUT:多次调用结果一致
- DELETE:删除已删除的资源应该返回成功
- POST:通常不是幂等的,但可以用唯一token解决
一个实战技巧:对于POST类创建操作,让客户端生成唯一ID(可以是UUID),服务端做去重。
// 客户端生成唯一ID
POST /orders
X-Idempotency-Key: a3f1c2d4-e5b6-7890-abcd-ef1234567890
// 服务端存储已处理的key,超时自动清理
if (idempotencyStore.has(key)) {
return idempotencyStore.get(key) // 返回缓存结果
}
idempotencyStore.set(key, result)
return result
六、字段命名:别用拼音,别用缩写
这是我见过最离谱的问题:
// 来自某国内项目的API
{
"yonghu_xingming": "张三",
"shijian": "2024-01-01",
"je": 1000
}
// je是什么?金额?积分?
// shijian是什么?创建时间?修改时间?
用英文,用完整的英文。遵循业界通用命名:
- user_name 或 userName(看你团队风格统一)
- created_at
- updated_at
- amount 或 total_amount
还有一个坑:时间字段必须有时区信息,或者统一用UTC。北京时间下午3点,在UTC是早上7点。如果你存的是本地时间,某天服务器迁移到国外,时区问题会让你怀疑人生。
结语
好的API设计,是让调用方用起来舒服,自己改起来也不痛苦的设计。它不是炫技,是克制——克制地使用功能,克制地返回数据,克制地在breaking change之前先发邮件通知。
记住:你的API是给别人用的,不是给自己炫技的。把用户当傻子一样设计接口,用户会用脚投票。
以上,踩坑完毕,欢迎补充。