API 设计完全指南:REST、GraphQL 与 gRPC 的选型实战

"我们用 REST 还是 GraphQL 还是 gRPC?"—— 这是 API 设计阶段最常见的问题。三者并非互斥,各有适用场景。这篇文章把三种 API 风格的设计哲学、典型用法、性能特点、适配场景一次讲透,帮你做出有依据的选择。

REST:互联网的事实标准

REST(Representational State Transfer)是 Roy Fielding 2000 年博士论文提出的架构风格。核心思想:把所有东西看作"资源",用 HTTP 动词操作资源

# 资源 + 动词
GET    /users              # 列表
GET    /users/123          # 单个
POST   /users              # 创建
PUT    /users/123          # 完整更新
PATCH  /users/123          # 部分更新
DELETE /users/123          # 删除

# 嵌套资源
GET /users/123/orders      # 用户的订单列表
GET /users/123/orders/456  # 某个订单

# 状态码语义
200 OK
201 Created
204 No Content
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Entity
500 Internal Server Error

REST 的设计原则

  • 无状态:每个请求自包含,服务端不存会话(状态在 token / 请求里)。
  • 统一接口:用 HTTP 标准方法,不自创动词。
  • 资源导向:URL 是名词,不是动词。/createUser 是反 REST 的,应该 POST /users
  • 可缓存:GET 请求应该可缓存(配 ETag / Cache-Control)。
  • HATEOAS:响应中包含相关资源的链接。实际项目里这条很少严格遵守。

REST 的优势

  • 简单:HTTP 大家都懂,curl 就能测试。
  • 缓存友好:CDN / 浏览器原生支持 GET 缓存。
  • 生态成熟:Swagger / OpenAPI 文档、SDK 生成、API Gateway 全部围绕 REST 设计。
  • 跨语言无障碍:JSON / XML 谁都能解析。

REST 的痛点

  • 过度获取:返回固定字段,前端只要 name 但接口给了 50 个字段。
  • 多次请求:前端要展示一个页面 = 调 5 个不同接口,网络往返多。
  • 版本管理:加字段不破坏旧客户端,删字段就 breaking change,版本演化烦。

GraphQL:解决 REST 的痛点

Facebook 2015 年开源。核心:客户端声明"我要什么",服务端只返回那些

# 一次请求拿到多种关联数据
query {
  user(id: 123) {
    name
    avatar
    orders(limit: 5) {
      id
      total
      items {
        product { name price }
      }
    }
  }
}

# 服务端只返回声明的字段
{
  "data": {
    "user": {
      "name": "mores",
      "avatar": "...",
      "orders": [
        { "id": 1, "total": 100, "items": [...] }
      ]
    }
  }
}

GraphQL 的核心要素

# Schema 定义类型
type User {
  id: ID!
  name: String!
  avatar: String
  orders(limit: Int): [Order!]!
}

type Order {
  id: ID!
  total: Float!
  items: [OrderItem!]!
}

type Query {
  user(id: ID!): User
  orders(filter: OrderFilter): [Order!]!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
}

type Subscription {
  orderUpdated(userId: ID!): Order!
}

GraphQL 的优势

  • 按需获取:消灭过度获取和多次请求。
  • 强类型:Schema 即文档 + 代码生成。
  • 聚合多源:一次查询里可以聚合多个微服务的数据。
  • 实时订阅:Subscription 天然支持 WebSocket 推送。

GraphQL 的痛点

  • 缓存难:每次查询都不同(字段组合无穷),HTTP 缓存 / CDN 用不上 —— 必须客户端做缓存。
  • N+1 查询:resolver 容易触发 N 次数据库查询。DataLoader 等 batching 工具是必备。
  • 性能监控难:一个 endpoint 对应无数查询,普通"endpoint 延迟"指标失效。
  • 恶意查询:客户端能写出嵌套极深的查询(friends.friends.friends...)把服务器搞挂。需要查询复杂度限制。
  • 过度灵活带来的设计负担:Schema 设计如果不严谨,客户端可以查出意料外的组合。

gRPC:高性能的 RPC 框架

Google 2015 年开源。核心:用 Protocol Buffers 定义服务接口,用 HTTP/2 传输,生成各语言强类型代码

// service.proto
syntax = "proto3";

service OrderService {
    rpc GetOrder(GetOrderRequest) returns (Order);
    rpc ListOrders(ListOrdersRequest) returns (stream Order);    // 服务端流
    rpc UploadOrders(stream Order) returns (UploadResult);         // 客户端流
    rpc Chat(stream Message) returns (stream Message);             // 双向流
}

message GetOrderRequest {
    int64 order_id = 1;
}

message Order {
    int64 id = 1;
    string user_id = 2;
    double total = 3;
    repeated OrderItem items = 4;
}

用 protoc 生成各语言代码:

# 生成 Go 代码
protoc --go_out=. --go-grpc_out=. service.proto

# 生成 Python 代码
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. service.proto

客户端调用看起来就像本地方法:

// Go 客户端
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := pb.NewOrderServiceClient(conn)
order, _ := client.GetOrder(ctx, &pb.GetOrderRequest{OrderId: 123})

gRPC 的优势

  • 性能极高:Protobuf 比 JSON 小 3-10 倍,HTTP/2 多路复用、二进制帧。
  • 强类型 + 代码生成:从 .proto 文件生成各语言 stub,改字段全栈编译错误。
  • 流式 RPC:服务端流、客户端流、双向流,适合大数据传输和实时通信。
  • 跨语言:Go、Java、Python、C++、Ruby、Node、Rust 都支持。

gRPC 的痛点

  • 浏览器不能直接用:HTTP/2 + 二进制帧,浏览器需要 gRPC-Web 代理。
  • 调试难:二进制流,curl 不能直接看。需要 grpcurl 等专门工具。
  • 对人不友好:Proto 文件需要学习,JSON 一眼能读懂的优势没了。
  • 生态偏后端:前端 / 移动端 / 公共 API 通常还是 REST / GraphQL。

三者对比表

维度          REST            GraphQL          gRPC
传输          HTTP/1.1+JSON   HTTP+JSON        HTTP/2+Protobuf
延迟          中              中(查询大时高)   极低
吞吐          中              中               极高
浏览器友好    极好            好               差(需 gRPC-Web)
缓存          原生 HTTP 缓存  难,要客户端做    无
强类型        弱(靠 Swagger) 强(Schema)      强(Proto)
版本演化      手动            字段级演化       字段编号兼容
学习曲线      低              中               中-高
适合场景      公共 API,简单业务  BFF,聚合层       微服务间通信
代表          GitHub API、Stripe   GitHub API v4、Shopify   K8s、Etcd、Envoy

什么时候用什么

用 REST 当默认

对外公共 API、简单 CRUD、需要浏览器直接访问、需要强缓存 —— REST 永远是稳妥选择。90% 的项目用 REST 够了

用 GraphQL 当聚合 / BFF

前端要的数据跨多个后端服务、UI 频繁变动需要按需查询、移动端流量贵需要精确控制返回大小 —— GraphQL 在这些场景大放异彩。Apollo Federation 让多个微服务的 GraphQL 子图合成一个统一图。

用 gRPC 当微服务内部

后端服务之间互调,追求性能、强类型、流式 —— gRPC 是首选。K8s / Istio / Envoy / TiKV / etcd 内部全是 gRPC。

混合使用

真实大公司通常三者混用:

  • 对外公共 API:REST(+ Webhook)。
  • 前端 BFF 层:GraphQL。
  • 微服务内部通信:gRPC。

API 设计的通用原则

无论用哪种风格,好的 API 都有这些性质:

1. 命名一致

# 不一致
GET /getUsers
GET /list-orders
GET /productInfo

# 一致(REST)
GET /users
GET /orders
GET /products

2. 版本管理

# URL 版本(直观)
GET /v1/users
GET /v2/users

# Header 版本(更 REST)
GET /users
Accept: application/vnd.myapp.v2+json

# GraphQL:用 @deprecated 渐进演化
type User {
  email: String @deprecated(reason: "Use emailAddress")
  emailAddress: String
}

3. 错误格式标准化

# RFC 7807 problem+json
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/invalid-email",
  "title": "Invalid email",
  "status": 400,
  "detail": "邮箱格式不正确",
  "instance": "/users/register",
  "errors": {
    "email": "格式不对"
  }
}

4. 分页

# 偏移分页(简单但深分页慢)
GET /users?page=10&per_page=20

# 游标分页(性能好,推荐)
GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
{
  "items": [...],
  "next_cursor": "eyJpZCI6MTQzfQ"
}

5. 限流头

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1700000000

6. 幂等性 header

POST /orders
Idempotency-Key: abc-123-uuid

常见坑

坑 1:在 URL 里用动词。 POST /getUserOrders 是反 REST,应该 GET /users/{id}/orders

坑 2:把异常当业务结果。 接口总返回 200,业务错误塞在 body 里 —— 客户端无法用 HTTP 状态码做通用错误处理。

坑 3:破坏向后兼容。 删字段、改类型、改语义都是 breaking change。规范:加字段允许,删字段 / 改语义要走 deprecation 周期。

坑 4:GraphQL N+1。 查 100 个用户,每个用户的订单分别 fetch,触发 101 次查询。用 DataLoader 批量加载。

坑 5:gRPC 在公网用。 客户端要严格的 HTTP/2 支持,某些防火墙 / 网关会有问题。公网用 gRPC-Gateway 转 REST,或者直接用 REST 对外。

OpenAPI / Protobuf:文档即代码

三种风格都有对应的"契约即代码"工具:

  • REST:OpenAPI(Swagger)。从 yaml 生成文档、SDK、Mock 服务。
  • GraphQL:Schema 本身就是文档。Apollo Studio、GraphiQL 提供交互式探索。
  • gRPC:Proto 文件就是契约。Buf 工具做 lint、breaking change 检测。

"API 先设计后实现"是 API First 思想 —— 团队先就 schema 达成共识,前后端并行开发。生产级 API 项目都这么做。

HATEOAS 实战

真正 RESTful 的响应应该附带后续可做操作的链接,让客户端"不需要硬编码 URL":

{
  "id": 1001,
  "status": "created",
  "total": 100,
  "_links": {
    "self":   { "href": "/orders/1001" },
    "pay":    { "href": "/orders/1001/payments", "method": "POST" },
    "cancel": { "href": "/orders/1001", "method": "DELETE" }
  }
}

状态变化后 _links 里的可用操作也变了 —— 已支付订单不再有 "pay" 链接,而是 "refund"。客户端按 _links 渲染按钮,后端单方面改业务流程也不会影响前端。理论上很美,实践中大多数 API 没严格做 —— 因为成本高且前端通常硬编码更方便。但它的思想(状态自描述)在 API 设计里仍有价值。

API 网关的角色

大型系统的 API 通常不直接对外,经过API Gateway(Kong、Apisix、Spring Cloud Gateway、AWS API Gateway):

  • 路由:不同路径 / 版本路由到不同后端。
  • 统一鉴权:JWT 校验、API Key、OAuth。
  • 限流熔断:全局保护。
  • 协议转换:对外 REST,对内 gRPC。
  • 监控记录:统一日志、metrics、链路追踪。
  • BFF 聚合:多服务数据聚合后返回前端。

网关是后端架构的"门面",所有横切关注点都在这里处理,业务服务保持纯粹。

API 版本演化的实战

真实世界的 API 版本管理是渐进而非革命:

# 加字段:兼容,直接加
{ "name": "...", "email": "..." }  ->
{ "name": "...", "email": "...", "phone": "..." }

# 改字段含义:必须新版本
v1: { "status": "1" }   # 数字字符串
v2: { "status": "active" }   # 枚举

# 删字段:走 deprecation 周期
1. 先标记 deprecated,文档里说明,但仍返回
2. 监控调用方,推动迁移
3. 半年到一年后真正删除

写在最后

API 风格选择不是技术问题,是场景匹配问题。三种风格各有强项,没有"最好"的,只有"最合适"的。理解每种风格的设计哲学比记住它们的语法重要 —— 这样面对新需求你能快速判断该用哪个。

给一个工程心得:API 是系统对外的契约,设计时要考虑"用户"(可能是其他团队、合作伙伴、外部开发者)而不是只考虑实现方便。一个清晰、一致、有文档的 API 就算实现复杂一点也值得;一个混乱的 API 即使实现简单也是技术债。把 API 设计作为产品来对待,你的系统集成成本会大大降低。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

幂等性与分布式锁完全指南:幂等 Token、Redis 锁与 Snowflake ID

2026-5-15 16:19:19

技术教程

Kafka 完全指南:从 Partition 到 ISR 的内部机制

2026-5-15 16:19:20

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索