正在加载,请稍候…

GraphQL vs REST:何时使用以及如何构建好两者

全面的 GraphQL 与 REST 对比:查询语言基础、模式、解析器、N+1 问题、订阅、身份验证,以及每种方法适合你的架构的时机。

GraphQL vs REST:何时使用以及如何构建好两者

真正的权衡(不是营销版本)

REST 与 GraphQL 的争论常常沦为部落式争论。现实是,两者都是有效的方法,各有真正的权衡,而“正确”的选择取决于你的客户端-服务器关系、团队结构和数据复杂性。

GraphQL 解决了 REST 的特定问题:过度获取、获取不足以及前端团队因后端变更而受阻。REST 解决了 GraphQL 的问题:缓存、简单性和可预测的性能。两者都不是普遍优越的。

GraphQL vs REST:何时使用以及如何构建好两者 插图

2026 年的 REST

REST(表述性状态转移)围绕资源和 HTTP 语义构建 API:

GET    /users          → 列出用户
GET    /users/123      → 获取用户 123
POST   /users          → 创建用户
PUT    /users/123      → 替换用户 123
PATCH  /users/123      → 部分更新用户 123
DELETE /users/123      → 删除用户 123

REST 的优势:

  • HTTP 缓存开箱即用(CDN、浏览器缓存)
  • 简单的思维模型——资源 + 动词
  • 易于监控(每个端点 = 一种操作类型)
  • 设计上无状态
  • 无需特殊工具即可与任何 HTTP 客户端配合使用

REST 的痛点:

  • 过度获取:GET /users 返回 20 个字段,你只需要 3 个
  • 获取不足:需要用户 + 帖子 + 评论 = 3 个请求
  • API 版本控制(v1, v2...)导致泛滥
  • 前端团队等待后端提供新的字段组合

GraphQL 基础

GraphQL 是一种 API 查询语言——客户端精确指定他们需要的数据:

# 客户端精确发送所需内容:
query GetUserProfile($userId: ID!) {
  user(id: $userId) {
    id
    name
    email
    # 请求帖子时只包含需要的字段
    posts(limit: 5) {
      title
      publishedAt
      commentCount  # 计算字段——无需额外请求
    }
  }
}

# 服务器精确返回该形状:
{
  "data": {
    "user": {
      "id": "123",
      "name": "Alice",
      "email": "alice@example.com",
      "posts": [
        { "title": "My First Post", "publishedAt": "2026-01-15", "commentCount": 7 }
      ]
    }
  }
}

一个 HTTP 端点(POST /graphql)处理所有查询。没有过度/获取不足。

模式优先设计

GraphQL 从定义 API 契约的模式开始:

# schema.graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts(limit: Int = 10, offset: Int = 0): [Post!]!
  followers: [User!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments(limit: Int = 20): [Comment!]!
  tags: [String!]!
  publishedAt: DateTime
  commentCount: Int!
}

type Comment {
  id: ID!
  content: String!
  author: User!
  createdAt: DateTime!
}

# 根类型——所有操作的入口点
type Query {
  user(id: ID!): User
  users(limit: Int = 20, offset: Int = 0): [User!]!
  post(id: ID!): Post
  searchPosts(query: String!): [Post!]!
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
  addComment(postId: ID!, content: String!): Comment!
}

type Subscription {
  commentAdded(postId: ID!): Comment!
}

input CreatePostInput {
  title: String!
  content: String!
  tags: [String!]
}

input UpdatePostInput {
  title: String
  content: String
  tags: [String!]
}

scalar DateTime

GraphQL vs REST:何时使用以及如何构建好两者 插图

构建 GraphQL 服务器(Node.js)

// 使用 Apollo Server 4 与 Express
import { ApolloServer } from '@apollo/server'
import { expressMiddleware } from '@apollo/server/express4'
import express from 'express'
import { readFileSync } from 'fs'

const typeDefs = readFileSync('./schema.graphql', 'utf8')

const resolvers = {
  Query: {
    user: async (_: unknown, { id }: { id: string }, ctx: Context) => {
      return ctx.db.users.findById(id)
    },
    
    users: async (_: unknown, { limit, offset }: { limit: number, offset: number }, ctx: Context) => {
      return ctx.db.users.findAll({ limit, offset })
    },

    searchPosts: async (_: unknown, { query }: { query: string }, ctx: Context) => {
      return ctx.db.posts.search(query)
    },
  },

  Mutation: {
    createPost: async (_: unknown, { input }: { input: CreatePostInput }, ctx: Context) => {
      if (!ctx.user) throw new AuthenticationError('Not authenticated')
      
      return ctx.db.posts.create({
        ...input,
        authorId: ctx.user.id,
        publishedAt: new Date(),
      })
    },
  },

  // 字段解析器——当请求该字段时调用
  User: {
    posts: async (user: User, { limit, offset }: PaginationArgs, ctx: Context) => {
      return ctx.db.posts.findByAuthor(user.id, { limit, offset })
    },
    
    followers: async (user: User, _: unknown, ctx: Context) => {
      return ctx.db.follows.findFollowers(user.id)
    },
  },

  Post: {
    author: async (post: Post, _: unknown, ctx: Context) => {
      return ctx.db.users.findById(post.authorId)
    },
    
    commentCount: async (post: Post, _: unknown, ctx: Context) => {
      return ctx.db.comments.countByPost(post.id)
    },
  },
}

const server = new ApolloServer({ typeDefs, resolvers })
await server.start()

const app = express()
app.use('/graphql', expressMiddleware(server, {
  context: async ({ req }) => ({
    user: await getUser(req.headers.authorization),
    db: createDbConnection(),
  }),
}))

N+1 问题与 DataLoader

最常见的 GraphQL 性能问题:解析列表时,每个项目触发一个单独的数据库查询:

查询:posts { author { name } }

执行(朴素方式):
1. SELECT * FROM posts LIMIT 10          → 10 篇帖子
2. SELECT * FROM users WHERE id = 1      → 帖子 1 的作者
3. SELECT * FROM users WHERE id = 2      → 帖子 2 的作者
... 还有 10 个查询!

总共:一个“简单”请求需要 11 个查询

DataLoader 批量处理这些查询:

import DataLoader from 'dataloader'

// 创建一个批量加载用户查找的加载器
const userLoader = new DataLoader<string, User>(async (userIds) => {
  // 一次查询所有 ID
  const users = await db.users.findByIds([...userIds])
  
  // DataLoader 要求结果与键的顺序相同
  const userMap = new Map(users.map(u => [u.id, u]))
  return userIds.map(id => userMap.get(id) ?? new Error(`User ${id} not found`))
})

// 在解析器中使用——DataLoader 自动批量处理这些
const resolvers = {
  Post: {
    author: (post: Post, _: unknown, ctx: Context) => {
      return ctx.loaders.user.load(post.authorId) // 已批量处理!
    },
  },
}

// 每个请求创建加载器(重要:不同用户不应共享缓存)
context: async ({ req }) => ({
  loaders: {
    user: new DataLoader(batchLoadUsers),
    comment: new DataLoader(batchLoadComments),
  },
})

// 现在:10 篇帖子 → 1 次 SELECT * FROM users WHERE id IN (1,2,3,...10)

身份验证与授权

// 模式:基于权限的字段授权

// 使用 GraphQL Shield
import { shield, rule, and, or } from 'graphql-shield'

const isAuthenticated = rule({ cache: 'contextual' })(
  async (_, __, ctx) => !!ctx.user
)

const isPostAuthor = rule({ cache: 'strict' })(
  async (post, _, ctx) => post.authorId === ctx.user?.id
)

const isAdmin = rule({ cache: 'contextual' })(
  async (_, __, ctx) => ctx.user?.role === 'admin'
)

const permissions = shield({
  Query: {
    users: isAdmin,
    user: isAuthenticated,
  },
  Mutation: {
    createPost: isAuthenticated,
    deletePost: or(isPostAuthor, isAdmin),
    updatePost: isPostAuthor,
  },
})

// 替代方案:基于指令的身份验证
# 用于授权的模式指令
type Query {
  adminStats: Stats @auth(requires: ADMIN)
  myProfile: User @auth(requires: USER)
  publicPosts: [Post!]!
}

directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role { ADMIN USER }

GraphQL vs REST:何时使用以及如何构建好两者 插图

订阅(实时)

// 基于 WebSocket 的订阅,使用 graphql-ws
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import { PubSub } from 'graphql-subscriptions'

const pubsub = new PubSub()

const resolvers = {
  Mutation: {
    addComment: async (_, { postId, content }, ctx) => {
      const comment = await ctx.db.comments.create({ postId, content, authorId: ctx.user.id })
      
      // 为订阅者发布事件
      await pubsub.publish('COMMENT_ADDED', { commentAdded: comment, postId })
      
      return comment
    },
  },

  Subscription: {
    commentAdded: {
      subscribe: (_, { postId }) => pubsub.asyncIterator(['COMMENT_ADDED']),
      resolve: (payload) => payload.commentAdded,
      // 过滤:仅发送给关注此帖子的订阅者
      filter: (payload, variables) => payload.postId === variables.postId,
    },
  },
}

// 客户端订阅:
const COMMENT_SUBSCRIPTION = gql`
  subscription OnCommentAdded($postId: ID!) {
    commentAdded(postId: $postId) {
      id
      content
      author { name }
      createdAt
    }
  }
`

使用 GraphQL 进行缓存

GraphQL 不像 REST 那样受益于 HTTP 缓存(所有请求都是 POST 到同一个 URL)。解决方案:

// 1. 持久化查询——将操作转换为 GET
// 客户端发送哈希 → 服务器查找完整查询
// GET /graphql?operationName=GetUser&variables={...}&extensions={"persistedQuery":{...}}

// Apollo 的自动持久化查询:
const link = createPersistedQueryLink({ useGETForHashedQueries: true })

// 2. 使用缓存提示进行响应缓存
const resolvers = {
  Query: {
    post: async (_, { id }, ctx) => {
      const post = await ctx.db.posts.findById(id)
      
      // 提示缓存 60 秒
      ctx.setCacheHint({ maxAge: 60, scope: 'PUBLIC' })
      
      return post
    },
  },
}

// 3. 使用 Apollo Client 进行客户端缓存
const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Post: {
        keyFields: ['id'],
        fields: {
          comments: {
            keyArgs: ['postId'],
            merge(existing = [], incoming) {
              return [...existing, ...incoming] // 追加分页
            },
          },
        },
      },
    },
  }),
})

何时选择 GraphQL vs REST

选择 GraphQL 当:

  • 多种客户端类型(Web、移动、第三方)具有不同的数据需求
  • 复杂、相互关联的数据(社交图谱、内容管理)
  • 前端团队速度因 API 变更而受阻
  • 你需要实时订阅
  • 移动客户端,带宽很重要

选择 REST 当:

  • 简单的 CRUD 操作
  • 面向第三方开发者的公共 API(REST 更熟悉)
  • CDN 缓存很重要
  • 团队规模小且 REST 已被充分理解
  • 文件上传是主要用例(GraphQL 处理起来很笨拙)

同时使用两者: 将 GraphQL 用于内部前端,将 REST 用于公共 API 和 webhook。

这个选择不是永久的——许多公司从 REST 开始,随着客户端需求增长而添加 GraphQL。避免重写工作的 API;相反,将 GraphQL 作为一层添加到现有 REST 端点旁边。

→ 使用 JSON to XML 转换器在 JSON 和 XML 格式之间转换和转换数据。