真正的权衡(不是营销版本)
REST 与 GraphQL 的争论常常沦为部落式争论。现实是,两者都是有效的方法,各有真正的权衡,而“正确”的选择取决于你的客户端-服务器关系、团队结构和数据复杂性。
GraphQL 解决了 REST 的特定问题:过度获取、获取不足以及前端团队因后端变更而受阻。REST 解决了 GraphQL 的问题:缓存、简单性和可预测的性能。两者都不是普遍优越的。

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 服务器(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 }
订阅(实时)
// 基于 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 格式之间转换和转换数据。