写接口5年后,我总结了这些血泪教训

2026-03-26 9 0

写接口5年后,我总结了这些血泪教训

干这行这么多年,见过的API没有一千也有八百。有时候接手别人的代码,真的想穿越回去问问对方:你当时是怎么想的?

今天不扯虚的,直接上干货。我把踩过的坑、见过的烂设计、还有自己血泪教训总结了一遍,保证都是实战经验,没有教科书上那些正确的废话。

一、URL设计:别把接口写成谜语

见过最离谱的接口是这个画风:

GET /api/v2/module/user/action/getInfo/param/123

说实话,看到这种URL,我第一反应是去庙里求个签问问这接口到底想干嘛。

好的URL设计应该是什么样子?记住这个公式:

资源命名 = 名词复数 + 层级关系 # 用户的帖子 → GET /users/{user_id}/posts # 某个帖子的评论 → GET /users/{user_id}/posts/{post_id}/comments # 创建评论 → POST /users/{user_id}/posts/{post_id}/comments 

动词由HTTP方法承担,别在URL里塞get、create、update这些玩意儿。REST不是让你把所有操作都塞进URL里。

还有个常见病:RESTful police综合症。有些人为了REST而REST,搞出一堆不伦不类的设计。比如:

# 搜索这种操作就别硬套REST GET /users/search?name=zhangsan # 还行 # 但复杂的过滤呢? GET /users/filter?age_gt=18&city_in=beijing,shanghai&sort=created_at&order=desc # 这种情况别倔,坦然用 query 参数,接受现实 

记住,规则是死的,业务是活的。别为了符合某种规范把自己逼死。

二、状态码:你真的会用吗?

很多后端工程师对HTTP状态码的认知就停留在200、400、500这三个。这种认知,写出来的API能把前端气哭。

先说最重要的几个:

  • 2xx:成功系列。200是默认成功,201是创建成功(POST后用这个很规范),204是删除成功(无返回内容)。
  • 4xx:客户端问题。400是参数错误,401是没登录,403是登录了但没权限,404是资源不存在,422是参数格式对但语义错。
  • 5xx:服务端问题。这个一般是你代码写炸了,记得日志要记清楚。

重点说422。这个状态码很多人不用,其实它很适合处理"参数格式没问题,但值不符合业务规则"的场景。比如:

# 用户名已经存在,返回422而不是400 # 因为参数格式完全正确,只是业务上不允许 { "error": "validation_failed", "message": "用户名已被注册", "field": "username" } # 再比如:创建订单时库存不足 { "error": "insufficient_stock", "message": "商品库存不足", "product_id": "SKU123", "available": 0, "requested": 5 } 

还有个小技巧:对于分页列表接口,即使没有数据,200也要返回正确的列表结构:

{ "data": [], "pagination": { "page": 1, "page_size": 20, "total": 0, "total_pages": 0 } }

别因为空列表就返回个null或者404,前端要为此多写一堆判断。

三、错误处理:别让前端猜谜

有些接口的错误返回是这个德行:

{ "message": "操作失败" }

操作失败是什么鬼?哪里失败了?为什么失败?前端拿着这个错误,能做的事情就是弹一个"操作失败"的Toast,然后用户一脸懵逼。

好的错误响应应该长这样:

{ "error": "INVALID_PARAMETER", "message": "请求参数验证失败", "details": [ { "field": "email", "message": "邮箱格式不正确" }, { "field": "password", "message": "密码长度不能少于8位" } ], "request_id": "req_abc123xyz" }

几个关键点:

  1. error字段用大写下划线格式:这是约定,便于前端做错误码判断。
  2. message给人看:这是给开发者的调试信息,会出现在日志里。
  3. details给用户看:如果需要展示具体的字段错误,放这里。
  4. request_id必须要有:线上排查问题全靠这个,重要性怎么强调都不为过。

另外,错误响应头里也可以加个X-Request-Id,方便在网关层统一打日志。

四、版本控制:向前容易向后难

接口要版本化,这个大家都知道。但具体怎么版本化,这里有个坑。

常见方案有两种:

# 方案1:URL路径版本(最常用) GET /api/v1/users GET /api/v2/users # 方案2:Header版本(更REST,但用起来麻烦) GET /users Accept: application/vnd.mycompany.v2+json 

我的建议是:老老实实用URL版本。为啥?因为调试方便。curl、浏览器、postman直接改路径就行,不用记那一堆header。团队里不是每个人都是RESTful原教旨主义者。

更重要的是,版本升级要考虑这些:

  • 老版本别急着下线:给用户留足迁移时间,一般至少维护两个大版本。
  • 不兼容的改动必须升版本:删除字段、修改字段类型、改变返回值结构,这些都是不兼容的改动。
  • 兼容的改动不用升版本:新增字段、新增接口,这些完全可以。

有个判断小技巧:只要是能让现有调用方代码坏掉的改动,都是不兼容的。

五、性能:别让接口拖死你的系统

性能问题往往是API设计的遗毒。我见过太多这种情况:设计的时候只管功能实现,不管性能,等上线用户一多就爆炸。

几个实战技巧:

1. 避免N+1查询

这个是重灾区。比如要获取用户列表及其订单:

# 烂设计:N+1查询 GET /users # 查1次用户 然后循环里查: GET /users/1/orders # 查N次订单 GET /users/2/orders ... # 好设计:用join一次查完 GET /users?include=orders # 或者专门的聚合接口 GET /users/{user_id}/orders-summary 

2. 合理使用分页

列表接口必须分页,这个没得商量。但分页实现要注意:

# offset分页适合小数据量 GET /posts?page=1&page_size=20 # cursor分页适合大数据量 GET /posts?cursor=eyJpZCI6MTAwfQ==&page_size=20 # 时间范围分页适合日志类数据 GET /events?start_time=2024-01-01&end_time=2024-01-02 

啥时候用啥分页方式,也是个经验活儿:数据量百万以下offset还行,超过就老实换cursor。

3. 字段筛选

# 让调用方只取需要的字段,减少数据传输 GET /users?fields=id,name,avatar&page_size=20 # 返回示例 { "data": [ {"id": 1, "name": "张三", "avatar": "https://..."}, ... ] } 

六、安全:别把后门敞开

最后说个很多人容易忽视的点:安全。

几个基本操作必须做好:

  1. 参数校验:所有外部输入都是不可信的,该校验就校验,别偷懒。
  2. SQL注入:用参数化查询,别手动拼SQL。
  3. 敏感数据:密码、token这些绝对不能出现在日志里,返回给前端时也要脱敏。
  4. 限流:加上rate limit,防止被人刷。
# 限流相关响应头 X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 999 X-RateLimit-Reset: 1640000000 # 超限后的响应 HTTP/1.1 429 Too Many Requests Retry-After: 60 

写在最后

写了这么多年,我觉得好的API设计,其实就一句话:让人用着舒服,而不是写着舒服

多站在调用方的角度想问题,多考虑各种边界情况,多问问自己"如果我是个啥都不懂的前端,看到这个接口会骂人吗"。

接口设计这事,说难听点就是"前人挖坑后人跳"。我们能做的,就是少挖点坑,让后人少跳几次。

共勉。

相关文章

OpenClaw 体验报告:论一只AI小龙虾是如何被自己的工具折腾疯的
OpenClaw:我的AI小助理养成日记
Go语言调度器原理深挖:goroutine原理不懂面试官都笑你
为什么你的try-catch写得比小学生作文还烂?
花39块让人帮你干活,还是自己折腾到凌晨3点?——代部署服务了解一下
一只小龙虾的OpenClaw使用手册:真香与踩坑并存

发布评论