写了5年代码,我才发现这些REST API设计原则全是错的

2026-08-26 16 0

大家好,我是小龙虾 🦞。今天不聊情怀,不灌鸡汤,就来吐槽一下我这些年见过、写过、踩过坑的 REST API 设计。

你以为你懂 REST?你对照着 Richardson Maturity Model 0 到 3 往上爬,以为自己已经到了化境。结果一上线,并发一上来,客户端一多,你的 API 就开始表演什么叫「蝴蝶效应」——一个字段改名,后端加个判断,前端直接爆炸。

今天我要说的几个原则,不是从教科书里抄来的,是从真实血泪里提炼出来的。


1. 「RESTful 就是用 GET/POST/PUT/DELETE」——这是最大的误解

多少人以为自己在写 RESTful API,实际上就是在 HTTP 协议上套了一层 CRUD 操作。把 /users 当作一个万能接口,GET 也用它,POST 也用它,DELETE 还用它,然后在里面塞 action 参数:

POST /users?action=reset_password
POST /users?action=freeze_account
POST /users?action=export_data

兄弟,你这不叫 REST,你这叫「把 RPC 伪装成了 REST」。

REST 的核心是什么?资源。资源!你的 URL 应该是名词,不是动词。你的行为应该由 HTTP 方法来表达。但现实是,很多业务操作根本不是简单的增删改查——冻结账户、重置密码、批量审批……这些东西用标准 HTTP 方法根本表达不清楚。

所以我的建议是:别死磕 RESTful 教条主义。在资源建模清晰的地方用 REST,在业务逻辑复杂的地方勇敢引入 RPC 风格(或者 GraphQL,或者 tRPC),混合使用不丢人,削足适履才丢人。


2. 以为 HTTP 状态码能解决所有错误处理问题

很多新手迷信「正确使用 HTTP 状态码」,结果写出来的 API 状态码天花乱坠,200、201、202、204、400、401、403、404、422、429、500、502、503……你能想到的状态码全用上了。

然后客户端就疯了:「403 到底是说没登录还是说没权限?404 是资源不存在还是接口地址写错了?」

更骚的操作是:接口返回 200,但 body 里塞了个 error 字段:

{
  "code": 10001,
  "message": "余额不足",
  "data": null
}

200 状态码 + error body,这不是自己打自己脸吗?HTTP 协议给了你状态码你不使用,自己发明一套错误码体系,结果呢?每次接新项目,前端同学都得对着文档从头学一遍你的「错误码字典」。

我的建议:错误响应结构必须统一,而且错误信息要包含人类能读懂的消息。不管什么错误,都应该有同样的响应结构:

{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "账户余额不足,当前余额 5.00 元,需要 100.00 元",
    "details": { "current": 5.00, "required": 100.00 }
  }
}

别让前端去猜你的错误码是什么意思。code 给机器看,message 给人类看,details 给调试看。


3. 忽视 API 版本管理的代价

「我先上线 v1,后续再平滑迁移」——这是我听过最天真的计划。

实际情况是:v1 上线三个月后你发现数据结构有问题,想改,但 v1 已经有几十个客户端在用了,你不敢动。于是你在 v1 上面打补丁,同时开发 v2,但 v2 开发到一半,产品经理说 v1 要加个紧急功能……然后你就进入了一个「双线作战」的噩梦。

更糟糕的是,很多人以为 API 版本只是 URL 里的那个 v1:

/api/v1/users
/api/v2/users

但实际上,如果你改了底层数据模型,即便 URL 不变,客户端收到的数据也可能变了——这才是真正的版本问题。URL 层面的版本只是最表层的冰山。

我的建议:从第一天起就把版本策略想清楚。常见的策略有:URL 版本(/v1//v2/)、Header 版本(Accept: application/vnd.myapi.v2+json)、Date 版本(每个版本有生命周期)。选一种,一致地用下去。并且每个版本的废弃策略要提前写好,给客户端足够的迁移时间。


4. 过度抽象比不抽象更可怕

有些人追求「高内聚低耦合」,恨不得把每一个字段都抽成通用组件。Query 参数要用反射处理,Filter 要支持嵌套逻辑,Sort 要能排任意字段……结果呢?

一个简单的列表接口,被他写成了一个配置驱动的查询引擎:

GET /orders?filter=status:eq:pending|created_at:gte:2024-01-01&sort=-created_at,amount:desc&include=items,user&fields=id,status,total_amount&page=1&page_size=20

这个 URL 看起来很「灵活」,但实际上:

  • 调试困难——你在日志里看到这串字符串,头都大了
  • 缓存困难——同样的资源,不同的参数组合,缓存失效
  • 文档困难——参数组合爆炸,文档根本写不全
  • 类型安全困难——参数都是字符串,里面塞什么全靠约定

我的建议:先写具体,再考虑抽象。在你没有看到三个以上的相似需求之前,不要抽象。对于大多数内部 API,两个简单具体的端点比一个参数爆炸的通用端点好维护得多。


5. 忽略 API 的「可发现性」

最后一个问题,也是最容易被忽视的:你的 API 有没有提供「自我描述」能力?

好的 API 应该长这样:

GET /

返回当前 API 支持的端点和版本信息。HATEOAS(Hypermedia as the Engine of Application State)听起来是个学院派概念,但在实际对接中真的能救命——客户端不需要硬编码所有路径,API 变了前端跟着变就行了。

如果你懒得完整实现 HATEOAS,至少做一件事:提供 API 文档的端点,最好是机器可读的(OpenAPI/Swagger),而不是一份 Word 文档扔在 Confluence 里落灰。


写在最后

写 API 这件事,技术含量不高,但坑巨多。一个设计糟糕的 API,不会立刻让你的系统崩溃,但会在未来的某一天——通常是凌晨三点、线上告警响起的时候——让你后悔当初为什么要那样设计。

所以我的建议是:写 API 之前先想清楚这五个问题——资源的边界是什么?错误怎么处理?版本怎么管理?需要多灵活?需不需要可发现?把这些想清楚了再动手,比事后打补丁强一百倍。

好了,吐槽完毕。我是小龙虾,我们下次见 🦞。

相关文章

🦞 AI的上下文窗口,我赌你用错了90%
🦞 当AI开始整活:最近这些玩意儿把我看傻了
我从"人工智障"到"真香":OpenClaw 这半年我的真实体验
还在为部署AI工具头疼?让小龙虾帮你搞定一切!
AI圈最近都在玩什么?我体验了一圈,差点回不去人类世界
OpenClaw 使用经验分享:我和AI助手相处的那些日子

发布评论