写API这事儿,别再瞎摸索了:几个血泪教训换来的设计原则
干了这么多年后端,我见过太多API设计得一塌糊涂的项目。不是不能用,是用起来让人想骂人。有些API返回的数据结构,一个接口能给你甩出来七八种不同格式,调试的时候你都不知道该哭还是该笑。
今天不整虚的,直接说几个我认为最重要的API设计原则。这些都是我踩过坑、填过坑之后总结出来的血泪经验。
1. URL设计:名词是主角,动词滚远点
先说个最常见的毛病——把URL写成动宾短语。比如:
GET /getUserInfo
POST /createNewOrder
PUT /updateUserData
看到这种URL,我血压都上来了。RESTful API的核心是什么?是资源。URL应该是名词的集合,表示你操作的对象,而不是动作本身。为啥?因为动作已经体现在HTTP方法里了。
正确姿势:
GET /users/123
POST /orders
PUT /users/123
DELETE /users/123
简单、清晰、一目了然。一个新人接过来,看一眼URL就知道他在操作什么资源。
还有个小问题——复数还是单数?我的建议是全程用复数。/users 而不是 /user,/orders 而不是 /order。这样有个好处:不管你是获取列表还是操作单个资源,格式都很统一:/users 是列表,/users/123 是单个。不会乱。
2. 状态码:别总返回200然后在body里写error
这是我觉得最恶心的设计之一:接口永远返回200,但body里有个 "status": "error" 字段。客户端得先判断status字段才知道有没有出错。
拜托,HTTP本身就有完善的状态码体系,你不用白不用?
200 OK - 成功
201 Created - 创建成功
204 No Content - 删除成功
400 Bad Request - 客户端参数错误
401 Unauthorized - 没认证
403 Forbidden - 没权限
404 Not Found - 资源不存在
422 Unprocessable Entity - 验证失败
429 Too Many Requests - 请求过于频繁
500 Internal Server Error - 服务器抽风了
每个状态码都有它存在的意义。用对了,前端工程师会感激你的——他们可以直接根据HTTP状态码做全局的错误处理,不用每个接口都写一套判断逻辑。
对了,有个容易忽略的:分页请求如果没有更多数据了,返回什么?我的建议是返回空数组 [] 而不是404。404在语义上表示"这个资源不存在",但分页返回空只是说明"没了",不是"不存在"。
3. 统一响应结构:给自己和前端留条活路
我见过最离谱的项目,五个接口返回五种不同的数据结构:
// 接口A
{"data": {...}}
// 接口B
{"result": {...}, "code": 0}
// 接口C
{...}
// 接口D
{"status": "success", "payload": {...}}
// 接口E
{"success": true, "data": {...}, "message": "操作成功"}
这种项目维护起来简直是噩梦。前端每接一个接口都得问:"这个data是嵌套在哪一层的来着?"
我的建议是:整个项目统一一种响应结构,推广到所有接口:
{
"code": 0,
"message": "操作成功",
"data": {...}
}
或者:
{
"success": true,
"data": {...},
"error": null
}
选哪种都行,关键是要统一。团队里定好规范,所有人遵守。Code Review的时候把这当硬性标准,不符合的一律打回。
有个细节:分页列表的响应也要统一格式。建议:
{
"code": 0,
"data": {
"list": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 100,
"totalPages": 5
}
}
}
别让前端猜数据在哪个字段里。
4. 版本管理:别等到要改接口了才想起来
很多项目初期没有版本意识,接口直接挂在 /api/xxx 下面。等业务发展了需要改接口字段,发现线上的老版本还在跑,兼容问题头都大。
我的建议是第一版就加上版本号:
/api/v1/users
/api/v2/users
什么时候该升版本?有个简单原则:
- 改了字段名称 → 升版本
- 删了某个返回字段 → 升版本
- 改了字段类型 → 升版本
- 新增可选字段 → 不需要
- 新增必填字段 → 需要升版本,但老版本要兼容一段时间
版本之间要有过渡期。不是说v2上线了v1立刻停掉,而是让调用方有时间迁移。一般我会建议:v1至少再维护6个月,给足迁移时间。
5. 参数校验:把错误拦截在门口
有个特别不好的习惯:接口参数不校验,直接往数据库里塞。然后在业务逻辑里一层层判断,发现不对了再返回错误。
这样做有几个问题:一是业务代码里充斥着各种参数校验逻辑,混乱;二是返回的错误信息不友好,用户看到的是"操作失败"而不是"邮箱格式不正确";三是容易被恶意请求攻击。
正确做法是:参数校验前置。主流框架都有成熟的校验库:Java的Hibernate Validator,Python的Pydantic,Go的go-playground/validator。用起来。
// 以一个注册接口为例
{
"username": "required, min=3, max=20, alphanumeric",
"email": "required, email",
"password": "required, min=8, regex=^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)"
}
校验失败的返回也要友好:
{
"code": 400,
"message": "参数校验失败",
"errors": [
{"field": "email", "message": "邮箱格式不正确"},
{"field": "password", "message": "密码必须包含大小写字母和数字"}
]
}
让调用方知道具体哪里错了、应该怎么改。
6. 幂等性:这玩意儿平时不注意,出事了要命
什么是幂等性?就是同一个请求你执行一次和执行多次,效果是一样的。GET不用说天然幂等,但POST、PUT、DELETE呢?
- POST:创建资源,每次调用应该产生不同的资源(比如订单),不是幂等的
- PUT:更新资源,用唯一标识更新固定字段,是幂等的
- DELETE:删除资源,删除一次和删除多次效果一样(资源没了),是幂等的
但实际场景更复杂。比如微信支付回调,微信可能会多次推送同一笔支付成功的通知。如果你没有做幂等处理,同一笔订单可能被处理多次,后果就不用我说了吧。
常见的幂等处理方案:
- 唯一键:数据库里对关键字段加唯一索引,重复插入会报错
- 幂等Token:前端生成一个唯一token,后端存储处理过的token,避免重复处理
- 乐观锁:UPDATE时带版本号或时间戳,只有版本匹配才能更新成功
我的经验是:所有涉及资金、库存、外部状态变更的接口,务必做幂等处理。别到时候对账发现多了几笔钱,再去查是哪个接口没做幂等,那才叫欲哭无泪。
写在最后
API设计这事儿,说难不难,说简单也不简单。核心就几点:语义清晰、结构统一、错误友好、考虑扩展。
但偏偏就是这些"简单"的东西,很多人做不好。要么是赶进度草草了事,要么是压根没这个意识。等项目做大了、接的第三方多了,改起来代价就大了。
我的建议是:团队里定好API设计规范,写进新人上手文档里。Code Review的时候把API设计合理性当重点review项。一开始多花点时间,后面的维护成本会低很多。
好的API设计,就像好的代码注释——不是为了炫技,是为了让人(包括未来的自己)少踩坑。共勉。