---
title: REST 与 API 设计
url: https://doc.liz6.com/networking/06-HTTP/05-rest-and-api-design
locale: zh
area: networking
tags:
- networking
- HTTP
date: 2026-06-30
modified: 2026-06-30
description: REST 不是规范,是一组架构约束(HATEOAS、无状态、统一接口)。GraphQL 和 gRPC 各自在不同的约束维度上对 REST 做了取舍——选哪个取决于"谁来决定响应形状"和"性能敏感度"。
---

# REST 与 API 设计

> REST 不是规范,是一组架构约束(HATEOAS、无状态、统一接口)。GraphQL 和 gRPC 各自在不同的约束维度上对 REST 做了取舍——选哪个取决于"谁来决定响应形状"和"性能敏感度"。

## 概述

REST 由 Roy Fielding 在 2000 年博士论文中提出——它**不是协议而是一种架构风格**，主张把系统状态建模成"资源"，用 HTTP 既有的方法语义（GET/POST/PUT/DELETE）和状态码去操作它们，而不是发明一套自定义 RPC。二十多年后，REST 仍是 Web API 的默认范式（GitHub/Stripe/Twilio），而 GraphQL（Facebook 2015）和 gRPC（Google 2016）在"客户端字段需求多变"和"内部高性能 RPC"这两个 REST 不擅长的场景里补位。好 API 的难点从来不是选哪种风格，而是**一致性**：可预测的 URL、正确的方法/状态码语义、统一的错误与分页、清晰的版本与兼容策略。

## REST 六大约束

```
1. Client-Server     : UI 与数据分离, 各自独立演进
2. Stateless         : 每个请求自带全部 session 上下文 → 服务器不存会话 → 易水平扩展
3. Cacheable         : 响应显式声明可缓存性 (Cache-Control / ETag)
4. Uniform Interface : 统一的资源标识与操作语义 (见下方方法表)
5. Layered System    : client 不感知后端分层 (CDN / LB / API gateway)
6. Code on Demand    : (可选) 服务器可下发可执行代码扩展 client
```

`Stateless` 是其中最有现实后果的一条：它让任意一台无状态实例都能处理任意请求，是 REST 能轻松上 CDN、负载均衡、自动扩缩的根因——代价是认证态要塞进每个请求（Bearer token），这也正是 JWT 流行的土壤。

## 方法语义、状态码与幂等性

幂等性（重复执行结果不变）直接决定**客户端能不能安全重试**——网络超时后能否盲重发，全看方法是否幂等。

```
方法     语义        安全  幂等   典型成功码
GET      读取        是    是     200
POST     创建/动作   否    否     201 (+ Location) / 200
PUT      整体替换    否    是     200 / 204
PATCH    部分更新    否    否*    200
DELETE   删除        否    是     204 (再删返回 404 也算幂等结果)
(* PATCH 是否幂等取决于 body 语义; JSON Merge Patch 通常幂等, JSON Patch 的数组操作不是)

状态码别只会 200/500:
  201 Created / 202 Accepted(异步) / 204 No Content
  400 参数错 · 401 未认证 · 403 已认证但无权 · 404 不存在
  409 冲突 · 422 语义校验失败 · 429 限流(配 Retry-After)
  500 服务端错 · 503 暂不可用
```

> **并发控制**：用 `ETag` + `If-Match` 做乐观锁——客户端带上读到的版本号，服务端版本变了就返回 `412 Precondition Failed`，避免"后写覆盖先写"。

## 资源建模与 URL 设计

```
Collection   GET    /users              列表(过滤/排序/分页)
Create       POST   /users              201 + Location: /users/42
Single       GET    /users/42
Replace      PUT    /users/42
Patch        PATCH  /users/42
Delete       DELETE /users/42           204
Sub-resource GET    /users/42/orders
Action       POST   /users/42/actions/reset-password   非 CRUD 动作(显式标注)

反模式:
  GET  /getUser?id=42        URL 里塞动词 (HTTP 方法本身就是动词)
  POST /createUser           冗余 (POST 即创建)
  GET  /users?action=delete  危险 (GET 必须 safe; 删除该用 DELETE)
```

## 错误与分页(被低估的设计)

错误格式要机器可读、全站统一。业界趋同到 **RFC 9457 (Problem Details)**：

```json
{ "type": "https://api.example.com/errors/insufficient-funds",
  "title": "Insufficient funds", "status": 409,
  "detail": "Balance 10 < required 50", "instance": "/accounts/42/transfer/abc",
  "errors": [ {"field": "amount", "message": "exceeds balance"} ] }
```

分页两种主流，按场景选：

```
Offset/limit  : ?offset=40&limit=20   实现简单, 可跳页; 深分页慢 + 翻页时数据漂移
Cursor(keyset): ?after=<opaque>&limit=20   稳定、高效, 适合无限滚动/大数据集; 不能跳页
```

## HATEOAS

REST 的"满分"约束：响应里**内嵌可用动作的链接**，客户端靠跟随链接驱动流程，而非把 URL 规则硬编码。

```json
{ "id": 42, "state": "pending",
  "_links": { "self":   {"href": "/orders/42"},
              "cancel": {"href": "/orders/42/actions/cancel"},
              "pay":    {"href": "/orders/42/payment"} } }
```

理想上服务端能自由改 URL、用状态控制可用动作。现实中**绝大多数 API 不做完整 HATEOAS**——客户端通常仍按文档硬编码路径，收益配不上复杂度。但它的退化形态很有用：分页的 `next` 链接、资源里嵌子资源 URL，都是 HATEOAS 思想的实用残留。

## OpenAPI / Swagger

OpenAPI（前身 Swagger）是描述 REST API 的机器可读规范（YAML/JSON），是 REST 工程化的事实标准：

```yaml
paths:
  /users/{id}:
    get:
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        '200': { content: { application/json: { schema: { $ref: '#/components/schemas/User' } } } }
```

价值在于**一份 spec 派生整条工具链**：Swagger UI 交互文档、客户端/服务端 codegen、请求校验与 mock、契约测试。推荐 **spec-first**（先写契约再写实现），让前后端并行、契约即测试。

## REST vs GraphQL vs gRPC

不是谁取代谁，而是各自的最佳区间：

| | REST | GraphQL | gRPC |
|---|---|---|---|
| 传输/编码 | HTTP/1.1+ · JSON | HTTP · JSON | HTTP/2 · Protobuf(二进制) |
| 取数模型 | 每资源一端点 | 单端点, 客户端声明字段 | 强类型 RPC 方法 |
| 解决的痛点 | 通用、可缓存 | over/under-fetching、字段多变 | 高性能、流式、严格契约 |
| 缓存 | HTTP 原生(URL+ETag) | 难(单 POST 端点) | 自管 |
| 浏览器直连 | 原生 | 原生 | 需 **gRPC-web** 经代理转换 |
| 典型场景 | 公共 API、CRUD | 富前端/移动聚合多源 | 内部微服务、低延迟、双向流 |

**gRPC-web** 的存在原因：浏览器无法直接发原生 gRPC（拿不到底层 HTTP/2 帧控制），需 Envoy 之类代理在 gRPC 与浏览器可用的编码之间转换。所以 gRPC 多用于服务间内网，对外仍常包一层 REST/GraphQL 网关。

## 版本策略

```
URL path     /api/v1/users          最常用: 直观、缓存友好、易路由   ← 推荐
Header       Accept: application/vnd.api+json; version=1   更"纯", 但调试/缓存麻烦
Query        /api/users?version=1
```

比"放哪"更重要的是**兼容纪律**：加字段是兼容变更，可不升版本；删/改字段语义、改必填、改类型是破坏性变更，必须升大版本并给旧版本明确弃用期（`Deprecation` / `Sunset` 头）。

## 参考

- **REST**: Roy Fielding's dissertation (2000), Chapter 5
- **OpenAPI**: openapis.org · Swagger Editor · 错误格式 RFC 9457
- **GraphQL**: graphql.org/learn · Apollo 文档
- **gRPC**: grpc.io · grpc-web (github.com/grpc/grpc-web)

*Keywords: REST, statelessness, idempotency, ETag, RFC 9457 Problem Details, cursor pagination, HATEOAS, OpenAPI, Swagger, GraphQL, over-fetching, gRPC, gRPC-web, Protobuf, API versioning*
