GraphQL vs REST:取捨與選擇
GraphQL 和 REST 是兩種主流的 API 設計方式。本篇將深入比較,幫助你做出正確的選擇。
一、 基本概念
1.1 REST
REST(Representational State Transfer)是以資源為中心的架構風格:
bash
GET /users/1
GET /users/1/posts
GET /posts/1/comments1.2 GraphQL
GraphQL 是一種查詢語言,讓前端決定要什麼資料:
graphql
query {
user(id: 1) {
name
posts {
title
comments {
content
}
}
}
}二、 核心差異
2.1 請求方式
2.2 取得資料
| 問題 | REST | GraphQL |
|---|---|---|
| Over-fetching | 返回過多資料 | 只拿需要的 |
| Under-fetching | 需要多次請求 | 一次取得 |
| N+1 問題 | 容易發生 | 需要特別處理 |
2.3 範例對比
javascript
// REST:需要 3 個請求
const user = await fetch("/users/1").then((r) => r.json());
const posts = await fetch(`/users/1/posts`).then((r) => r.json());
const comments = await Promise.all(
posts.map((p) => fetch(`/posts/${p.id}/comments`).then((r) => r.json()))
);
// GraphQL:1 個請求
const { data } = await fetch("/graphql", {
method: "POST",
body: JSON.stringify({
query: `
query {
user(id: 1) {
name
posts {
title
comments { content }
}
}
}
`,
}),
}).then((r) => r.json());三、 GraphQL 深入
3.1 Schema 定義
graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
name: String!
email: String!
}3.2 Express + Apollo 實作
javascript
const { ApolloServer } = require("@apollo/server");
const { expressMiddleware } = require("@apollo/server/express4");
const typeDefs = `
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
const resolvers = {
Query: {
user: (_, { id }) => User.findById(id),
users: () => User.find(),
},
User: {
posts: (user) => Post.find({ authorId: user.id }),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
app.use("/graphql", expressMiddleware(server));3.3 N+1 問題解決
javascript
const DataLoader = require("dataloader");
// 批次載入
const postLoader = new DataLoader(async (userIds) => {
const posts = await Post.find({ authorId: { $in: userIds } });
// 按用戶分組
const postsByUser = posts.reduce((acc, post) => {
acc[post.authorId] = acc[post.authorId] || [];
acc[post.authorId].push(post);
return acc;
}, {});
return userIds.map((id) => postsByUser[id] || []);
});
const resolvers = {
User: {
posts: (user) => postLoader.load(user.id),
},
};四、 優缺點比較
4.1 REST 優點
| 優點 | 說明 |
|---|---|
| 簡單易懂 | HTTP 方法直觀 |
| 快取友好 | 可用 HTTP 快取 |
| 工具成熟 | cURL、Postman 等 |
| 無需學習 | 標準 HTTP |
| 易於除錯 | 每個端點獨立 |
4.2 REST 缺點
| 缺點 | 說明 |
|---|---|
| Over-fetching | 返回不需要的資料 |
| Under-fetching | 需要多次請求 |
| 版本管理 | 需要明確版本策略 |
| 文檔維護 | 需要額外工具 |
4.3 GraphQL 優點
| 優點 | 說明 |
|---|---|
| 精確取得 | 只拿需要的資料 |
| 單一端點 | 簡化請求 |
| 強型別 | Schema 即文檔 |
| 自省能力 | 自動生成文檔 |
| 版本無感 | 新增欄位不破壞 |
4.4 GraphQL 缺點
| 缺點 | 說明 |
|---|---|
| 學習曲線 | 新概念較多 |
| 快取困難 | POST 請求難快取 |
| N+1 問題 | 需要 DataLoader |
| 複雜查詢 | 可能造成性能問題 |
| 檔案上傳 | 較為複雜 |
五、 何時選擇哪個?
5.1 選擇 REST
markdown
✅ 簡單的 CRUD 應用
✅ 需要 HTTP 快取
✅ 團隊熟悉 REST
✅ 公開 API
✅ 資源模型簡單5.2 選擇 GraphQL
markdown
✅ 複雜的關聯資料
✅ 多平台(Web、行動)
✅ 頻繁變更的需求
✅ 前端主導的團隊
✅ 需要即時訂閱5.3 決策樹
六、 混合使用
6.1 REST + GraphQL
javascript
// REST 用於簡單端點
app.get("/health", (req, res) => res.json({ status: "ok" }));
app.post("/auth/login", loginHandler);
app.post("/upload", uploadHandler);
// GraphQL 用於複雜查詢
app.use("/graphql", graphqlMiddleware);6.2 BFF 模式
七、 效能考量
7.1 GraphQL 查詢複雜度限制
javascript
const { createComplexityLimitRule } = require("graphql-validation-complexity");
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000), // 限制複雜度
],
});7.2 深度限制
javascript
const depthLimit = require("graphql-depth-limit");
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5), // 最多 5 層嵌套
],
});7.3 持久化查詢
javascript
// 預先註冊查詢
const queries = {
'getUserWithPosts': `
query GetUser($id: ID!) {
user(id: $id) {
name
posts { title }
}
}
`
}
// 請求只需發送 ID
POST /graphql
{ "id": "getUserWithPosts", "variables": { "id": "1" } }總結
| 面向 | REST | GraphQL |
|---|---|---|
| 學習曲線 | 低 | 中 |
| 靈活性 | 中 | 高 |
| 快取 | 易 | 難 |
| 效能 | 穩定 | 取決於查詢 |
| 適用場景 | 通用 | 複雜關聯 |
> **沒有絕對的對錯**——根據專案需求、團隊能力、時間限制來選擇。
進階挑戰
- 將一個 REST API 遷移到 GraphQL,比較開發體驗。
- 實作 GraphQL Subscription(即時訂閱)。
- 研究 tRPC,了解另一種類型安全的 API 方式。