凡是写过接口或者调过接口的人,都见过这样的场景:一个系统里所有接口清一色用 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 协议里早已存在的语义用起来:路径回答 "操作哪个资源",方法回答 "做什么操作",状态码回答 "结果如何"。写接口时先问自己这三个问题,再把答案分别放进路径、方法和状态码里,一套清晰自明的接口自然就成形了。规范的价值不在条文本身,而在于让下一个读你接口的人------可能是三个月后的你自己------少翻一遍文档。