REST 仍是默认选择,但有些场景 GraphQL 更合适。下面讲两者的取舍 +
最小骨架。
哲学差异
- REST:服务端定义资源 + 端点,前端按需调用多个端点
- GraphQL:服务端定义 schema + resolver,前端按需 query 任意字段
谁该选 GraphQL
- 移动端 / 弱网:一次请求拿所有数据,比串多个 REST 快
- 复杂前端(仪表盘 / 列表 + 详情):避免 over-fetch / under-fetch
- 多客户端(web / iOS / android)字段需求差异大
- 公共 API(GitHub / Shopify):用户按需 query
谁该选 REST
- 简单 CRUD
- 强缓存需求(HTTP cache 直接生效)
- 流式 / 文件上传下载(REST 更自然)
- 团队熟悉度
1. REST 最小骨架 (FastAPI)
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
@app.get('/users/{uid}', response_model=User)
def get_user(uid: int): ...
@app.get('/users/{uid}/posts')
def list_user_posts(uid: int): ...
@app.post('/users', response_model=User, status_code=201)
def create_user(payload: CreateUser): ...
每个资源一组端点:GET / POST / PATCH / DELETE。
返回固定字段集合。
前端要拿"用户 + 他的 5 篇最新帖子":
// REST: 2 个请求 (worst case 串行)
const user = await fetch('/users/42').then(r => r.json())
const posts = await fetch('/users/42/posts?limit=5').then(r => r.json())
2. GraphQL 最小骨架 (Strawberry, Python)
uv add 'strawberry-graphql[fastapi]'
import strawberry
from typing import List
@strawberry.type
class Post:
id: int
title: str
body: str
@strawberry.type
class User:
id: int
name: str
email: str
@strawberry.field
def posts(self, limit: int = 5) -> List[Post]:
return db.get_user_posts(self.id, limit)
@strawberry.type
class Query:
@strawberry.field
def user(self, id: int) -> User:
return db.get_user(id)
schema = strawberry.Schema(query=Query)
from strawberry.fastapi import GraphQLRouter
from fastapi import FastAPI
app = FastAPI()
app.include_router(GraphQLRouter(schema), prefix='/graphql')
启动后浏览器打开 http://localhost:8000/graphql,自带 Playground IDE。
前端 query:
query {
user(id: 42) {
id
name
posts(limit: 5) {
id
title
}
}
}
一个请求拿全部,不传 email / body 等不需要的字段。
3. 避免 N+1 query:DataLoader
GraphQL 最大坑是 resolver 一对多时 N+1:
@strawberry.type
class Post:
author_id: int
@strawberry.field
def author(self) -> User:
return db.get_user(self.author_id) # 每篇帖子都查一次
20 篇帖子 → 1 (list posts) + 20 (各自 author) = 21 次 DB 查询。
DataLoader 模式 batch + cache:
from strawberry.dataloader import DataLoader
async def load_users(keys: List[int]) -> List[User]:
rows = db.get_users_in(keys)
by_id = {u.id: u for u in rows}
return [by_id[k] for k in keys]
user_loader = DataLoader(load_fn=load_users)
@strawberry.type
class Post:
author_id: int
@strawberry.field
async def author(self) -> User:
return await user_loader.load(self.author_id)
20 篇帖子的 author 字段被合并成一次 SELECT WHERE id IN (...)。
所有现代 GraphQL 库都自带或推荐 DataLoader。
4. mutation
@strawberry.input
class CreatePostInput:
title: str
body: str
@strawberry.type
class Mutation:
@strawberry.mutation
def create_post(self, input: CreatePostInput) -> Post:
return db.create_post(input)
schema = strawberry.Schema(query=Query, mutation=Mutation)
前端:
mutation {
createPost(input: { title: "Hi", body: "..." }) {
id
title
}
}
5. subscription(GraphQL over WebSocket)
@strawberry.type
class Subscription:
@strawberry.subscription
async def comment_added(self, post_id: int) -> AsyncGenerator[Comment, None]:
async for c in db.watch_comments(post_id):
yield c
schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription)
前端用 WebSocket:
subscription {
commentAdded(postId: 1) {
id
body
}
}
6. 缓存
REST:HTTP cache + ETag + max-age 直接生效,CDN 友好。
GraphQL:所有 query 都 POST 到 /graphql,HTTP cache 不起作用。
要客户端层缓存(Apollo Client / urql / Relay)。
7. 鉴权 / 授权
REST:每个 endpoint 装饰器 @require_login 简单。
GraphQL:每个 resolver 自己检查 ctx。或者用 directive:
type Query {
adminStats: Stats @auth(role: "admin")
}
实现时拦截 schema 执行。
8. error handling
REST:HTTP status code (4xx/5xx)。
GraphQL:固定 200,error 在响应 errors 数组:
{
"data": { "user": null },
"errors": [{
"message": "User not found",
"extensions": { "code": "NOT_FOUND" }
}]
}
部分字段失败仍返回其它字段的结果(partial success)—— GraphQL 的特点。
9. 性能
- REST 缓存友好;GraphQL 缓存差
- GraphQL N+1 不注意会严重慢;DataLoader 必备
- REST endpoint 容易优化(针对单接口加索引);GraphQL 查询任意组合,
查询计划难预测
10. 实际生产经验
- 内部团队 API:REST 够用 + 简单
- 公共开放 API:GraphQL 客户体验好但运维难
- 移动 app:GraphQL 节省流量
- B2B + 强一致:REST + OpenAPI 文档生成 client SDK 最稳
可以混搭:核心业务 REST,前端聚合层 GraphQL(BFF 模式)。
11. 替代:tRPC
如果前后端都是 TypeScript,tRPC 让你完全跳过 schema:
// 后端
export const appRouter = router({
user: router({
get: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => db.users.findUnique({ where: { id: input.id } })),
}),
})
// 前端(类型完全自动推导)
const user = await trpc.user.get.query({ id: '1' })
不是 REST / GraphQL 任一阵营,但端到端类型安全极其爽。
踩过的坑
- GraphQL schema 暴露太多内部字段 → 给客户端"复杂查询"的能力变成
DOS 武器。query depth limit + complexity limit 是必须的。 - 把 GraphQL 用成"REST + JSON in URL":每个 query 只取一个对象一层字段,
完全没用 GraphQL 的优势。这种场景就用 REST。 - DataLoader 跨 request 共享:缓存了别的 request 的数据 → 数据错乱。
每个 request 新建 loader 实例。 - GraphQL 客户端缓存(Apollo)配错 normalize key → mutation 后 UI 没刷新。
仔细读 normalize 文档。
登录后参与评论。