HTTP 请求方法与 RESTful 接口设计:GET 和 POST 到底怎么用

凡是写过接口或者调过接口的人,都见过这样的场景:一个系统里所有接口清一色用 POST,查询用户是 POST,删除订单也是 POST,靠路径里的 /getUser、/deleteOrder 来区分操作。这样的接口能跑,但在接手别人代码时,你得读完每个接口的文档才知道它干什么;而一套设计良好的 RESTful 接口,光看路径和请求方法,就能猜出七八成。

这篇文章讲清三件事:HTTP 的几个请求方法各自是什么含义、RESTful 接口设计的具体规范、以及实际开发中围绕 GET 和 POST 的那些常见争议。不涉及高深理论,目标是让你下次设计接口时有一套可以照着做的标准。示例以 HTTP/1.1 协议为准,配少量 JavaScript 调用代码。

1 请求方法:动词决定语义

1.1 方法不是随便选的

HTTP 的请求方法(GET、POST、PUT、DELETE 等)不是单纯的 "传数据的方式",每个方法自带明确语义,服务器和中间设备(浏览器缓存、代理、网关)会依据这些语义做不同的处理。日常开发用得到的是五个:

方法 语义 典型场景
GET 获取资源 查询用户列表、获取商品详情
POST 新建资源 创建订单、提交注册
PUT 整体更新资源 全量修改用户资料
PATCH 部分更新资源 只改用户昵称
DELETE 删除资源 删除一条评论

一句话概括:请求方法表达的是 "对资源做什么操作",而不是 "数据怎么传输"。理解了这一点,很多争议就迎刃而解。

除了语义,还有两个属性值得了解。一是安全性:GET、HEAD 这类方法被设计为 "只读",不改变服务器状态,所以叫安全方法;二是幂等性:同一个请求执行一次和执行多次,效果相同。GET 天然幂等,PUT 和 DELETE 被设计为幂等(删除第二次时资源已经不在了,但状态不变),POST 不幂等,重复提交会创建多份资源。这两个属性不只是概念,它们直接关系到浏览器能否缓存 GET 响应、重试机制敢不敢自动重发请求,第 4 节会用到。

1.2 GET 与 POST 的实质区别

这两个方法占了日常开发的九成以上,值得单独说清。除了语义上一个读一个写,它们的机制差异主要有三点:

  • 参数位置: GET 的参数放在 URL 的查询串里,形如 ?page=2&size=10;POST 的数据放在请求体里。这决定了 GET 的 URL 会被浏览器历史、服务器日志完整记录,所以密码、令牌这类敏感信息绝不能放在 GET 参数里。
  • **长度限制:**URL 的长度受浏览器和服务器限制(通常几 KB),GET 不适合传大块数据;POST 的请求体可以承载文件和长文本。
  • **缓存与重试:**GET 是安全且幂等的,浏览器可以缓存它的响应,请求失败时也可以放心自动重试;POST 不幂等,自动重试可能导致重复下单,这类请求的重试必须由业务代码谨慎控制。

反过来看一个常见误解:很多人说 "POST 比 GET 安全,因为参数看不见"。这只对了一半------POST 的请求体虽然在地址栏看不到,但在抓包工具面前同样是明文。真正的传输安全靠 HTTPS,不靠请求方法。


2 RESTful:把方法与路径组合成一套语言

2.1 核心思想:路径是名词,方法是动词

RESTful 的核心只有一条:URL 指向资源(名词),HTTP 方法表达操作(动词)。两者组合起来,一个接口的用途便一目了然。

拿用户管理来对比。不规范的写法:

bash 复制代码
POST /getUserById      获取用户
POST /createUser       创建用户
POST /updateUser       更新用户
POST /deleteUser       删除用户

RESTful 的写法:

bash 复制代码
GET    /users/42       获取 ID 为 42 的用户
POST   /users          创建用户
PUT    /users/42       整体更新 ID 为 42 的用户
PATCH  /users/42       部分更新 ID 为 42 的用户
DELETE /users/42       删除 ID 为 42 的用户

同样四个功能,第二种写法里资源(/users/42)和操作(GET、PUT、DELETE)各司其职。读代码的人看到 DELETE /users/42,不需要查文档就知道是删除指定用户。这就是 1.1 节说的 "方法自带语义" 落到实处。

2.2 路径设计的几条规范

围绕 "资源" 这个概念,有几条约定俗成的规则:

  • **用名词复数:**集合用 /users 而不是 /user,单个资源用 /users/42;
  • **层级表达从属关系:**某用户的订单列表写成 /users/42/orders,一眼看出归属;
  • 过滤、分页、排序放查询串:/users?page=2&size=10&sort=created_at,这些属于 "怎么取",不是资源本身的一部分;
  • **路径里不出现动词:**操作由请求方法表达,不要写成 /users/42/delete;
  • 路径全用小写单词,多个单词用连字符分隔,如 /order-items。

按这些规则,一套订单接口的完整形状大致是:

bash 复制代码
GET    /orders                   分页查询订单列表
GET    /orders/1001              查询某笔订单详情
POST   /orders                   创建订单
PUT    /orders/1001              整体修改订单
PATCH  /orders/1001/status       只修改订单状态
DELETE /orders/1001              取消(删除)订单
GET    /orders/1001/items        查询订单下的商品明细

2.3 状态码:接口的另一种表达

RESTful 的另一半是正确使用 HTTP 状态码。响应状态码是协议层面的 "结果通报",客户端不看响应体就该知道请求的大致结果。日常最该用对的是这几个:

状态码 含义 使用场景
200 成功 GET、PUT、PATCH 成功返回
201 已创建 POST 创建资源成功
204 无内容 DELETE 成功,响应体为空
400 请求有误 参数缺失或格式错误
401 未认证 未登录或令牌失效
403 无权限 已登录但权限不足
404 资源不存在 查询或删除的对象不存在
500 服务器错误 服务端异常

很多项目里所有响应一律返回 200,把业务成功与否塞进响应体的 code 字段里让客户端自己判断。这种做法能跑,但等于放弃了协议层的信息,监控工具、网关、浏览器开发者工具都无法从状态码直接识别错误。至少做到:错误返回 4xx 或 5xx,创建成功返回 201,删除成功返回 204。


3 用代码走一遍

用 JavaScript 的 fetch 调用上面那套订单接口,看各类请求在客户端的写法差异:

javascript 复制代码
// 查询订单列表:GET,参数放查询串
const res = await fetch("/orders?page=1&size=10&status=paid");
const list = await res.json();

// 创建订单:POST,数据放请求体,需声明内容类型
const createRes = await fetch("/orders", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    productId: 88,
    quantity: 2,
  }),
});
// 创建成功应返回 201,响应体里带新订单的 ID
const created = await createRes.json();

// 只改订单状态:PATCH,请求体只传变化的字段
await fetch("/orders/1001/status", {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ status: "shipped" }),
});

// 删除订单:DELETE,成功应返回 204,无响应体
await fetch("/orders/1001", { method: "DELETE" });

有三个细节值得留意。第一,POST 和 PATCH 的请求体要在 Content-Type 里声明格式,最常见的是 application/json;不声明的话,服务端可能无法正确解析请求体。第二,PATCH 的请求体只包含要改的字段,这是它和 PUT 的分工------PUT 表达 "用这份完整数据替换资源",PATCH 表达 "只更新这几个字段",语义上不要混用。第三,DELETE 成功后不要期待响应体里有内容,204 本身就表示 "处理完了,没有东西返回"。


4 实际开发中的几个常见问题

规范讲完了,落地时总会遇到一些规范没有直接回答的问题,这里集中处理。

4.1 查询条件复杂时,GET 还放得下吗?

多条件组合查询的参数可能很长,有人因此改用 POST 来做查询。多数情况下不必要:查询串承载十来个参数毫无压力,真正超长时说明查询条件本身设计得过于复杂,优先考虑精简参数或改用 POST 请求体传结构化条件。判断标准回到语义------只要是读操作,就别为了参数长而改用 POST,否则会失去 GET 的可缓存性和可重试性。

4.2 浏览器表单只支持 GET 和 POST,怎么办?

HTML 原生表单确实只有这两个方法,但浏览器只是客户端的一种。前后端分离的项目里,前端用 fetch、axios 发请求,想用什么方法都可以;服务端框架(如 Spring、Express)对 PUT、DELETE 的路由支持也都是现成的。这个限制在实践中影响很小。真正会碰到的是某些企业防火墙或旧网关默认拦截非 GET/POST 请求,这时可以用请求头覆盖(X-HTTP-Method-Override)等方式绕过,属于个别场景的特殊处理。

4.3 DELETE 之后再 GET,该返回什么?

这正是幂等性发挥作用的地方:删除一个不存在的资源,规范的做法是照样返回 204 或 200,而不是 404。因为 "删除后资源不存在" 是符合预期的最终状态,重复删除没有产生副作用。客户端重试一个失败的 DELETE 请求时,第二次拿到成功响应,恰恰说明幂等性在保护它。反过来说,如果你的 DELETE 重复执行会报错甚至删错数据,那说明实现没有遵守幂等约定,需要修的不是客户端,而是接口本身。

4.4 响应体的结构要不要统一?

状态码之外,响应体建议有一层统一的包裹:成功时携带数据,失败时携带错误码和可读的提示信息。一个常见的约定是:

javascript 复制代码
{
  "code": 0,
  "message": "ok",
  "data": { "id": 1001, "status": "paid" }
}

code 为 0 表示业务成功,非 0 时 message 说明原因。这样前端只需要一套统一的处理逻辑,错误提示也能直接展示给用户。注意这和 2.3 节不矛盾:HTTP 状态码负责协议层的成败(让网关、监控看得懂),响应体里的 code 负责业务层的细分错误(让前端看得懂),两层各管一段。

回头看,RESTful 并不是什么高深的架构,它做的事情是把 HTTP 协议里早已存在的语义用起来:路径回答 "操作哪个资源",方法回答 "做什么操作",状态码回答 "结果如何"。写接口时先问自己这三个问题,再把答案分别放进路径、方法和状态码里,一套清晰自明的接口自然就成形了。规范的价值不在条文本身,而在于让下一个读你接口的人------可能是三个月后的你自己------少翻一遍文档。

相关推荐
LK_071 天前
【FastAPI】基础篇 01|开始之前:API、HTTP 与 FastAPI 到底是什么
网络协议·http·fastapi
阿星_1 天前
pydio登陆不了分析问题
网络协议·http·https
Joy T1 天前
Gzip 压缩原理与实践:HTTP 响应压缩、性能收益与工程配置
网络·网络协议·http
DevOpenClub2 天前
网站标题与图标 API:URL 输入、空字段与返回边界
api·favicon·网站工具
ndz3 天前
在 ModelFold 使用 Claude:无需代理,稳定可靠的日常开发体验
人工智能·api
ndz3 天前
在 ModelFold 接入 GPT Image 2.6
api
IT规划师3 天前
API 安全三支柱:认证、防篡改、防重放的工程实践
安全·api
xixingzhe24 天前
AI 对话打字机效果:SSE 和 HTTP 的区别与适用场景
网络·网络协议·http