GraphQL vs REST:我们为什么把 API 从 REST 迁移到 GraphQL
我们的 SaaS 产品原来用 REST API,前端经常抱怨数据不够灵活、请求太多。
迁移到 GraphQL 后,前端开发效率提升 40%,API 请求减少 60%。
问题背景
REST API 的痛点
痛点 1:过度获取(Over-fetching)
JAVASCRIPT
// 前端只需要用户名和头像
// 但 REST API 返回了所有字段
GET /api/users/123
// 响应
{
"id": 123,
"username": "john",
"email": "john@example.com",
"avatar": "https://...",
"bio": "...",
"location": "...",
"website": "...",
"created_at": "...",
"updated_at": "...",
// ... 还有 20 个字段
}问题:
- 传输了不需要的数据
- 浪费带宽
- 增加解析时间
痛点 2:获取不足(Under-fetching)
JAVASCRIPT
// 显示用户主页需要:
// 1. 用户信息
GET /api/users/123
// 2. 用户的文章
GET /api/users/123/posts
// 3. 每篇文章的评论
GET /api/posts/1/comments
GET /api/posts/2/comments
GET /api/posts/3/comments
// 4. 用户的关注者
GET /api/users/123/followers问题:
- 需要多个请求
- 瀑布式加载
- 前端逻辑复杂
痛点 3:版本管理困难
JAVASCRIPT
// v1 返回简单数据
GET /api/v1/users/123
{ "name": "John", "age": 30 }
// v2 返回更多数据
GET /api/v2/users/123
{ "name": "John", "age": 30, "email": "...", "avatar": "..." }
// 需要维护多个版本GraphQL 解决方案
解决方案 1:精确获取需要的数据
GRAPHQL
# 只需要用户名和头像
query {
user(id: 123) {
username
avatar
}
}
# 响应
{
"data": {
"user": {
"username": "john",
"avatar": "https://..."
}
}
}优势:
- 只传输需要的数据
- 减少带宽
- 加快解析
解决方案 2:一次请求获取所有数据
GRAPHQL
# 一次请求获取用户主页所有数据
query {
user(id: 123) {
username
avatar
posts(limit: 10) {
title
content
comments(limit: 5) {
author {
username
}
content
}
}
followers(limit: 10) {
username
avatar
}
}
}优势:
- 一次请求
- 并行加载
- 前端逻辑简单
解决方案 3:无需版本管理
GRAPHQL
# 旧客户端继续使用旧字段
query {
user(id: 123) {
name # 旧字段
age
}
}
# 新客户端使用新字段
query {
user(id: 123) {
username # 新字段
email
avatar
}
}优势:
- 向后兼容
- 渐进式迁移
- 无需维护多版本
迁移过程
阶段 1:Schema 设计
GRAPHQL
# schema.graphql
type User {
id: ID!
username: String!
email: String!
avatar: String
bio: String
posts(limit: Int = 10, offset: Int = 0): [Post!]!
followers(limit: Int = 10): [User!]!
following(limit: Int = 10): [User!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments(limit: Int = 10): [Comment!]!
tags: [String!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
post(id: ID!): Post
posts(limit: Int = 10, offset: Int = 0): [Post!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
addComment(postId: ID!, content: String!): Comment!
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}阶段 2:Resolver 实现
JAVASCRIPT
// resolvers.js
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
},
users: async (_, { limit, offset }, { dataSources }) => {
return dataSources.userAPI.getUsers({ limit, offset });
},
post: async (_, { id }, { dataSources }) => {
return dataSources.postAPI.getPostById(id);
},
posts: async (_, { limit, offset }, { dataSources }) => {
return dataSources.postAPI.getPosts({ limit, offset });
}
},
User: {
posts: async (user, { limit, offset }, { dataSources }) => {
return dataSources.postAPI.getPostsByAuthor(user.id, { limit, offset });
},
followers: async (user, { limit }, { dataSources }) => {
return dataSources.userAPI.getFollowers(user.id, { limit });
},
following: async (user, { limit }, { dataSources }) => {
return dataSources.userAPI.getFollowing(user.id, { limit });
}
},
Post: {
author: async (post, _, { dataSources }) => {
return dataSources.userAPI.getUserById(post.authorId);
},
comments: async (post, { limit }, { dataSources }) => {
return dataSources.commentAPI.getCommentsByPost(post.id, { limit });
}
},
Comment: {
author: async (comment, _, { dataSources }) => {
return dataSources.userAPI.getUserById(comment.authorId);
},
post: async (comment, _, { dataSources }) => {
return dataSources.postAPI.getPostById(comment.postId);
}
},
Mutation: {
createPost: async (_, { input }, { dataSources, user }) => {
if (!user) throw new AuthenticationError('Must be logged in');
return dataSources.postAPI.createPost({ ...input, authorId: user.id });
},
updatePost: async (_, { id, input }, { dataSources, user }) => {
if (!user) throw new AuthenticationError('Must be logged in');
const post = await dataSources.postAPI.getPostById(id);
if (post.authorId !== user.id) {
throw new ForbiddenError('Not authorized');
}
return dataSources.postAPI.updatePost(id, input);
},
deletePost: async (_, { id }, { dataSources, user }) => {
if (!user) throw new AuthenticationError('Must be logged in');
const post = await dataSources.postAPI.getPostById(id);
if (post.authorId !== user.id) {
throw new ForbiddenError('Not authorized');
}
return dataSources.postAPI.deletePost(id);
},
addComment: async (_, { postId, content }, { dataSources, user }) => {
if (!user) throw new AuthenticationError('Must be logged in');
return dataSources.commentAPI.addComment({
postId,
authorId: user.id,
content
});
}
}
};阶段 3:DataLoader 优化
JAVASCRIPT
// dataloaders.js
const DataLoader = require('dataloader');
function createLoaders(dataSources) {
return {
userLoader: new DataLoader(async (userIds) => {
const users = await dataSources.userAPI.getUsersByIds(userIds);
// 确保顺序一致
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id));
}),
postLoader: new DataLoader(async (postIds) => {
const posts = await dataSources.postAPI.getPostsByIds(postIds);
const postMap = new Map(posts.map(p => [p.id, p]));
return postIds.map(id => postMap.get(id));
}),
commentsByPostLoader: new DataLoader(async (postIds) => {
const comments = await dataSources.commentAPI.getCommentsByPostIds(postIds);
// 按 postId 分组
const commentsMap = new Map();
postIds.forEach(id => commentsMap.set(id, []));
comments.forEach(c => {
commentsMap.get(c.postId).push(c);
});
return postIds.map(id => commentsMap.get(id));
})
};
}
// 在 context 中使用
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const user = await getUserFromToken(req.headers.authorization);
const loaders = createLoaders(dataSources);
return { user, loaders, dataSources };
}
});阶段 4:渐进式迁移
JAVASCRIPT
// 同时保留 REST 和 GraphQL
// REST API 调用 GraphQL
app.get('/api/users/:id', async (req, res) => {
const result = await graphql(schema, `
query GetUser($id: ID!) {
user(id: $id) {
id
username
email
avatar
}
}
`, null, context, { id: req.params.id });
res.json(result.data.user);
});性能对比
| 指标 | REST | GraphQL | 改进 |
|------|------|---------|------|
| 平均请求数 | 5.2 次/页 | 1 次/页 | 81% |
| 平均响应大小 | 45KB | 12KB | 73% |
| 首屏加载时间 | 2.8s | 1.2s | 57% |
| 前端开发时间 | 5 天/功能 | 3 天/功能 | 40% |
最佳实践
1. 查询复杂度限制
JAVASCRIPT
// 防止恶意查询
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5) // 最大嵌套深度 5
]
});2. 查询成本分析
JAVASCRIPT
// 限制查询复杂度
const costLimit = require('graphql-cost-analysis');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
costLimit({
maxCost: 1000,
variables: {
limit: 10
}
})
]
});3. 缓存策略
JAVASCRIPT
// 使用 Apollo Client 缓存
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
fields: {
posts: {
merge(existing, incoming) {
return incoming;
}
}
}
}
}
})
});总结
GraphQL vs REST 选择指南:
选择 GraphQL 当:
- 前端需要灵活的数据获取
- 有多个客户端(Web、Mobile、API)
- 数据关系复杂
- 需要减少请求次数
选择 REST 当:
- API 简单,数据结构固定
- 需要简单的缓存策略
- 团队不熟悉 GraphQL
- 需要文件上传等简单场景
我们的选择是正确的,GraphQL 让前端开发更高效。
迁移时间:2026年6月
项目规模:SaaS 产品,50+ API
效率提升:40%
#GraphQL #REST #API设计 #前端开发
读者评论 2