如果你对接过不同公司的开放平台,肯定见过这种接口文档:一水的 POST。创建是 POST,更新是 POST,删除也是 POST。别笑,有些平台连查询都要求你用 POST,理由是“避免 URL 太长”。这个时候很多刚看完 REST 教程的新人就会犯嘀咕:说好的 DELETE /users/{id} 呢? PUT 和 DELETE 难道不是 HTTP 的基本动词吗?为什么很多大厂 API 不再使用 PUT 和 DELETE,甚至连 GET 都不太愿意用?
这篇文章不打算批判任何风格,而是想以一线开发者的视角,把这件事背后的工程权衡拆开讲清楚。你会看到,大厂不用 PUT/DELETE,不是因为他们不懂 REST,而是因为他们要考虑的调用方、部署环境、网关策略和运维成本,和你在本地写一个 Demo 完全不一样。适合正在设计接口的后端开发、对接外部 API 的客户端同学,以及被“规范之争”困扰的团队参考。
1. 先从现象说起:那些只有 GET/POST 的接口文档
1.1 你看到的“不按套路出牌”的 API 长什么样
我最早遇到这种 API,是很多年前对接一个云厂商的开放平台。文档里所有需要鉴权和写操作的接口,都长这样:
POST /v1/resource/delete Content-Type: application/json { "resource_id": "abc-123" }当时我的第一反应是:这算什么?删除不应该是DELETE /v1/resource/abc-123吗?后来接触多了才发现,这不是某一家公司的特例。支付渠道、消息推送、企业 SaaS、大模型平台……很多对外开放接口都在走同一条路。有些甚至把所有操作统一成唯一一个 POST 入口,路径里带一个action参数,服务端根据参数路由到不同处理逻辑。
你更常见到的是“动作路径”风格,比如:
POST /v1/orders/123/cancel POST /v1/users/456/disable POST /v1/files/789/archive路径仍然可以标识资源,但操作动词从 HTTP Method 挪到了路径的最后一个词。这种方式既保留了资源意识,又避开了 PUT/DELETE 的语义负担。
1.2 是 API 设计倒退,还是我们误解了 REST?
教科书告诉我们,RESTful API 应该用 HTTP 方法表达操作:GET 查询、POST 新建、PUT 整体替换、PATCH 部分更新、DELETE 删除。这套理论当然没错,但它有几个隐含前提:资源可以被精确定义、网络环境足够开放、调用方愿意区分各种状态码和幂等语义。
但在真实的开放平台上,这些前提常常不成立。REST 是一种架构风格,不是协议规范,它对“资源”“表示”“状态转移”都有一定哲学要求。而大厂对外 API 的第一目标往往不是我优雅地表达资源模型,而是让成千上万的第三方开发者能稳定、简单、安全地接入。
说得直白一点:大部分对外 API 本质上是“远程过程调用”,你调用的是“完成某个业务动作”,而不是“对某个资源做 CRUD”。RPC 风格天然适合用一个统一的动词来承载所有动作,这个动词就是 POST。所以你在很多大厂文档里看到的不是设计倒退,而是他们把 API 当作 RPC 在使用,只不过用了 HTTP 作为传输层而已。
2. 为什么 POST 成了“万能钥匙”:四个现实原因
2.1 URL 长度与复杂查询:GET 装不下的参数
GET 请求的参数要放在 URL 的 Query String 里。看着好像挺灵活,但生产环境里,URL 从来不是无限长的。常见的 Web 服务器、负载均衡器、CDN 对请求行的限制一般在 4KB 到 8KB 左右,有些代理甚至更短。一旦查询条件多了,比如时间范围、多字段筛选、坐标、标签、排序规则组合起来,URL 分分钟超限。
更麻烦的是,GET 请求在语义上不允许带 body。虽然很多服务端能读到 GET body,但你不能依赖中间代理帮你传。浏览器fetch对 GET 带 body 的处理也五花八门,客户端 SDK 更不会默认支持。所以遇到复杂查询,与其争论“GET 能不能带 body”,不如直接用 POST,把参数放在请求体里,干净、不截断、不被代理乱改。
我见过一个团队做导出功能,筛选条件有十几个字段,放在 URL 里直接爆掉,最后只能改用 POST,把所有筛选项放在 JSON 对象里。从那以后,他们的接口规范就多了一条:凡是不确定 URL 长度上限的查询,一律 POST。
2.2 幂等性与重试:PUT/DELETE 的严格要求太“苛刻”
HTTP 语义里,PUT 和 DELETE 被设计成幂等操作。幂等的意思是“执行一次和执行多次,结果一样”。PUT 要求整体替换资源,所以重复提交同一个 body,最终资源状态一致;DELETE 重复删除同一个资源,也应该返回 200/204 或 404,而不应该产生副作用。
理论很完美,现实很骨感。很多业务接口根本不是这种“纯资源操作”。比如“删除订单”这件事,除了把订单状态改成已删除,还要发通知、释放库存、给财务记账,甚至还可能触发退款流程。第二次请求过来,难道要让这些副作用再执行一遍吗?如果只在第一次执行,第二次请求又应该返回什么?你很难把一个异步、有副作用的删除操作,硬塞进 DELETE 的幂等模型里。
POST 的好处是没有幂等承诺。你可以自己设计幂等键,通过Idempotency-Key之类的请求头来保证重试安全。这样既保留了重试能力,又不会因为 HTTP 方法本身的语义约束而自缚手脚。很多支付平台就是这么做的:客户端生成一个唯一的 request ID,服务端在窗口期内对相同 ID 只处理一次。
2.3 网关、防火墙与浏览器环境:不是所有网络都认识 PUT
本质上,HTTP 协议支持 PUT、DELETE、PATCH,但现实网络里存在大量中间设备,比如企业防火墙、代理服务器、老旧负载均衡、嵌入式网关。我可以负责任地说,很多传统企业客户的内网策略只放行 GET 和 POST,其他方法被安全策略直接拦截,连请求都到不了你的后端。
这听起来像段子,但在一线对接客户时太常见了。你写了一个完全符合 REST 规范的 DELETE 接口,客户那边怎么调都超时或者 405,最后查下来,是他们中间那道网关把非 GET/POST 方法全丢了。遇到这种情况,服务端代码根本没机会执行。
除了企业环境,浏览器也有自己的问题。HTML 表单历史上只支持 GET 和 POST,这意味着很多老系统、低代码平台、嵌入式设备在发起非 POST 请求时各有各的“幺蛾子”。为了让最大范围的调用方都能接入,大厂干脆只保留最通用、最容易穿透网络的 GET 和 POST。这不是技术能力不行,这是对生态妥协的智慧。
2.4 安全与审计:方法越少,规则越简单
站在安全团队的角度看,一个对外接口如果方法种类太多,WAF 规则、审计日志、日志解析都要处理更多情况。比如要分析“哪些操作修改了数据”,你可能需要同时关注 PUT、POST、PATCH、DELETE,还得区分 DELETE 是软删还是硬删。如果写操作统一用 POST,那审计逻辑就收敛了很多:基本只要关注 POST 请求的路径和请求体。
我也不想夸大这一点,毕竟单靠方法判断危险度根本不靠谱。但“减少方法种类可以让安全策略更集中”确实是一些平台设计的考虑因素。特别是当你的 API 要通过海外 CDN、第三方网关时,给对方一份“只开放 GET 和 POST”的清单,比反复向对方解释 PUT 与 PATCH 的区别要省事得多。
3. PUT 与 DELETE 的理想、现实与尴尬
3.1 PUT 的“整体替换”语义,和现代接口需求不太匹配
RFC 7231 里,PUT 表示“用请求里的表示完整替换目标资源”。也就是说,客户端发送什么,服务器就保存什么。这在早期静态文件管理、KV 存储场景下很自然,但放到现代业务系统里就很别扭。
比如一个用户对象有姓名、手机号、头像、地址、偏好设置等多个字段。客户端只是想更新头像,按 PUT 的严格语义,它应该把整个用户对象提交上来,包括所有字段。可实际调用方根本不知道其他字段当前是什么,强行 PUT 就会把没传的字段清空。于是很多团队私下把 PUT 实现成“部分更新”,但一旦请求重试,第一次把字段 A 更新了,第二次可能把字段 B 也更新了,行为变得不可预期,还违背了 PUT 应该幂等的原则。
正确的部分更新方法是 PATCH,但 PATCH 的格式一直没有像 JSON 统一,有人用application/merge-patch+json,有人用application/json-patch+json,客户端对接成本一下子变高。与其解释半天天,不如约定:一切写操作都走 POST,服务端自己决定是更新还是合并。这样接口实现自由度高,客户端也不容易踩坑。
3.2 DELETE 的“瞬时移除”假设,遇到软删除就失灵
DELETE 的语义是“资源被移除,之后再访问应该 404”。但现代系统几乎不可能真删除数据。用户注销账号,你要保留交易记录;订单作废,你要留审计日志;文件删除,你还要在回收站里躺 30 天。
于是你被迫做软删除:标记一个deleted_at字段,然后让查询过滤掉这些记录。表面上看起来是删了,但实际数据还在。如果客户端对同一个资源调用多次 DELETE,第一次返回 204,第二次返回 404,严格说这已经不符合 DELETE 的幂等预期了,因为资源在第一个请求后就不存在了,但第二个请求返回不同状态码。
再加上删除操作常常伴随异步任务,比如释放资源、回调通知、清理关联数据,你很难在 DELETE 的同步响应里告诉客户端“删除正在处理中”。如果你返回 202 Accepted,下一次调用可能还会拿到资源被删除之前的中间状态。这一类问题用 POST 表达“发起一个删除动作”会自然很多:服务端返回 202 + 任务 ID,客户端再查询任务状态,整个生命周期清晰可控。
3.3 很多接口本质是“动作”,不是“资源”
你平时写 API,真的只有 CRUD 吗?不是的。发送短信、转账、创建会话、调用模型、启动爬虫、导出报表……这些操作很难被强行拆成“创建/读取/更新/删除”四件套。它们更像是一个函数、一个动作、一次任务投递。
如果用 REST 资源思维建模,你可能要发明一堆“资源名称”来填空,比如POST /smsTasks、PUT /transferRecords,折腾半天也没让谁觉得更好用。反而用 RPC 风格直接一点:
POST /v1/transfers Content-Type: application/json { "from": "account_123", "to": "account_456", "amount": 100 }这不叫懒惰,这叫接口跟随业务。大模型平台的 API 也是这种思路,对话接口几乎就是一个POST /v1/chat/completions,你不可能用 GET 去“获取”一个对话,也不太会用 DELETE 去“删除”一段上下文,因为对话是一个持续性的活动过程,不是一个躺在数据库里供增删改查的静态对象。
4. 大厂 API 规范背后:一致性、工具链与生态
4.1 统一动词能降低 SDK 生成和客户端接入的复杂度
如果你维护过 OpenAPI 规范,会发现一个现象:客户端 SDK 根据 HTTP 方法生成代码时,RESTful 风格会生成很多零散方法,比如getUserById、updateUserById、deleteUserById,每个方法对应一个资源操作。如果采用动作风格,方法名可以直接对齐业务语义,例如cancelOrder、disableUser、archiveFile。对调用方来说,这种 SDK 更好上手——不用记资源对应的动词,只需要找“这个操作应该叫什么”。
同时网关层要做限流、鉴权、路由、灰度,如果所有写操作都是 POST,规则就可以统一按路径前缀匹配,不需要为不同方法准备不同的策略。表面上看是“失去语义”,实际上换来了极强的可预测性:新上线的接口,团队成员看一眼路径就知道它属于哪类动作,也大致能猜到请求体结构。
4.2 “自定义方法”规范:Google API 设计指南也在用 POST
很多人不知道,Google 的 API Design Guide 里明确推荐:自定义方法(custom methods)用 POST 实现。比如你有一个资源集合,除了标准 List/Get/Create/Update/Delete,还需要“rename”“cancel”“undelete”这类动作时,规范建议写成:
POST https://example.com/v1/users/user1:undelete路径用冒号引出动作名称。这就是大厂面对 PUT/DELETE 语义不足给出的官方解:不是硬把动作塞进资源 CRUD,而是保留资源标识,用 POST 触发自定义方法。这套设计被很多云厂商和内部基础设施采纳,我们今天看到很多接口不用 DELETE 或 PUT,其实是遵循了一套比“教科书 REST”更加工程化的风格。
当然,Google 也明确保留标准方法的 GET/PUT/DELETE。真正的大厂不是不会用 PUT,而是懂得该用的时候用,不该用的时候不硬用。对自定义业务动作统一用 POST,这才是他们“不用 PUT/DELETE”的真实语境。
4.3 兼容旧客户端与中间件,是现实世界的头等大事
我见过一些创业公司,早期为了打扫“政治正确”,非常严格地使用 PUT/DELETE,结果做海外客户接入时频繁出问题。客户侧使用的往往是旧版 HTTP 库、企业内部代理、定制 WAF。这些东西兼容 GET/POST 几十年了,但对 PATCH 和 DELETE 的处理经常让人头大。
站在甲方视角,一个新平台如果对网络设备有额外要求,企业内部要提交变更审批,业务方很可能直接放弃接入,转投一个“只要 POST 就能调”的竞品。这不是技术选型,是商务问题。大厂 API 不做 PUT/DELETE,很大一部分原因就是为了减少客户接入的阻碍,降低成功对接的门槛。
4.4 那大厂还有没有 DELETE?有的,但分工变了
别误会,不是说大厂彻底消灭了 PUT 和 DELETE。对纯资源的标准生命周期操作,比如删除一个未绑定业务的图片、清空一个测试对象,很多平台依然提供DELETE /v1/resource/{id}。但这种接口通常不是核心业务入口,而是辅助能力。
核心业务里,凡是牵扯审批、回调、任务、账单、状态流转的操作,基本都会设计成 POST。比如“删除一个客户”看似简单,实际上可能引发计费停止、数据导出、关联权限回收。这种操作宁可让客户端先发一个POST /v1/customers/{id}/deactivate,也不愿意用 DELETE 假装只是删一条记录。所以更准确地说,大厂不是“不再使用 PUT 和 DELETE”,而是把这两个方法收敛到了真正适合它们的场景。
5. 什么时候你仍然应该用 PUT/DELETE,以及怎么配套
5.1 内部系统与标准 CRUD:RESTful 依然舒适
如果你的服务部署在完全可控的内网,调用方都是自家后端服务,网关也明确支持常见方法,那么继续用 PUT 和 DELETE 没有任何问题。标准 CRUD 接口用 REST 风格,可以给团队带来清晰的资源边界,尤其是面对数据库表模型时,一个资源对应一个表,GET/POST/PUT/DELETE 的操作意图一目了然。
内部系统还有一个优势:你可以完全控制调用方 SDK,不需要为第三方客户的奇怪网络环境妥协。这种情况下,用 PUT 表示整体更新、PATCH 表示部分更新、DELETE 表示删除,语义清晰,也没有沟通负担。我不会因为外面流行“全 POST”就劝你改,关键是看约束条件。
5.2 只用 GET/POST 的接口,需要九个配套设计
如果你决定走大厂那种“对外接口以 POST 为主”的路线,有一些配套设计不能省,否则接口会变得一团糟:
- 路径里必须写清楚动作:
POST /v1/orders/{id}/cancel,而不是POST /v1/cancelOrder,资源模型能保留一半是好事。 - 给写操作加幂等键:用
Idempotency-Key或X-Request-ID,服务端记住已处理过的 key,重复请求直接返回第一次的结果。 - 明确的版本策略:路径带
/v1/、/v2/,避免动作语义演进时破坏老调用方。 - 统一错误结构:比如
code、message、trace_id,让客户端不用靠 HTTP 状态码猜原因。 - 复杂查询也要定义查询对象:字段名固定,别让客户端裸拼 JSON,否则容易埋雷。
- 区分响应时机:同步操作返回 200/201,异步操作返回 202 + 任务 ID,别把所有响应都塞进 200。
- 设置超时与重试策略:POST 默认可能产生重复操作,必须配合幂等键才能安全重试。
- 在文档里明确“写操作不缓存”:别让客户以为查询接口和写操作走同一套缓存策略。
- 保留 GET 给无副作用操作:能安全用 GET 查询的就用 GET,别为了统一把检索也全改成 POST,这对调试和 CDN 缓存都不友好。
5.3 架构评审时可以问哪些问题
被“该不该用 DELETE”这种问题卡住的团队,其实需要回到更本质的问题。下次评审 API 设计时,先问五件事:
- 调用方是谁?浏览器、后端服务、低代码平台,还是设备端?
- 请求要经过哪些网络节点?有没有企业防火墙、CDN、自定义网关?
- 操作本身是资源 CRUD,还是包含审批、回调、状态机的动作?
- 客户端是否会重试?重试失败后需要保证幂等吗?
- 团队有没有能力维护一套严格的 REST 语义,并约束所有调用方遵守?
这些问题比“用哪个 HTTP 动词”重要得多。只要有任何一个问题的答案是“第三方环境很复杂”,你就有充分理由把动作操作统一收敛到 POST,再配合幂等键和任务状态机制,把逻辑做扎实。
6. 典型问题、踩坑记录与快捷排查
6.1 CORS 预检请求:为什么 DELETE 会先冒出一个 OPTIONS
如果你在浏览器环境调用带自定义 Header 的 DELETE 请求,浏览器会先发一个 OPTIONS 预检,确认服务器允许的Access-Control-Allow-Methods里是否包含 DELETE。如果后端网关没有响应 OPTIONS,或者返回的允许方法列表里没有 DELETE,实际请求根本不会发出。
很多团队被这个问题折磨过之后,就规定“统一用 POST”。不过注意:POST + application/json同样会触发预检,并不是放弃 DELETE 就可以逃避 CORS。CORS 问题的根源在于“非简单请求”和“自定义 Header”,而不是方法本身。该处理预检的还是要处理,在网关层统一放行 OPTIONS 和配置允许方法即可。
6.2 现场环境只放行 GET/POST:405 和 501 的锅
症状很典型:本地用curl -X DELETE一切正常,到了客户环境就返回 405 Method Not Allowed,或者直接连接超时。你的服务端日志里根本没有这条请求记录,说明请求在中间层就被拦截了。
排查方法很简单,先用curl -i -X DELETE https://your-api.example.com/v1/orders/123看返回内容;再绕过域名直接访问后端服务 IP(如果能),比如用curl -H "Host: your-api.example.com" ...,确认是应用服务的问题还是中间代理的问题。如果直连后端正常、过网关 405,基本可以确定是网关白名单没放开 DELETE。这种时候,业务上能回退就回退成POST /v1/orders/123/delete,不能回退就要找运维开网络策略变更。
6.3 “全 POST” 接口的隐患:缓存、重复提交、爬虫误伤
只用 POST 也有一堆新问题,不是没有代价。最明显的是缓存失效:HTTP 缓存主要面向 GET 请求,POST 默认不会被 CDN 和浏览器缓存,所以相同筛选条件的查询每次都要打到后端,性能压力更大。其次是重复提交:网络超时后客户端重试,如果没有幂等键,用户可能收到两次扣款、两条订单。
还有一个容易被忽视的问题是爬虫和搜索引擎。GET 接口可以方便地被工具识别和收录,全 POST 的接口对 SEO、文档站就特别不友好。有些团队最后会采用折中方案:对只读操作保留 GET,只有业务动作才用 POST,保证性能和调试体验。
6.4 一个快速的接口问题排查顺序
我给自己总结了一套排查办法,遇到“为什么 DELETE 调不通”这类问题,按顺序走:
- 先用
curl -v -X DELETE看握手、请求头、响应头,判断请求有没有到达你的服务。 - 检查服务端访问日志,如果没有任何记录,基本可以判断是网络中间层拦截。
- 在服务端写一个临时 debug 接口,把收到的 method、path、headers 全部打印出来,一目了然。
- 确认网关层的
allow_methods配置,是否包含 DELETE、PUT、PATCH。 - 如果是浏览器调用,先看 CORS 预检日志,确认 OPTIONS 请求是否被正确响应。
- 如果生产环境时间紧急,最稳的 fallback 方案就是改为 POST 动作接口,先恢复业务可用性,再回头完善语义。
这个顺序在绝大多数场景下都能帮你快速定位问题,而不是在文山会海里争论“PUT 到底应不应该用”。我在实际对接中用过很多次,尤其遇到第三方网络环境时,几乎都是这套方法救场。
说起来,我自己刚开始写接口的时候也是个“方法原教旨主义者”,总觉得不用 PUT/DELETE 的 API 都是业余设计。直到有一次做企业客户对接,对方内部网络设备只放行 GET 和 POST,生产环境里删除订单的接口怎么调都失败。最后我们连夜把所有状态变更接口改成POST /v1/orders/{id}/cancel这种动作风格,第二天客户一次就调通了。从那时起我才明白,HTTP 动词只是工具,不是为了上课。选 GET 还是 POST,说到底是在团队、客户、网络基础设施之间找一个最大公约数。下次你看到全是 POST 的大厂 API,不用急着吐槽,先想想他们的调用方是谁、部署环境在哪里,你大概率就能理解这个决定了。