GraphQL vs REST:取舍 + 各自的最小项目骨架

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 文档。
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。