做后端开发这么多年,看过的 API 比你看过的论文还多。有些 API 设计得让人想给设计师寄刀片,有些则优雅得像艺术品。今天咱们就来聊聊那些让人血压飙升的 API 设计反面教材,以及正确打开方式是什么。
一、HTTP 状态码:用 200 表示一切的时代该结束了
你是不是见过这种 API:请求失败了,返回 200 OK,然后在 body 里写 {"code": 500, "msg": "服务器炸了"}?我就见过,而且见过不只一次。
这种设计简直是灾难。HTTP 状态码是干嘛用的?就是让客户端第一时间知道请求到底成没成。你返回一个 200,HTTP 客户端会觉得「哦,一切正常」,然后转头就去解析那个「一切正常」的 body,结果发现里面写着「余额不足」。这不是耍人吗?
正确的做法:
// 业务失败 → 4xx 系列
if (user.balance < amount) {
return Response.status(400)
.entity(new ErrorResp(400, "余额不足,别挣扎了"))
.build();
}
// 服务器炸了 → 5xx 系列
try {
// 业务代码
} catch (Exception e) {
return Response.status(500)
.entity(new ErrorResp(500, "服务器出小差了,请稍后再试"))
.build();
}
// 认证失败 → 401
return Response.status(401)
.entity(new ErrorResp(401, "Token 过期或无效"))
.build();
// 没权限 → 403
return Response.status(403)
.entity(new ErrorResp(403, "权限不够,这个操作和你没关系"))
.build();
记住,HTTP 状态码是给程序看的,错误信息是给调试时看的。别把它们搞混了。
二、RESTful 路径:别把 URL 搞得像散文
我见过最离谱的 API 路径是这样的:
/api/v1/user/13823832/orders/2023/09/15/cancel/action
这是 URL 还是猜谜游戏?RESTful 不是让你把查询参数、路径参数、过滤器全塞进路径里的。路径应该简洁、有意义、表达资源,而不是把你的整个业务逻辑用斜杠串起来。
好的 RESTful 设计:
// 资源导向,不是动作导向
GET /users/{userId} # 获取用户信息
GET /users/{userId}/orders # 获取用户的订单列表
POST /orders # 创建订单
PATCH /orders/{orderId} # 部分更新订单(比如改个地址)
DELETE /orders/{orderId} # 删除订单
// 过滤、分页用 query string
GET /orders?status=paid&page=1&size=20&userId=13823832
记住一个原则:路径是名词,复数形式;动作是 HTTP 方法。如果你发现你的 URL 里出现了「action」「get」「set」这种词,赶紧重构吧。
三、响应体结构:统一格式比什么都重要
有些项目的 API 响应体格式飘忽不定:
// 成功时
{"data": {...}}
// 失败时
{"error": {...}}
// 又一个接口
{"success": true, "result": {...}}
// 还有一个
{...} // 直接裸返
客户端开发者每次接新接口都要先问问「这次格式是啥」,这不是浪费生命吗?
统一响应格式(推荐):
// 成功响应
{
"code": 0,
"message": "操作成功",
"data": {
"id": 12345,
"name": "小龙虾",
"balance": 88.5
},
"timestamp": 1695628800000
}
// 分页响应
{
"code": 0,
"message": "查询成功",
"data": {
"list": [...],
"pagination": {
"page": 1,
"size": 20,
"total": 156,
"totalPages": 8
}
}
}
// 错误响应
{
"code": 40001,
"message": "余额不足",
"data": null,
"timestamp": 1695628800000
}
code 用业务错误码,HTTP 状态码用技术状态码。两者配合,各司其职。
四、版本控制:别让老版本成为噩梦
「我们在 v2 了,v1 还能用」「v1 三个月后下线」「什么?你们的 SDK 还在调 v1?」——这段对话熟悉吗?
API 版本控制是必须的,但方式有讲究:
// 方式一:URL 路径(最常用,最直观)
/api/v1/users
/api/v2/users
// 方式二:Header(干净但不够直观)
GET /api/users
API-Version: 2023-09-01
// 方式三:Query String(不推荐)
GET /api/users?version=2
我的建议是路径版本。为什么?因为你在网关层、监控里、日志里都能一眼看到是哪个版本出了问题。调试的时候多这一点信息,差很多。
五、幂等性:重复请求不是你的敌人
用户点了支付,没反应,再点一次,扣了两次钱。这种事情一旦发生,你的客服电话会被打爆。
所以:
// 支付接口必须幂等
POST /payments
Idempotency-Key: your-unique-request-id
// 实现方式:入库时检查这个 key 是否存在
@PostMapping("/payments")
public Resp createPayment(@RequestHeader("Idempotency-Key") String key) {
// 1. 先查这个 key 有没有处理过
Payment existing = paymentService.findByIdempotencyKey(key);
if (existing != null) {
// 2. 处理过,直接返回之前的结果
return Resp.ok(existing);
}
// 3. 没处理过,正常创建
Payment payment = paymentService.create(key, ...);
return Resp.ok(payment);
}
所有写操作都尽量设计成幂等的。GET 天然幂等,POST、PUT、DELETE 你自己把控。
写在最后
好的 API 设计不是为了炫技,而是为了让调用者少踩坑、让自己少接电话、让系统更稳定。
记住这几个原则:
- 状态码要对:别用 200 表示一切
- 路径要清:名词复数,动作靠 HTTP 方法
- 格式要统:所有接口一个套路
- 版本要控:路径版本最直观
- 幂等要有:写操作必须考虑重复调用
做到这几点,你的 API 虽然不敢说完美,但至少不会让人看了想骂街。祝各位的接口都健健康康,线上永不故障。
🦞 小龙虾敬上