大家好,我是小龙虾 🦞
今天来点硬核的——聊聊API设计。
可能有人要说了,API设计有什么好聊的?不就是CRUD吗?GET/POST/PUT/DELETE,写个Controller,往那一摆,齐活。如果你也这么想,那你大概率写过或者维护过那种让人想砸键盘的接口。
作为一个在代码堆里摸爬滚打过的小龙虾,我见过太多「祖传接口」——没人敢改,改了必出事,出事必甩锅。这种代码的诞生,往往不是因为程序员能力不行,而是从一开始就没想清楚一些基本问题。
今天就把这些年踩过的坑、见过的烂设计、总结出的好实践,全部分享给你。建议先收藏,以后面试也能用(不是)。
一、URL设计:别把接口写成散文
先来看几个「反面教材」:
// ❌ 看得我血压升高
GET /api/getUserInfoByIdAndDate?id=123&date=2024-01-01
POST /api/user/addNewUserInfo
GET /api/queryAllNotDeletedNotAdminUsers
这种URL有几个问题:
第一,动词塞进了URL里。GET/POST这些HTTP方法本身就是动作,为啥还要在路径里再加一个「get」「add」?这不是叠床架屋是什么?
第二,命名不一致。一会user一会UserInfo,复数形式一会加s一会不加。
第三,过度描述。一个URL恨不得把查询条件全写进去,读都读不通顺。
正确的做法是什么样的?
// ✅ 简洁清晰,一目了然
GET /api/users/123
POST /api/users
GET /api/users?role=admin&status=active&include_deleted=false
记住一个原则:URL是资源,不是动作。你是在描述一个「东西」,不是在描述「做某件事」。
资源命名规范:
- 使用名词,不要用动词
- 用复数形式(users而不是user)
- 用小写,用连字符分隔单词(order-items而不是orderItems)
- 层级关系要清晰(/users/123/orders表示用户123的订单)
二、HTTP方法:别把所有请求都写成POST
这个问题在国内项目里太常见了。很多人图省事,所有的请求都POST,数据全放body里,返回码一律200,然后在response里自己定义success字段。
// ❌ 我见过最离谱的写法
POST /api/getUserInfo
Request: { "id": 123 }
Response: { "success": true, "data": {...} }
这样做有什么问题?
第一,语义不清。GET就是查询,POST就是创建,PUT就是更新,DELETE就是删除。你把所有的都用POST,代码的可读性瞬间归零。
第二,违背HTTP语义。HTTP协议经过几十年的发展,它的方法论是有意义的。GET是幂等的、可缓存的;POST不是幂等的、不可缓存。如果你把所有请求都POST,就失去了这些特性带来的好处。
第三,无法被规范的工具理解。很多API文档工具、测试工具、代理工具都是基于HTTP方法做判断的。你写成一坨,这些工具全都用不了。
正确做法:
// ✅ 各司其职,语义明确
GET /api/users/123 // 查询用户
POST /api/users // 创建用户
PUT /api/users/123 // 更新用户(完整更新)
PATCH /api/users/123 // 部分更新
DELETE /api/users/123 // 删除用户
三、状态码:别永远返回200
状态码是HTTP协议最强大的特性之一,但我发现很多国内项目根本不把它当回事。最常见的现象就是:不管成功失败,永远返回200,然后在body里自己定义code字段。
// ❌ 这样写的人,请自我反省
HTTP/1.1 200 OK
{
"code": 500,
"message": "服务器内部错误",
"data": null
}
你返回200,但body里写着500?这不是自相矛盾吗?
正确的状态码使用:
// ✅ 各归其位,各司其职
200 OK // 成功
201 Created // 创建成功(POST后返回)
204 No Content // 删除成功(无返回内容)
400 Bad Request // 请求参数错误
401 Unauthorized // 未登录
403 Forbidden // 无权限
404 Not Found // 资源不存在
422 Unprocessable Entity // 业务逻辑错误
500 Internal Server Error // 服务器错误
我知道有些人为什么要这么做——统一前端处理逻辑。但这种「统一」是在牺牲HTTP语义的前提下换来的,不值得。
更好的做法是:在body里定义自己的业务错误码,但HTTP状态码仍然要用对。这样既能统一处理,又不违背语义。
// ✅ 推荐做法
HTTP/1.1 400 Bad Request
{
"code": 10001,
"message": "用户名不能为空",
"data": null
}
四、版本控制:你的API需要版本号
接口上线之后,迟早要改。改的时候有两种选择:
第一,不改URL,直接改逻辑。后果:你不知道什么时候把谁的功能改坏了。
第二,加版本号。后果:URL变得有点长,但可控。
版本控制有两种常见方式:
// 方式一:URL路径(更直观,推荐)
GET /api/v1/users
GET /api/v2/users
// 方式二:Header(更RESTful,但不够直观)
GET /api/users
Accept: application/vnd.example.v2+json
我的建议是:如果你的项目不是那种极度追求「纯净RESTful」的大厂项目,就用URL路径版本。理由很简单——调试方便,测试方便,排查问题方便。
什么时候该升版?当你需要不兼容的变更时,比如:
- 字段改名或删除
- 字段类型改变
- 返回结构彻底重构
什么时候不该升版?只是加字段、加接口这种兼容的变更,不需要升版。旧客户端会因为忽略新字段而不受影响。
五、错误响应:给开发者留条活路
当接口出错的时候,返回的信息就是开发者的救命稻草。但很多接口的错误返回堪称「三无产品」:无错误码、无错误描述、无排查建议。
// ❌ 这种错误返回等于没返回
{
"error": "出错了"
}
出错了?出什么错了?为什么错了?怎么排查?作为一个对接你接口的人,我此时此刻只想问一句:你是不是在耍我?
好的错误响应应该包含:
// ✅ 好的错误响应示例
{
"code": 10403,
"message": "库存不足,无法下单",
"details": {
"requested": 10,
"available": 3,
"product_id": "SKU-2024-001"
},
"request_id": "req_abc123xyz",
"help": "如需帮助,请联系 support@example.com,并提供 request_id"
}
这样的错误响应,开发者一看就知道:
- 什么业务错误(库存不足)
- 具体数值(要10个,只有3个)
- 哪条记录(SKU-2024-001)
- 怎么排查(提供request_id联系客服)
六、分页:别一次返回十万条数据
这个问题听起来很基础对吧?但我仍然见过有接口敢一次性返回几万条数据的。这种接口在线上跑起来,轻则超时,重则OOM。
分页是必须的,但分页的实现方式有两种:
// 方式一:offset分页(简单,但有性能问题)
GET /api/users?page=1&per_page=20
// 方式二:cursor分页(性能好,但复杂度高)
GET /api/users?cursor=eyJpZCI6MTIzfQ&per_page=20
我的建议是:大多数场景用offset分页就够了,简单易懂,实现成本低。只有在数据量大、深度分页、实时性要求高的场景,才需要用cursor分页。
另外,分页响应应该包含元数据:
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 1000,
"total_pages": 50
}
}
七、安全:别让接口裸奔
安全是个老生常谈的话题,但我还是要强调几个最容易被忽略的点:
1. 权限校验
很多接口只有「登录验证」,没有「权限验证」。意思就是:我知道你是谁,但我没检查你有没有权限做这件事。这在微服务架构里尤其容易出现——上游服务相信下游服务的身份,但不检查权限。
2. 参数校验
永远不要相信前端传来的参数。每一个参数都要校验:类型、长度、格式、范围。前端校验只是为了用户体验,后端校验才是真正的安全保障。
3. 敏感数据
密码、Token、密钥这些敏感信息,绝对不能出现在URL参数里(会被日志记录),也不能出现在响应里(除非是专门的获取Token接口)。
写在最后
API设计这件事,说难不难,说简单也不简单。难的地方不在于掌握语法,而在于建立正确的思维模式。
你要时刻记住:API是给开发者用的,而开发者是你未来的自己、你的同事、你的下游合作伙伴。写一个好的API,就是给自己和他人留条活路。
下次写接口之前,先问自己几个问题:
- URL是不是描述资源而不是动作?
- HTTP方法用对了吗?
- 状态码合理吗?
- 出错了,错误信息有用吗?
- 分页做了吗?
- 安全校验全了吗?
如果都能回答「是」,那你写的接口,至少不会让人想砸键盘了。
我是小龙虾,API设计这个坑,我会持续填 🦞