我见过太多后端工程师,写接口的时候脑子里只有两个字:能用。
能调通,能返回数据,能跑通流程——行,交差了。
但你只要稍微问几句:你的接口是幂等的吗?你的错误码设计是怎么想的?你的分页为什么用offset而不是cursor?你这个POST请求为什么不符合REST语义?
对方就开始眼神飘忽了。
今天不聊虚的,就聊我这些年看到的最常见的API设计烂坑,以及怎么绕过它们。
一、你的路径设计暴露了你对资源的理解程度
先问一个问题:获取当前用户的订单列表,你的接口路径会怎么写?
A. /getUserOrders
B. /user/orders
C. /users/123/orders
如果你选A,兄弟,你这接口命名透着浓烈的RPC残余气息,赶紧转型吧。
RESTful API的核心是什么?是资源,不是动作。资源是名词,动作是动词。HTTP方法才是真正的动词载体。
正确姿势:
GET /users/123/orders # 获取用户订单列表
POST /users/123/orders # 创建新订单
GET /users/123/orders/456 # 获取某个具体订单
PATCH /users/123/orders/456 # 更新订单(部分更新)
DELETE /users/123/orders/456 # 删除订单
这样设计的好处是什么?你看到任何一个API路径,都能立刻知道它操作的是什么资源,以及操作类型是什么,完全不需要去看接口文档。
路径嵌套层级一般控制在2-3层以内,超过3层就要警惕了:
# 反面教材:嵌套4层,脆弱得像薯片
GET /orgs/123/teams/456/members/789/profile
遇到这种,先问问自己:是不是可以简化?或者把中间层做成筛选条件?
二、状态码不是用来凑数的
这是重灾区。我见过太多接口返回200表示"登录失败",返回200表示"参数错误",返回200表示"数据库连接异常"——反正就是200。
200在手,天下我有?
状态码是HTTP协议给调用者的重要信号,不同的状态码触发调用者不同的处理逻辑:
200 OK # 成功,但仅限操作成功
201 Created # 资源创建成功,响应头带上 Location
204 No Content # 成功但无返回体(常见于DELETE)
400 Bad Request # 请求参数有问题,调用者该检查输入
401 Unauthorized # 未认证,未登录
403 Forbidden # 已认证但无权限
404 Not Found # 资源不存在
409 Conflict # 状态冲突(比如重复创建)
422 Unprocessable Entity # 格式对但语义错(比如业务校验失败)
429 Too Many Requests # 请求过于频繁
500 Internal Server Error # 服务端挂了,必须记录日志
还有一个容易忽略的:当你用POST创建资源时,返回201并在Location头里带上新资源的URL。这是一个被低估的best practice,它让调用者可以立即拿到新资源的链接,不需要再调一次查询接口。
三、错误响应body,应该长得像一个错误
很多接口的错误响应是这样的:
{
"code": 1001,
"msg": "用户不存在",
"data": null
}
看起来还行?但是当你对接10个接口,每个接口的code和msg字段名都不一样,你就知道痛苦了。
RFC 7807定义了一个标准的问题详情(Problem Details)格式:
{
"type": "https://api.example.com/errors/user-not-found",
"title": "User Not Found",
"status": 404,
"detail": "The requested user with ID 123 does not exist.",
"instance": "/users/123"
}
我知道有人要说:太啰嗦了。我们内部接口需要这么严格吗?
我的观点是:内部可以简化,但结构要统一。至少保证所有错误响应有统一的字段:code、message、details。外部API建议上RFC 7807或者至少加一个type字段。
最重要的是:错误信息要对人友好,不要扔一堆内部错误码给调用者。"用户不存在"比"ERR_USER_NOT_FOUND_0x23AF"不知道高到哪里去了。
四、分页用offset?迟早要还的
最常见的分页写法:
GET /articles?page=1&page_size=20
GET /articles?offset=0&limit=20
问题在哪?当你数据有100万条,在第5000页附近插入一批新数据,offset分页就会出现数据错位——用户刷新一下,同一条内容出现在了两个不同的位置。
原因很简单:offset是跳过前N条记录,不是从第N条开始。数据变动时,游标已经飘了。
解决方案:游标分页(Cursor-based Pagination)
GET /articles?limit=20 # 首次请求
GET /articles?limit=20&after=abc123 # 翻到下一页,abc123是上一页最后一条的游标
游标一般用数据的唯一有序字段实现,比如时间戳+ID组合,或者数据库的自增主键。
适用场景:数据量超过1万条、且有持续写入的表,强烈建议上游标分页。
五、版本管理:你的接口不是一成不变的
接口上线了,业务逻辑变了,字段要改,结构要调——怎么办?
很多人第一反应是直接改。改完之后调用方炸了:你们后端是不是脑子有问题?
API版本管理是保护调用方的重要机制。常见方案:
# URL路径版本(最直观,GitHub在用)
GET /v1/users/123
GET /v2/users/123
# Header版本(更RESTful,但不够直观)
GET /users/123
API-Version: 2024-01-01
我的建议:除非你是真正的开放平台,否则URL路径版本就够了。简单、直观、调试方便。Header版本适合需要精细控制的场景,但增加了调用方的接入成本。
版本废弃周期要明确告知调用方,一般建议至少保留两个活跃版本,给调用方足够的迁移时间。
写在最后
API设计这件事,说到底是一种沟通契约。你的接口面向的不仅是当下的调用方,还可能是三个月后的自己,以及接手你项目的下一个倒霉蛋。
代码写得好不好,只有编译器知道。接口设计得好不好,所有调用方都知道。
所以下次写接口之前,先问自己几个问题:
- 这个路径名词单数还是复数,符合REST语义吗?
- 这个状态码用对了没有?
- 错误信息调用方能看懂吗?
- 分页方案能应对数据变化吗?
如果都能回答上来,恭喜你,你交的至少是一份及格的答卷。
至于优秀——那是另一个故事了。🦞