
我第一次独立设计 API 的时候是个教科书级信徒用户更新用PUT /users/{id}删除用DELETE /users/{id}还在接口文档里煞有介事地标注了幂等性。结果联调第一天就被网关打回来了——运维丢给我一句话我们这只放行 GET 和 POST其他的你自己想办法。后来我养成了一个习惯每次接到新项目先翻一遍主流大厂的公开 API 文档。翻得多了才发现PUT 和 DELETE 在这些公司的接口设计里确实越来越少见很多新服务干脆只认 POST更新和删除全用 POST 动作路径来解决。这不是某个人的审美偏好而是网络基础设施、业务语义、安全合规、还有 AI 时代 API 风格四股力量拧在一起的结果。今天就把这件事拆开聊透。1. REST 教科书里的完美分工PUT 和 DELETE 本该干什么1.1 方法论的分工GET 读、POST 增、PUT 全量替换、DELETE 删先回到标准定义。HTTP 方法不是随便定的每个方法都有明确的语义约定GET 负责读取资源POST 负责提交数据让服务端创建资源或执行某个流程PUT 要求把请求体里的资源完整替换掉 URL 指向的资源DELETE 表示删除 URL 指向的资源PATCH 是后来补上的局部更新方法。在早期的 RESTful 设计里这五个方法配合资源路径能表达绝大多数 CRUD 场景。举个例子一个典型的博客系统GET /posts/42读取文章POST /posts创建文章PUT /posts/42提交整篇文章内容直接覆盖原文章DELETE /posts/42删掉这篇文章。逻辑非常自洽接口文档一写出来调用方看方法名基本就知道该干什么。这套设计的核心思想是资源导向你面对的是一个个可以被识别、被操作的对象HTTP 方法就是对这些对象的标准操作接口。只要资源建模清晰方法语义就是天然的接口文档。1.2 幂等性这把尺子是理解整件事的关键要理解 PUT 和 DELETE 为什么会被嫌弃先得搞懂幂等性这个概念。一个请求是幂等的意味着同一个请求执行一次和执行一百次服务端最终状态是一样的。在标准定义里GET 是安全且幂等的只读不写PUT 是非安全但幂等的不管调用多少次资源最终都是同一个完整状态DELETE 也是非安全但幂等的删一个不存在的资源结果同样是目标不存在不会报错而 POST 既不安全也不幂等——调两次可能产生两条数据。设计者的本意很美好给 PUT/DELETE 标上幂等属性调用方就知道这个请求就算网络超时重发一遍也没事从而简化了分布式场景下的重试逻辑。当年的教科书里PUT 和 DELETE 因此被赋予了安全可重试的光环。1.3 教科书设计在哪些场景里真的成立实话实说这套分工在特定场景里至今依然是最优解。最典型的就是对象存储和 KV 存储你 PUT 一个对象到某个 bucket 路径无论执行多少次最终都是那个键对应的对象被完整覆盖你 DELETE 一个对象执行多少次都是删掉它。资源生命周期极其清晰客户端永远持有完整数据这种情况下 PUT/DELETE 天然匹配硬改成 POST 反而画蛇添足。配置中心、分布式锁、任务资源管理这类场景也是同理。只要你处理的确实是一个可以被整体定义、整体替换的实体PUT 和 DELETE 就是最准确、最省事的表达方式。看到这里你可能会问既然这么好为什么大厂还在大量接口里放弃它们答案在下一节——因为现实网络环境没这么理想。2. 中间链路的偏见网关、防火墙和代理为什么拦 PUT/DELETE2.1 设备厂商对非常见方法的保守策略HTTP 方法在标准里定义了不少但中间网络设备真正熟练处理的只有 GET 和 POST。很多老旧代理服务器、企业内网出口设备、甚至某些云厂商的负载均衡器对 PUT/DELETE 的处理逻辑非常不完善有的直接丢弃请求有的返回 501 Not Implemented有的会把 DELETE 当成危险操作给拦下来。我亲身踩过一个大坑给某个客户做系统对接对方内网代理把我们发过去的 PUT 请求直接吞掉了返回了一个空的 200 响应。我们排查了一个下午最后发现是中间设备的策略问题——它只转发 GET 和 POST其他方法一律静默丢弃。从那以后我就明白了在不可控的网络链路上方法是越常见越安全越冷门越容易出幺蛾子。这类问题在跨企业、跨网络的 API 对接里尤其致命。你没法控制调用方网络里那台老旧设备的行为所以对外 API 设计时很多团队第一反应就是方法尽量只用 GET 和 POST这俩是全网基础设施兼容性最好的没有之一。2.2 安全扫描和运维合规是压死 DELETE 的最后一根稻草更让 PUT/DELETE 雪上加霜的是安全扫描器的偏见。大量漏洞扫描工具和合规检查系统把删改类方法视为高风险信号一旦扫描报告里出现 PUT/DELETE就会触发一堆告警比如检测到不安全的 HTTP 方法服务器支持 DELETE 方法可能被恶意利用之类的条目。这些告警大部分是误报——支持 DELETE 方法本身不代表有漏洞真正的风险在于接口的鉴权逻辑是否严密。可是安全团队和运维团队没有动力去逐条解释误报。为了过审、为了减少沟通成本最省事的方案就是对外网关层直接不给 PUT/DELETE 开口子。我见过不止一个团队就是这么做的安全扫描报告一夜之间清净了代价是 API 设计被迫向 POST 收敛。2.3 CDN 与缓存策略带来的隐性限制还有一个容易忽略的层面CDN 和缓存中间件。GET 天然可以被 CDN 缓存POST 按约定一般不缓存而 PUT/DELETE 的缓存和回源行为在各大 CDN 厂商那里的处理策略非常不统一。有些边缘节点会把非 GET/HEAD 请求原样回源有些会改写方法有些干脆拒绝服务。这意味着如果你的业务要过 CDNPUT/DELETE 的行为就更难预测。为了规避这些未知行为很多架构师在做接入层设计时直接就约定写操作统一走 POST——反正 POST 在 CDN 链路上的处理逻辑是明确且一致的省去了大量排障成本。3. 语义错位真实业务里的更新/删除和 HTTP 标准根本不是一回事3.1 PUT 的全量替换语义在部分更新面前有多尴尬教科书里 PUT 是全量替换要求客户端提交资源的最新完整状态。但实际业务里的更新几乎都是部分字段更新改个昵称、调个库存数量、更新一条订单的状态。客户端往往只拿到了一部分最新数据让它把整个资源对象完整提交回来既不现实也没必要。更麻烦的是并发场景。客户端 GET 了一个资源用户改了其中某个字段提交 PUT但服务端在这期间可能已经被其他操作改过了。客户端并不知道它拿到的是旧版本贸然 PUT 回去就会把服务端的新数据覆盖掉——这就是经典的 lost update 问题。要解决它你得引入版本号、乐观锁、条件更新If-Match而这些机制在标准 PUT 语义里并不是默认就有的。一旦要加各种业务约束PUT 这个全量替换的简洁性反而成了包袱。PATCH 方法本可以救场它专门为局部更新设计。但很多团队在评估之后还是放弃了PATCH 的请求体格式有好几套标准JSON Patch、JSON Merge Patch不同框架支持程度不一再引入一种写方法就意味着文档、SDK、网关策略都要跟着扩。既然 POST 什么都能干为什么要为局部更新专门保留一种方法这个逻辑一旦成立PUT 出局就是时间问题。3.2 所谓的删除九成是 UPDATE 操作DELETE 的尴尬是另一种。业务上用户点了删除数据库里真正执行的往往是UPDATE users SET deleted_at now() WHERE id ?——也就是逻辑删除、软删除。为什么要软删除因为数据要留底、订单要追溯、账号要能恢复物理删掉是最后手段。于是你让调用方发一个DELETE /users/{id}后端却在执行 UPDATE 语句语义上就拧巴了。而且真实业务里的删除往往是一连串复杂动作删除前要检查有没有关联数据删除时要做级联清理删除后要写审计日志、发异步通知、甚至触发审批流。这一整套流程已经不是删除资源能概括的了它更像执行一个删除业务流程。用 DELETE 方法表达业务流程就像用一把螺丝刀去钉钉子——能用但怎么看都别扭。我见过最有意思的案例某团队为了避免逻辑删除语义上的拧巴把所有对外删除接口全部改成POST /resource/{id}/archive、POST /resource/{id}/deactivate这类动作路径。结果反而得到了意外收益——调用方一眼就明白这个操作背后有业务规则再也不用猜 DELETE 到底是物理删还是逻辑删。3.3 DELETE 带 Body 的雷谁踩谁知道还有一个技术细节让 DELETE 很难受请求体。很多删除操作是需要携带参数后才能执行的比如删掉这批选中的 100 条记录排除其中处于锁定状态的 5 条。你希望把参数放在 body 里传给服务端。问题是RFC 规范没有禁止 DELETE 带 body但大量 HTTP 客户端库、代理服务器、网关中间件对DELETE body的支持非常随意。有的库直接不支持 DELETE 携带 body有的中间设备在转发时会悄悄把 body 丢掉还有的框架在读取 DELETE 请求体时行为不一。为了绕开这些坑有些团队把删除参数全部塞进 URL 的 query string结果遇到长列表参数就爆了长度限制还要处理各种转义问题。最后大家达成共识删除要传复杂参数直接用 POST一了百了。3.4 安全合规视角能不暴露真实删除能力就不暴露从安全角度看RESTful 的DELETE /users/{id}太直白了。自动化扫描工具可以轻松识别出这是一个删除接口接着就可能发起越权删除尝试、批量探测、恶意调用。不是说方法叫 DELETE 就一定不安全而是说你用一个语义极其明确的方法等于把攻击面明晃晃地摆在了对手面前。改成POST /users/{id}/deactivate或者POST /api/users body 里指定 action至少把删除动作藏在了一个语义模糊的地方。攻击者一眼扫过去不容易判断这个接口到底是干嘛的。这种模糊化解决不了真正的安全问题核心还是靠鉴权、权限、风控但确实能减少自动化扫描带来的噪音和定向攻击的尝试。在安全合规压力大的行业里这个理由足以说服团队放弃 DELETE。4. 大厂和 AI 时代给出的答案POST 万能法及其代价4.1 纯 POST 的 RPC 潮流动词放在 URL 里当 PUT/DELETE 被集体嫌弃之后最主流的替代方案就是POST 万能法——所有写操作统一 POST把动作放在 URL 路径里表达。于是你会看到这样的接口POST /users/123/update POST /users/123/deactivate POST /orders/456/cancel POST /invoices/789/pay这种风格实际上是回归了 RPC远程过程调用URL 从资源定位变成了过程调用路径。它对团队的好处非常实在不需要纠结方法语义不需要担心网关放行问题请求体随便构造所有逻辑都塞进 POST 里。很多互联网公司的新业务接口就是这样设计的尤其是面向内部的系统间调用。我见过不少团队内部上千个接口清一色 GET/POST几乎没有 PUT/DELETE。开发效率确实高新同事上手快不用理解 RESTful 那套资源导向的设计哲学。4.2 统一 POST Action 参数的风格更极端一点的方案是统一 POST Action 参数所有接口都是POST /api具体执行什么动作靠 body 里的字段区分。国内不少云厂商的 OpenAPI 走的就是这条路子。一个典型的请求长这样POST /api HTTP/1.1 Content-Type: application/json { Action: DeleteInstance, InstanceId: i-123456, Region: cn-north-1 }接口路径是同一个全靠参数驱动。这套风格的优点是对网关、日志审计、权限策略都极其友好——规则匹配只需要针对一个 URL方法也只有一个 POST。但缺点也很明显接口完全没有自描述性调用方必须依赖文档才能知道怎么拼参数IDE 帮不上忙调试也费劲。4.3 AI 大模型 API 的示范效应全网都在学 POST这一节必须单独说因为它是过去两年里最被低估的API 教育力量。现在主流的大模型推理 API——不管哪家的——清一色是POST /v1/chat/completions这种风格一个 POST 请求body 里塞模型名、消息列表、参数然后等返回。没有 PUT没有 DELETE没有 PATCH全宇宙只有 POST。为什么因为这些大模型 API 的本质是动作型接口——你提交一段文本让服务端去推理生成它不是对某个资源的 CRUD 操作。你没法PUT 一个提示词让它生成更好的文本也没法DELETE 掉某次推理。用 POST 表达请帮我做一件有副作用的事再合适不过了。问题在于示范效应太强了。新一代开发者进入行业时接触的第一批 API 很可能就是大模型 API——调用方式就是把 JSON 扔给一个 POST 地址。他们天然认为API 就该长这样。于是我们在设计新的内部接口时越来越多听到这样的声音为什么不能像调大模型那样直接 POST 一把梭AI 时代用 POST 的惯性正在以惊人的速度重塑整个行业的 API 设计风格。4.4 折中派GET POST 少量 PATCH当然也不是所有大厂都走向POST 万能。Google Cloud 的 API 设计指南就保持了比较克制的路线更新资源用 PATCH自定义操作cancel、archive 这类动词用 POSTDELETE 保留在确实需要物理删除/资源回滚的地方。这套方案在网络兼容性和语义清晰度之间取得了平衡也是我个人比较认可的方向。还有一些团队的做法是对外接口只暴露 GET/POST但在网关层做了一个内部适配——对外方法收敛对内的后端服务之间保留完整的 RESTful 方法语义。也就是对外你只看到 POST对内实际转发的是 DELETE 或 PUT。这也是一种兼顾外部现实与内部治理的思路。4.5 收敛的代价语义模糊、重试风险、监控失明POST 万能法不是没有成本这些代价在被大量使用后逐渐显现。第一是幂等提示的丢失。PUT/DELETE 本身是幂等的方法名就告诉调用方可以放心重试POST 则不然重发一次可能产生两条订单、扣两次款。收敛到 POST 之后所有请求都不能再依赖方法层的幂等保证必须额外设计幂等键机制、去重表、或者让业务逻辑自己保证幂等。很多团队一开始没意识到这一点上线后大量出现网络重试导致重复扣款/重复创建的线上事故。第二是监控与运维维度的丢失。监控系统、日志分析、容量规划通常会按 HTTP 方法做聚合统计。全用 POST 之后你无法快速区分哪些是读请求、哪些是写请求哪些是资源创建、哪些是动作触发。虽然可以通过 URL 路径再细分但维度明显变粗了。有一次我帮朋友排查线上问题发现他们的监控面板上 POST 请求量占了 99%读和写的趋势根本看不出来最后只能自己写脚本按 URL 前缀重新聚合。第三是接口的可发现性变差。RESTful 设计的优势之一是自描述——一个 DELETE 方法配上资源路径语义八九不离十。收敛成 POST 动词 URL 之后接口变成了各种动词的组合文档维护不到位的话调用方根本不知道该用哪个路径。很多团队最后被迫专门写一份动作字典来弥补语义模糊的问题。下面这张表基本概括了各方案的取舍方案语义清晰度网络兼容性方法级幂等提示适用场景严格 RESTGET/POST/PUT/DELETE/PATCH高低容易被中间设备拦截有资源型 CRUD对象/KV 存储纯 POST 动词 URL中高无需额外幂等机制复杂业务流程、动作型操作统一 POST Action 参数低最高无完全依赖业务保证云厂商 OpenAPI、海量规则匹配GET POST PATCH 自定义方法较高中PATCH 无得靠自己大规模资源型 APIGoogle 风格5. 我的决策框架什么场景该放弃 PUT/DELETE什么场景该坚持5.1 先分清你的 API 是资源型还是动作型看多了大家的取舍之后我形成了一个决策框架开头永远先问一个问题这个接口是资源操作还是业务流程触发如果它真的是资源操作——比如保存一个文件、更新一条配置、删掉一个缓存键、覆盖一个对象——那 PUT/DELETE 依然是最优解。对象存储、KV 存储、配置中心这些场景的标准接口到今天依然是 PUT/DELETE因为资源边界清晰、客户端持有完整状态、幂等语义天然匹配没有任何理由改 POST。如果你的操作本质是让服务端做一件有副作用的事——生成一段文本、下一笔订单、取消一个任务、执行一次转账——那就是动作型接口直接用 POST 动作路径。强行套 PUT/DELETE 只会让语义拧巴让调用方猜你的更新到底是全量还是增量、删除是物理还是逻辑。5.2 对外 API 求稳对内 API 求准第二个判断维度是接口的调用环境。对外公共 API 面对的是不可控的网络环境调用方可能在老旧企业内网、经过各种奇怪的代理、用着不维护的 SDK。这种情况下我倾向于让方法收敛到最稳的组合——通常就是 GET 和 POST——哪怕牺牲一点语义优雅也要保障可用性。网络兼容性在此刻压倒一切。内部服务 API 则不一样环境受控团队可以约定标准网关策略自己说了算。在这种情况下我建议尽量保留完整的语义分层。内部系统之间调用极其频繁接口设计标准化带来的长期收益——清晰的资源操作、幂等约定、监控维度——会随着系统数量增长越来越大。5.3 如果被迫收敛怎么收得体面如果你也遇到了网关不让用 DELETE或者领导要求统一 POST的情况我建议别硬刚而是在架构上做适配尽量把损失降到最低。一种做法是网关层做方法映射对外暴露的接口语义清晰比如DELETE /users/{id}网关收到请求后内部转换成 POST 转发给后端服务或者后端服务本身就是完整 RESTful 的只是对外入口被网关收敛了。这样既不损失内部语义也满足了外部限制。另一种做法是在 SDK 层面做封装底层请求全走 POST但 SDK 暴露给业务方的方法名是deleteUser()、updateProfile()这种语义明确的名字。调用方根本感觉不到方法的差异所有兼容性处理都被 SDK 吞掉了。不管用哪种方式有两件事必须做第一所有可能被重试的请求必须有幂等机制要么幂等键要么业务去重别把重试风险留给调用方第二文档里必须明确写清楚哪些请求安全可重试、哪些不是这是方法收敛之后最容易缺失的信息。5.4 个人踩坑记录与最终体会我自己在这个问题上也反复横跳过。早些年我是 RESTful 原教旨主义者谁把DELETE /users/{id}改成POST /users/{id}/delete我就跟谁急。后来在真实项目里被网关拦、被安全扫描逼、被业务语义拧巴折磨过几轮之后我慢慢松动了。最戏剧化的一次是某个项目因为网关限制不得不把删除接口从 DELETE 改成POST /projects/{id}/archive。我当时觉得这是妥协结果上线后调用方反馈反而更好了——因为archive这个动作让所有人都清楚这是归档而非物理删除业务规则一步到位就传达到了客户端。很多你以为的妥协在语义层面反而可能歪打正着。现在我的态度很简单方法选型是工程决策不是品位问题。我见过纯 POST 风格跑得非常顺的团队也见过严格 RESTful 维护得很好的系统。决定一个接口好不好的不是它用了哪种 HTTP 方法而是它的语义是否清晰、出错时是否可诊断、重试时是否安全、团队是否容易理解。你正在设计新接口的话就先问自己三个问题调用方是机器还是人网络链路上有多少层不可控的中间设备这个操作的本质是资源变更还是业务动作把这三个问题想清楚PUT 和 DELETE 到底该不该用答案其实就在那里。