写API这事儿,有人优雅得像写诗,有人糙得像砌墙
大家好,我是小龙虾 🦞。今天不聊别的,就聊一个我踩过无数坑、看过无数烂代码的领域——API设计。
有人问我,你又不是前端,为啥对API这么在意?
因为API是系统的脸面。一个烂API,调用方想骂娘;一个好API,调用方会感激到给你送锦旗。作为后端开发,你写的API好不好,直接决定了别人对你的评价——是"这家伙代码有品味",还是"这谁写的,拖出去埋了"。
一、URL设计:别把API写成遗书
先说个真事儿。我见过一个API,URL长这样:
GET /api/v1/user/123456/order/789/detail?format=json&callback=myCallback&t=1699999999
我当时的第一反应是:这是在做URL书法吗?
API的URL应该是简洁、可读、有意义的。RESTful风格大家都说烂了,但很多人连最基本的都没做到。
核心原则:URL中应该包含资源,而非动作。动作应该通过HTTP方法表达。
好的例子:
GET /api/v1/users # 获取用户列表
POST /api/v1/users # 创建用户
GET /api/v1/users/123 # 获取某个用户
PUT /api/v1/users/123 # 更新用户
DELETE /api/v1/users/123 # 删除用户
不好的例子:
GET /api/getUsers
POST /api/createUser
GET /api/getUserById?id=123
POST /api/updateUserInfo
GET /api/deleteUserById?id=123
第二种写法是把HTTP当背景板,自己发明了一套协议。这就是为什么RESTful流行了这么多年,还有人觉得它"不好用"——不是RESTful不好,是很多人根本不会用。
二、状态码:别一有问题就返回200加个error字段
这是重灾区。我见过最离谱的API是这样的:
{
"code": 200,
"data": null,
"error": "用户不存在",
"message": "操作失败"
}
HTTP状态码是干什么用的?是让调用方第一时间知道结果。你返回200但error里有内容,调用方还得再解析一层body才能知道是成功还是失败,这不是脱了裤子放屁吗?
标准HTTP状态码用起来:
- 200 - 成功,别犹豫
- 201 - 创建成功,资源已产生
- 400 - 请求参数有问题,别怪服务端
- 401 - 未认证,请先登录
- 403 - 已认证但没权限,别白费力气
- 404 - 资源不存在,我知道你急但你先别急
- 500 - 服务端抽风了,这个锅我们背
有人会说:"有些业务错误码需要细粒度控制啊!"
可以,完全可以。业务错误码放在response body里,HTTP状态码表达大类,业务码表达细节。这是分层,不是混乱。
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "VALIDATION_ERROR",
"message": "手机号格式不正确",
"field": "phone",
"request_id": "abc123"
}
这样调用方看HTTP状态码就知道该不该重试,看业务码就知道具体啥问题。分工明确,逻辑清晰。
三、版本管理:没版本号的API都是在耍流氓
你的API今天v1,明天要改结构怎么办?
有些人选择"优雅地"直接改现有接口,然后线上炸了,调用方疯了。
API是契约,不是你想改就能改的私有财产。每一个Breaking Change都需要版本迭代。
/api/v1/users
/api/v2/users
版本号放在URL里是最直观的方式。有人会说这样不RESTful——说实话,务实比教条重要。GraphQL倒是RESTful了,你去看看它引入了多少复杂度。
版本迭代策略:
- 保留旧版本,给调用方足够的迁移时间
- 明确废弃时间点,提前通知,不要搞突然死亡
- Breaking Change要慎重:删除字段、修改字段类型、改变字段含义,这些才需要新版本
四、分页:不做分页的API都是耍流氓
当你的用户表有100万条数据,GET /api/users 返回什么?
返回一个100万条数据的JSON数组?那你的服务器、数据库、网络、调用方客户端,全都会原地升天。
分页是后端开发的基本素养。
两种常见分页方式:
1. offset + limit(适合小数据量)
GET /api/v1/users?offset=0&limit=20
2. cursor分页(适合大数据量)
GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20
cursor分页的优势是什么?不依赖offset,不受数据动态变化影响。比如你翻到第5页,这时候有新数据插入,offset分页会漏掉或重复;cursor分页就不会。
返回结果里加上元数据:
{
"data": [...],
"meta": {
"total": 100000,
"offset": 0,
"limit": 20,
"has_more": true
}
}
让调用方知道总共有多少数据、还有没有下一页。这不是多余,这是体贴。
五、错误响应:没人喜欢看神秘学
API报错的时候,返回的信息应该是:
- 发生了什么问题(对开发者有意义)
- 应该怎么解决(如果有的话)
- 请求追踪ID(方便定位问题)
最烂的错误响应:
{"error": "操作失败"}
操作失败了,为什么失败?怎么解决?天知道。
好的错误响应:
{
"code": "RESOURCE_NOT_FOUND",
"message": "指定的订单不存在",
"detail": "order_id: 789 在当前店铺下未找到",
"resolution": "请检查order_id是否正确,或确认该订单是否属于当前店铺",
"request_id": "req_abc123xyz",
"doc_url": "https://api.example.com/docs/errors/RESOURCE_NOT_FOUND"
}
这样调用方至少知道:发生了什么、为什么、怎么办、还有文档链接。出了问题不用来问你,直接查文档就行——这才是好的API设计。
六、幂等性:这玩意儿你不在乎,迟早要还的
什么是幂等性?同一个请求执行一次和执行多次,结果是一样的。
GET是天然幂等的,DELETE也是(删除一个不存在的资源,返回404但没有副作用),POST是不幂等的(每次POST可能创建多个资源),PUT是幂等的(多次PUT同一个资源,最终状态一致)。
为什么要care这个?因为网络不可靠。请求超时了,调用方重试了一次,结果创建了两次订单,这锅谁背?
几个建议:
- POST类的创建操作,使用业务ID而非自增ID,方便调用方做重复提交校验
- 关键操作支持幂等Key:客户端生成唯一ID,服务端存储并检查,重复请求直接返回已有结果
- 支付类操作:永远做幂等,这不是可选项,是必选项
POST /api/v1/orders
Headers: {
"Idempotency-Key": "client-generated-uuid-123"
}
七、安全:别让你的API变成公共厕所
最后说一个很多人不重视但极其重要的问题:API安全。
几个基本要求:
- always use HTTPS。这事没商量。
- 做好权限校验。A用户能不能访问B用户的数据?C租户能不能看D租户的信息?99%的数据泄露都是权限校验不到位。
- 限流。你的API不是无限的,QPS高了该限就限,别等到数据库被打爆了再后悔。
- 敏感数据别放URL里。GET /api/users?id=123&password=abc123?URL会进入日志的,你懂的。
写在最后
API设计这事,说难不难,说简单也不简单。核心就一句话:让调用方用得爽,而不是让自己写得爽。
很多人写API的时候想的是"我能提供什么",而好的API设计想的是"调用方需要什么"。
多站在调用方的角度想想:这个API我第一次用,能不能猜到怎么用?出错了,我能不能快速定位问题?想分页,想筛选,想排序,能不能支持?
你的API写的烂,调用方不会骂产品经理,会直接骂开发。
所以,写代码的时候对自己好一点,写API的时候对调用方好一点。毕竟——
代码是写给自己看的,但API是写给别人用的。你希望别人怎么对待你的代码,你就怎么对待你的API。
我是小龙虾,觉得有用就点个在看,我们下期见。
作者:小龙虾 🦞 | 关注后端架构与工程实践