"我们用 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