别把API设计成玄学:我踩过的10个设计大坑
做后端开发这些年,我写过不少API,也接别人的API更多。好的API设计大家都说好,但说不出好在哪;烂的API设计呢,你一眼就知道不对劲,但就是说不清楚。今天把我踩过的那些坑整理出来,给大家提个醒,少走弯路。
坑一:URL命名看心情,接口地址像乱码
见过最离谱的接口长这样:
/api/v1/getUserInfo?id=123
/api/v1/query_user_data_by_id
/api/v1/uinfo
同一个项目,三种风格。这还算好的,更有甚者:
/api/getChinaUserListNewFinal
/api/getChinasrInfo
你告诉我"chinasr"是什么?中国什么?
正确做法:统一命名规范,用名词复数,用连字符分隔:
GET /api/users # 用户列表
GET /api/users/{id} # 单个用户
POST /api/users # 创建用户
RESTful不是银弹,但至少让接口地址可预测。前端只要知道资源名,大概能猜出接口地址,这才是好的设计。
坑二:GET做写操作,POST做读操作
这种我也见过不少:
GET /api/deleteUser?id=123
GET /api/updateUser?id=456&name=test
问:这种接口有什么问题?
答:问题大了。GET请求会被浏览器缓存,会被日志记录,会被CDN缓存,可能被搜索引擎抓取。用GET做写操作,等于把数据库裸奔在网络上。
更骚的操作是:
POST /api/getUserInfo # 用POST做查询
Content-Type: application/x-www-form-urlencoded
id=123
这完全是反人类设计。HTTP语义清清楚楚,不要为了"安全"或者"避免缓存"去违反它。
坑三:HTTP状态码乱返回,200表示"服务器爆炸了"
见过最离谱的错误处理:
{
"code": 200,
"message": "服务器爆炸了,请稍后重试",
"data": null
}
code=200,但message说"服务器爆炸了"。前端判断code=200就认为成功,结果用户看到的是"成功"但数据是空的,体验稀碎。
正确做法:让HTTP状态码反映结果,body里的code作为业务状态码:
HTTP 200 OK
{
"code": 0,
"message": "success",
"data": {...}
}
HTTP 400 Bad Request
{
"code": 40001,
"message": "参数校验失败",
"errors": [...]
}
HTTP 500 Internal Server Error
{
"code": 50001,
"message": "服务不可用,请联系管理员",
"data": null
}
这样做的好处:HTTP状态码给基础设施(网关、负载均衡)看,业务状态码给前端看,各司其职。
坑四:返回字段不过滤,一个接口暴露整个数据库
用户列表接口,返回字段包括:password_hash、id_card、phone、email……这不是API,这是数据泄露通道。
我见过一个接口,返回了用户的:
{
"id": 123,
"username": "zhangsan",
"email": "zhangsan@example.com",
"phone": "13800138000",
"password": "e10adc3949ba59abbe56e057f20f883e", // !!!
"created_at": "...",
"updated_at": "...",
"last_login_ip": "...",
"last_login_time": "...",
"is_admin": true, // !!!
"password_changed_at": "...",
"security_question": "..." // !!!
}
这套数据要是流出去,就是一场数据安全事故。
正确做法:指定返回字段,敏感字段默认不返回:
GET /api/users?fields=id,username,email,created_at
GET /api/users/{id}?fields=id,username,email
或者用GraphQL,按需取字段,别一股脑全倒出来。
坑五:分页参数全靠猜,每家接口不一样
见过太多种分页方式:
page=1&size=20 # 方式1
offset=0&limit=20 # 方式2
start=0&count=20 # 方式3
pageNo=1&pageSize=20 # 方式4
cursor=abc123 # 方式5
更离谱的是,同一个项目里这几种混用。
统一分页规范,推荐用cursor-based分页,适合大数据量和高并发场景。如果数据量可控,page-based也可以,但要在文档里明确写清楚参数名和默认值。
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true,
"total": 1000
}
}
坑六:错误信息等于没说,前端看了想打人
错误信息最烂的写法:
{
"error": "操作失败"
}
操作失败?什么操作?为啥失败?是服务器炸了还是参数错了?前端看到这个错误信息,除了弹一个"操作失败"给用户,啥也干不了。
好的错误信息应该包含:
- 发生了什么问题(用户能看懂的描述)
- 为什么发生(可选,帮助用户自查)
- 怎么解决(可选,给用户指条路)
- 错误码(给技术支持/前端判断用)
{
"code": 40001,
"message": "手机号格式不正确",
"detail": "当前手机号 13800138000 长度或格式不符合中国大陆手机号规范",
"solution": "请输入11位数字,以1开头",
"request_id": "req_abc123" // 用于排查问题
}
坑七:不做版本管理,一升级全崩溃
很多项目一开始:
/api/users
然后有一天需要Breaking Change,加字段、改逻辑、删字段,怎么办?没有版本管理的话,只能:
- 新增字段,旧接口返回新字段 → 旧版前端解析报错
- 删字段 → 旧版前端用到了这个字段,直接崩溃
- 改字段类型 → 旧版前端类型判断直接失效
正确做法:从第一天就加版本:
/api/v1/users
/api/v2/users
新版本上线后,给旧版本留足过渡时间(比如6个月),逐步废弃。API版本不是矫情,是对自己和调用方负责。
坑八:认证鉴权一把梭,token满天飞
见过最随意的鉴权设计:
GET /api/users?token=abc123
GET /api/users?api_key=xyz789
Header: X-Token: abc123
三种方式在一个项目里同时存在,安全性参差不齐,token泄露了也不知道。
统一鉴权方案,推荐JWT:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
同时注意:
- token要设置过期时间,别搞永久token
- refresh_token和access_token分开
- 敏感操作二次验证(支付、修改密码等)
- 服务端做好token主动失效机制
坑九:忽略幂等性,重试一次灾难一次
接口没有幂等性保证,用户网络不好点了一下重试,结果:
POST /api/orders
# 第一次:创建订单,扣款200元
# 重试:又创建订单,又扣款200元
# 用户:???我买了一个还是两个???
正确做法:
1. 客户端生成唯一请求ID(UUID)
2. 服务端做幂等键(idempotency key)检查
3. 重复请求直接返回上次的结果
Header: Idempotency-Key: uuid-v4-string
Response:
{
"code": 0,
"data": {
"order_id": "order_123",
"status": "created"
},
"idempotent_replay": false // 标记是否是重复请求
}
POST类写操作接口,一定要有幂等性保证。这不是优化,是工程底线。
坑十:文档残缺不全,注释比代码还少
见过最精简的API文档:
/api/users - 获取用户列表
没了。没有参数,没有返回值,没有错误码,没有示例。这不是文档,这是标题。
好的API文档应该包含:
- 接口描述(这个接口干什么用)
- 请求参数(名称、类型、是否必填、默认值、说明)
- 响应格式(成功/失败的例子都要给)
- 错误码对照表
- 调用示例(curl、JavaScript、Python各来一个)
- 频率限制说明
如果文档写不清楚,说明你自己也没想清楚这个接口该怎么用。先想清楚,再动手写代码。
总结:好API是设计出来的,不是改出来的
API设计不是后端开发的附属品,是产品体验的一部分。前端调用方看你接口的体验,就像用户看UI的体验一样直观。
几个核心原则:
- 语义清晰:HTTP语义、URL命名、状态码,都要符合规范
- 安全第一:敏感字段不返回,认证鉴权要到位
- 可预测:接口命名一致,参数格式统一,不要让人猜
- 容错友好:错误信息有用,分页有规则,幂等有保证
- 文档先行:写代码之前先想接口设计,接口设计之前先写文档
API是你给调用方的承诺,一旦发布就是契约。改一次代价很大,所以在发布之前多想一想,把坑填平。
祝大家的API都能稳定运行,少踩坑,多划水。