过去几年,GraphQL、gRPC、tRPC、BFF、AI Agent 工具接口都很热,但在大多数业务系统里,REST 仍然是最常见、最容易落地、最容易协作的接口风格。
原因很简单:REST 足够朴素。
它基于 HTTP,不要求客户端和服务端共享复杂协议,也不要求团队一开始就搭建完整的 IDL、网关和代码生成体系。前端、移动端、后端、测试、运维都能理解它。一个接口文档写清楚 URL、Method、参数、返回值和状态码,基本就能协作。
但也正因为 REST 看起来简单,很多项目最后会写成“披着 HTTP 外衣的随意 RPC”:所有接口都是 POST /doSomething,状态码永远 200,错误都塞在 message 里,资源命名混乱,分页和过滤各写各的。短期能跑,长期会很难维护。
这篇文章讲的不是 REST 教科书,而是传统业务系统里更实用的一套 REST 设计习惯。
REST 的核心:围绕资源设计
REST 最重要的思想不是“用 JSON”,也不是“用 HTTP”,而是围绕资源建模。
资源可以是用户、订单、商品、文章、评论、文件、任务。接口设计时,URL 表达资源,HTTP Method 表达动作。
比如订单资源:
GET /orders 查询订单列表
POST /orders 创建订单
GET /orders/1001 查询单个订单
PUT /orders/1001 整体更新订单
PATCH /orders/1001 部分更新订单
DELETE /orders/1001 删除订单
这种设计的好处是清晰。看到接口就能大致知道它操作什么资源、做什么动作。
反过来,下面这种写法就不太 REST:
POST /getOrderList
POST /createOrder
POST /updateOrderStatus
POST /deleteOrder
它不是不能用,而是把动作都塞进 URL 里,HTTP Method 失去了语义。接口少的时候问题不大,一旦系统变复杂,命名就会越来越乱。
一个典型 REST 请求流程

一个好的 REST 接口,不只是 Controller 里能返回数据,还要在流程中的每一层保持清楚边界:网关处理通用流量问题,Controller 负责协议和参数,Service 负责业务逻辑,Repository 负责数据访问,DTO 负责对外表达。
HTTP Method 要用对
传统 REST 里最容易混乱的是 Method。
常见约定如下:
| Method | 含义 | 是否应该幂等 |
|---|---|---|
| GET | 查询资源 | 是 |
| POST | 创建资源或提交动作 | 否 |
| PUT | 整体替换资源 | 是 |
| PATCH | 局部更新资源 | 通常是 |
| DELETE | 删除资源 | 是 |
幂等的意思是:同一个请求执行一次和执行多次,最终结果一致。
比如:
DELETE /orders/1001
执行一次是删除订单,再执行一次仍然是“订单不存在”,最终状态没有变化,所以它是幂等的。
而:
POST /orders
每执行一次都可能创建一个新订单,所以不是幂等的。
实际业务里,有些动作很难完全资源化,比如“支付订单”“取消订单”“提交审批”。这时可以把动作建模成子资源或业务操作:
POST /orders/1001/payment
POST /orders/1001/cancellation
POST /approval-tasks/2001/submission
不要为了追求形式上的 REST,把所有业务动作硬拧成 PUT /orders/1001。工程设计要讲语义,也要讲可读性。
状态码不要永远 200
很多系统喜欢这样返回:
{
"code": 500,
"message": "server error",
"data": null
}
HTTP 状态码却永远是 200 OK。
这种做法对调试、网关、监控、SDK、缓存都不友好。更合理的方式是让 HTTP 状态码表达协议层结果,让响应 body 表达业务细节。
常用状态码:
| 状态码 | 含义 |
|---|---|
| 200 | 请求成功 |
| 201 | 创建成功 |
| 204 | 成功但无响应体 |
| 400 | 参数错误 |
| 401 | 未登录或 token 无效 |
| 403 | 已登录但无权限 |
| 404 | 资源不存在 |
| 409 | 资源冲突 |
| 422 | 语义校验失败 |
| 429 | 请求过多 |
| 500 | 服务端异常 |
错误响应可以统一成这样:
{
"error": {
"code": "ORDER_STATUS_INVALID",
"message": "当前订单状态不允许取消",
"requestId": "req_01HZX8K7"
}
}
code 给程序判断,message 给人看,requestId 给排障用。
查询、分页和排序要统一
列表接口是 REST 系统里最容易长歪的地方。
建议统一使用 query string:
GET /orders?status=paid&createdAfter=2026-05-01&page=1&pageSize=20&sort=-createdAt
常见约定:
page / pageSize:传统分页
limit / offset:偏移分页
cursor / limit:游标分页
sort=-createdAt:按创建时间倒序
sort=createdAt:按创建时间正序
如果数据量大,或者列表会频繁新增,游标分页比 page 分页更稳:
GET /orders?cursor=eyJpZCI6MTAwMX0=&limit=20
返回:
{
"items": [],
"nextCursor": "eyJpZCI6MTAyMX0=",
"hasMore": true
}
不要每个接口各自发明分页字段。统一约定能明显降低前后端沟通成本。
版本管理要提前设计
接口一旦被客户端使用,就不能随意改。
常见版本方式有三种:
/api/v1/orders
Accept: application/vnd.company.v1+json
?apiVersion=1
业务系统最常用的是 URL 版本:
GET /api/v1/orders
它不一定最优雅,但最直观,网关、文档、测试都容易处理。
版本管理的重点不是路径怎么写,而是不要破坏已有客户端。新增字段通常是兼容的,删除字段、改变字段含义、改变枚举值、改变错误结构,都是高风险变更。
鉴权和权限要分清
REST 接口里经常混淆两个概念:
Authentication:你是谁
Authorization:你能做什么
前者通常通过 token、session、API key、OAuth2 完成。后者要结合角色、资源归属、租户、数据权限判断。
比如:
GET /orders/1001
Authorization: Bearer <token>
服务端不仅要验证 token 是否有效,还要判断当前用户是否有权访问订单 1001。
典型错误是只做登录校验,不做资源级权限校验。这样用户只要猜到 ID,就可能越权访问别人的数据。
REST 的优点和边界
REST 的优点很明显:
- 基于 HTTP,生态成熟;
- 对人友好,容易调试;
- 对缓存、代理、网关支持好;
- 前后端协作成本低;
- 适合 CRUD 和大多数业务接口。
但它也有边界。
当客户端需要一次请求灵活选择字段、组合多个资源时,GraphQL 可能更合适。
当服务之间追求高性能、强类型、低延迟通信时,gRPC 可能更合适。
当系统内部动作强过程化,比如复杂工作流、批处理任务、命令调度时,RPC 风格也未必比 REST 差。
REST 不是银弹。它适合的是稳定、清晰、资源导向的业务接口。
实践建议
第一,URL 用名词,不要用动词。
GET /users/1
POST /orders
PATCH /products/10
第二,状态码要有语义,不要全部返回 200。
第三,请求和响应 DTO 要稳定,不要直接暴露数据库实体。
第四,分页、排序、错误结构、时间格式要统一。
第五,敏感操作要考虑幂等,比如支付、退款、创建订单,可以使用 Idempotency-Key。
POST /payments
Idempotency-Key: 8f0b2b4a-7f2e-4d8a
第六,接口文档要和代码一起维护。OpenAPI / Swagger 不一定完美,但比口口相传可靠。
总结
传统 REST 之所以还在大量系统里使用,不是因为它新,而是因为它稳。
它把接口设计约束在一个简单模型里:
资源用 URL 表达
动作由 HTTP Method 表达
结果由状态码表达
细节由 JSON body 表达
真正写好 REST,不靠复杂框架,而靠一致性。命名一致、状态码一致、错误结构一致、分页一致、权限判断一致,系统就会越来越好维护。
新技术值得学,但 REST 仍然是后端工程师绕不开的基本功。很多系统的问题不是 REST 过时了,而是从一开始就没有认真设计过 REST。