GraphQL vs REST API设计
约 1541 字大约 5 分钟
graphqlrestapi
2025-08-04
概述
GraphQL 是 Facebook 于 2015 年开源的 API 查询语言和运行时,它提供了一种不同于 REST 的 API 设计范式。REST 基于资源和 HTTP 方法,而 GraphQL 允许客户端精确描述所需数据,通过单一端点返回结果。两种方案各有优劣,理解其差异有助于为不同场景选择合适的方案。
核心对比
1. GraphQL Schema 定义
# Schema 定义类型系统
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
followers: [User!]!
followersCount: Int!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: DateTime!
tags: [String!]
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
createdAt: DateTime!
}
# 输入类型
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
# 枚举
enum SortOrder {
ASC
DESC
}
# 查询入口
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
post(id: ID!): Post
searchPosts(query: String!, sort: SortOrder): [Post!]!
}
# 变更入口
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
followUser(userId: ID!): User!
}
# 订阅入口
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}2. Queries / Mutations / Subscriptions
Query 查询
# 客户端精确指定需要的字段
query GetUserWithPosts {
user(id: "123") {
name
email
posts {
title
createdAt
comments {
text
author {
name
}
}
}
}
}
# 响应完全匹配查询结构
# {
# "data": {
# "user": {
# "name": "Alice",
# "email": "alice@example.com",
# "posts": [
# {
# "title": "GraphQL 入门",
# "createdAt": "2025-01-15",
# "comments": [
# { "text": "好文章!", "author": { "name": "Bob" } }
# ]
# }
# ]
# }
# }
# }Mutation 变更
mutation CreateNewPost {
createPost(input: {
title: "My New Post"
content: "Hello World"
tags: ["graphql", "api"]
}) {
id
title
createdAt
}
}Subscription 订阅
subscription OnNewComment {
commentAdded(postId: "456") {
id
text
author {
name
}
}
}3. Resolvers(解析器)
// Node.js + Apollo Server
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
return context.dataSources.userAPI.getUser(id);
},
users: async (parent, { limit, offset }, context) => {
return context.dataSources.userAPI.getUsers({ limit, offset });
},
searchPosts: async (parent, { query, sort }, context) => {
return context.dataSources.postAPI.search(query, sort);
}
},
Mutation: {
createPost: async (parent, { input }, context) => {
// 认证检查
if (!context.user) throw new AuthenticationError('Not authenticated');
return context.dataSources.postAPI.create({
...input,
authorId: context.user.id
});
}
},
// 字段级解析器(延迟加载关联数据)
User: {
posts: async (user, args, context) => {
return context.dataSources.postAPI.getByAuthor(user.id);
},
followersCount: async (user, args, context) => {
return context.dataSources.userAPI.getFollowersCount(user.id);
}
},
Post: {
author: async (post, args, context) => {
return context.dataSources.userAPI.getUser(post.authorId);
},
comments: async (post, args, context) => {
return context.dataSources.commentAPI.getByPost(post.id);
}
}
};4. REST 等价对比
| 场景 | REST | GraphQL |
|---|---|---|
| 获取用户+帖子+评论 | 3-N 次请求 | 1 次请求 |
| 只需用户名 | 返回整个用户对象(Over-fetching) | 只返回 name 字段 |
| 不同客户端需求 | 多个端点或查询参数 | 同一端点,不同 query |
| 缓存 | HTTP 缓存天然支持 | 需要额外方案 |
5. N+1 问题与 DataLoader
DataLoader 解决 N+1
const DataLoader = require('dataloader');
// 批量加载函数
const userLoader = new DataLoader(async (userIds) => {
// 一次查询获取所有用户
const users = await db.users.findMany({
where: { id: { in: userIds } }
});
// 按输入顺序排列结果
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id) || null);
});
// 解析器中使用 DataLoader
const resolvers = {
Post: {
author: (post, args, context) => {
// 自动批量和缓存
return context.loaders.userLoader.load(post.authorId);
}
}
};
// 每次请求创建新的 loader 实例
const context = ({ req }) => ({
loaders: {
userLoader: new DataLoader(batchGetUsers),
postLoader: new DataLoader(batchGetPosts),
}
});6. 缓存策略
REST 缓存
GET /users/123
Cache-Control: max-age=3600
ETag: "abc123"
# HTTP 缓存天然有效,URL 是天然的缓存键GraphQL 缓存
// Apollo Client 标准化缓存
import { InMemoryCache, ApolloClient } from '@apollo/client';
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'], // 用 id 作为缓存键
},
Post: {
keyFields: ['id'],
fields: {
comments: {
// 分页字段合并策略
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
})
});
// 缓存读取
const { data } = useQuery(GET_USER, {
variables: { id: '123' },
fetchPolicy: 'cache-first' // cache-first | network-only | cache-and-network
});CDN 级别的 GraphQL 缓存
// 使用 Persisted Queries
// 客户端发送查询的哈希而非完整查询文本
// GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc..."}}
// 这使得 GraphQL 请求可以使用 GET 方法,利用 CDN 缓存7. Federation(联邦架构)
# Users Service
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
# Posts Service — 扩展 User 类型
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
author: User!
}8. 何时选择哪种方案
| 选择 GraphQL 的场景 | 选择 REST 的场景 |
|---|---|
| 多客户端,数据需求差异大 | 简单的 CRUD API |
| 深度嵌套的关联数据 | 以文件传输为主 |
| 需要实时订阅 | 公开的第三方 API |
| 快速迭代的前端需求 | 需要充分利用 HTTP 缓存 |
| 减少 Over/Under-fetching | 团队对 REST 更熟悉 |
| 强类型 Schema 驱动开发 | 无状态、简单的微服务间通信 |
总结
GraphQL 通过强类型 Schema、灵活的查询语言和单端点设计,解决了 REST 中常见的 Over-fetching 和 Under-fetching 问题,特别适合数据关系复杂、客户端多样化的场景。REST 则在简单性、HTTP 缓存利用和工具生态成熟度上有优势。DataLoader 解决了 GraphQL 的 N+1 查询问题,Federation 支持微服务架构下的 Schema 拆分。在实际项目中,两者并非互斥,可以根据不同模块的需求混合使用。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于