跳至正文

REST

Representational State Transfer

REST 是一种基于 HTTP 语义的 API 设计风格——把系统抽象为资源,用 URL 定位资源、用 HTTP 方法表示操作、用 HTTP 状态码表示结果。是公开 API 的默认选择,生态和基础设施最成熟。

Note

特点

  • 用 URL + HTTP 方法即可定义接口,客户端用任何 HTTP 库都能调用
  • 天然兼容 HTTP 缓存(Cache-Control / ETag)和 CDN
  • 浏览器、日志、监控、防火墙、代理全链路原生支持
  • 无状态,任意实例可处理任意请求,水平扩展简单

Note

局限

  • 没有内置类型系统:需要 OpenAPI + codegen 才能获得类型安全
  • Over-fetching / Under-fetching:服务端决定返回结构,客户端可能拿到多余字段或需要发多次请求
  • API 演进需要版本号:新增字段一般安全,删改字段需 /v2/ 或 Header 切换
  • RPC 风味操作难以表达结算购物车发送验证码 这类动词操作硬塞进资源模型会很别扭

Note

适用场景

  • 对外公开 API、第三方集成(GitHub v3、Stripe 等大多数公共 API)
  • 简单 CRUD、资源模型清晰的业务
  • 需要 HTTP 缓存 / CDN / 标准监控 / 代理链路兼容
  • 多语言客户端(没有共享类型环境)

REST 的五个层次

Leonard Richardson 提出的成熟度模型(RMM):

Level特征状态
0单一端点、单一方法(POST /api 全部操作)伪 REST
1资源分离(/users/orders普通
2正确使用 HTTP 方法 + 状态码业界大多数「REST」停在这里
3HATEOAS(响应里带下一步可做的操作链接)少见,成本高

Note

要点

业界说的 REST 几乎都是 Level 2。追求 Level 3 的投入产出比在多数项目里不划算,知道它存在即可。

资源与 URL

核心思想:URL 是名词(资源),HTTP 方法是动词

✅ GET    /users              获取用户列表
✅ GET    /users/1            获取单个用户
✅ POST   /users              创建用户
✅ PUT    /users/1            整体替换用户
✅ PATCH  /users/1            局部更新
✅ DELETE /users/1            删除用户
✅ GET    /users/1/orders     用户的订单(嵌套资源)

❌ GET  /getUserList          动词塞 URL
❌ POST /deleteUser?id=1      POST 当万能方法

Note

嵌套深度建议不超过 2 层

  • /users/1/orders
  • /users/1/orders/9/items ⚠️ 可以,但开始笨重
  • /users/1/orders/9/items/5/comments ❌ 改成 /items/5/comments

深嵌套 URL 会让缓存键、权限校验、路由定义都变复杂。

HTTP 方法的语义

方法幂等安全典型用途
GET读取资源,不改变状态
HEAD只取响应头(检查是否存在、获取 ETag)
POST创建资源、非幂等操作
PUT整体替换资源(同样请求重发结果相同)
PATCH⚠️局部更新(是否幂等取决于实现)
DELETE删除资源(重复 DELETE 的结果相同)

Note

幂等与安全

  • 安全(Safe):不改变服务端状态,可任意重发。只有 GET / HEAD / OPTIONS
  • 幂等(Idempotent):多次执行效果等同于一次。GET / PUT / DELETE 幂等,POST 不幂等
  • 幂等是重试的基础:网络超时后客户端重试 PUT 安全,重试 POST 要防重复提交(用 Idempotency-Key 头)

状态码的常见用法

有语义的状态码,让代理、监控、客户端能正确理解响应。

类别典型含义
2xx 成功200 OK通用成功
201 Created创建资源成功(Location 头带新资源 URL)
204 No Content成功但无响应体(DELETE、PUT 后常用)
3xx 重定向301 Moved Permanently永久重定向
304 Not Modified缓存命中(配合 ETag / Last-Modified)
4xx 客户端错误400 Bad Request参数错误
401 Unauthorized未认证
403 Forbidden已认证但无权限
404 Not Found资源不存在
409 Conflict资源冲突(重复创建、版本冲突)
422 Unprocessable Entity参数格式对但业务规则不通过
429 Too Many Requests频率限制
5xx 服务端错误500 Internal Server Error未捕获异常
502 Bad Gateway上游异常
503 Service Unavailable服务不可用(维护、过载)
504 Gateway Timeout上游超时

Note

反模式:永远 200

部分项目把业务错误全塞进响应体 { code: -1, message: "..." },HTTP 层永远 200。这会让:

  • HTTP 缓存错误地缓存失败响应
  • 监控无法按状态码统计错误率
  • 客户端 / SDK 不能直接用标准错误处理

老接口为了历史兼容可以保留,新接口应该用正常状态码。

分页、排序、过滤

URL 的 query string 是事实标准。

GET /users?page=2&pageSize=20                 分页
GET /users?sort=-createdAt                    排序(前缀 - 表示降序)
GET /users?filter[status]=active              过滤
GET /users?fields=id,name                     字段筛选(可选)
GET /users?cursor=xxx&limit=20                游标分页(大数据集)

Note

分页风格

  • 偏移分页page + pageSize):简单、可跳页,但深翻页慢、新增数据会错位
  • 游标分页cursor + limit):稳定、高效,但不能跳页

大数据集 / 流式展示用游标分页,后台表格用偏移分页。

缓存

REST 的核心优势,GraphQL / tRPC / gRPC 都没有的能力。

客户端 / CDN 缓存

Cache-Control: public, max-age=3600           共享缓存可存 1 小时
Cache-Control: private, max-age=60            仅浏览器缓存
Cache-Control: no-store                       禁止任何缓存

条件请求(ETag / Last-Modified)

响应:
  ETag: "abc123"
  Last-Modified: Sat, 12 Apr 2025 10:00:00 GMT

下次请求:
  If-None-Match: "abc123"
  If-Modified-Since: Sat, 12 Apr 2025 10:00:00 GMT

→ 未变化时服务端返回 304 Not Modified(无响应体,省带宽)

Note

要点

  • Cache-Control 控制多久内不发请求
  • ETag / Last-Modified 控制发了请求但内容没变时省带宽
  • 两者常配合使用:max-age=60 + ETag

类型安全:OpenAPI

REST 本身没有类型系统,需要用 OpenAPI(Swagger)弥补。

paths:
  /users/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      type: object
      required: [id, name]
      properties:
        id: { type: string }
        name: { type: string }
        email: { type: string, format: email }

基于 OpenAPI Schema 可以:

  • codegenopenapi-typescript / openapi-generator 生成前后端类型
  • 文档:Swagger UI / Redoc 自动渲染交互式 API 文档
  • 校验ajv 等库运行时校验请求 / 响应

Note

代价

OpenAPI 需要手动维护(或用框架自动从代码生成,如 NestJS @nestjs/swagger、FastAPI、Hono RPC)。比 tRPC 的”零 codegen 自动推导”和 GraphQL 的”Schema 即文档”多一步。

CORS

浏览器对跨源请求有同源策略,REST 作为公开 API 必须配置 CORS 才能被第三方 Web 客户端调用。

场景关键响应头
简单请求(GET / 简单 POST)Access-Control-Allow-Origin
预检请求(PUT / DELETE / 自定义头)浏览器先发 OPTIONS,服务端用 Access-Control-Allow-Methods / Allow-Headers 回应
带 Cookie / Authorization客户端 credentials: "include",服务端 Access-Control-Allow-Credentials: true,且 Allow-Origin 不能是 *

Note

陷阱

  • 生产环境不要用 Access-Control-Allow-Origin: *,至少按域名白名单
  • 带凭证的请求用 * 浏览器直接拒绝
  • 预检请求频繁时用 Access-Control-Max-Age 缓存(秒),减少 OPTIONS 开销
  • 不是所有跨源错误都是 CORS 问题:网络断开、404 也会在 DevTools 里显示”CORS error”,先看服务端日志确认请求是否到达

错误响应建议

统一错误结构方便客户端处理。RFC 7807 的 Problem Details 是常见选择:

{
  "type": "https://example.com/errors/insufficient-balance",
  "title": "Insufficient balance",
  "status": 422,
  "detail": "Your balance is 10 but 50 is required.",
  "instance": "/orders/9"
}

自研格式至少包含:HTTP 状态码(在响应头里)、业务 code(可选)、message、traceId(方便排查)。

与其他方案的对比

详细对比见:

选型概览:

场景推荐
对外 API、第三方集成REST
多客户端共用 API、灵活查询GraphQL
TS 全栈、同团队 monorepotRPC
微服务内部、多语言、高性能gRPC

最小示例(Node.js + Express)

import express from "express";

const app = express();
app.use(express.json());

const users = new Map();

app.get("/users", (req, res) => {
  res.json([...users.values()]);
});

app.get("/users/:id", (req, res) => {
  const user = users.get(req.params.id);
  if (!user) return res.status(404).json({ error: "User not found" });
  res.json(user);
});

app.post("/users", (req, res) => {
  const id = crypto.randomUUID();
  const user = { id, ...req.body };
  users.set(id, user);
  res.status(201).location(`/users/${id}`).json(user);
});

app.delete("/users/:id", (req, res) => {
  if (!users.delete(req.params.id)) {
    return res.status(404).json({ error: "User not found" });
  }
  res.status(204).end();
});

app.listen(3000);

回到 API 通信总览