跳至正文

🔄 REST API vs GraphQL

📖 定義

REST API是使用HTTP方法以資源為中心設計的傳統API架構。GraphQL是Facebook開發的查詢語言,是一種靈活的API方式,允許客戶端精確請求所需的資料。REST有多個端點,而GraphQL通過單個端點處理所有資料。

🎯 通過比喻理解

餐廳 vs 自助餐

REST API = 餐廳菜單
├─ 只能訂購預定義的菜單
├─ 每個菜單都有固定的組成
├─ 「請給我一份披薩」→ 得到整個披薩
├─ 難以去除或添加配料
└─ 簡單且可預測

GraphQL = 自助餐
├─ 只挑選想要的食物
├─ 只選擇需要的量
├─ 「只要起司」→ 只得到起司
├─ 可以自由組合
└─ 靈活但可能複雜

圖書館借書

REST API = 傳統借閱
圖書管理員:「您需要什麼書?」
我:「請給我電腦相關的書」
圖書管理員:「這些全部給您」(10本書)
我:「我只需要第1章...」(其餘不需要)

GraphQL = 智慧借閱
我:「我只需要電腦書的第3章」
圖書管理員:「只給您影印第3章」(正好是所需的)
我:「完美!」

⚙️ 工作原理

1. 資料請求方式比較

REST API: 多個端點
GET /users/1 → 使用者資訊
GET /users/1/posts → 使用者的貼文
GET /posts/1/comments → 貼文的留言

總共需要3次請求!

GraphQL: 單個端點
POST /graphql
{
user(id: 1) {
name
posts {
title
comments {
text
}
}
}
}

1次請求獲取所有資料!

2. Over-fetching vs Under-fetching

REST API問題

Over-fetching(接收不必要的資料)
GET /users/1
{
"id": 1,
"name": "王小明",
"email": "wang@example.com",
"phone": "0912-345-678",
"address": "台北市...",
"createdAt": "2024-01-01",
// 只需要名字卻接收了所有資訊!
}

Under-fetching(資料不足)
GET /users/1 → 使用者資訊
GET /users/1/posts → 需要額外請求
GET /users/1/friends → 又需要額外請求
// 需要多次請求!

GraphQL解決方案

精確所需
{
user(id: 1) {
name // 只請求名字!
}
}
→ { "name": "王小明" }

一次性獲取所有
{
user(id: 1) {
name
posts { title }
friends { name }
}
}
→ 所有資料一次性獲取!

3. API設計哲學

REST: 以資源為中心
┌─────────────────┐
│ /users │ → 使用者列表
│ /users/1 │ → 特定使用者
│ /posts │ → 貼文列表
│ /posts/1 │ → 特定貼文
└─────────────────┘
每個資源一個端點

GraphQL: 以查詢為中心
┌─────────────────┐
│ /graphql │ → 所有請求
└─────────────────┘

┌────────┐
│ Query │ → 讀取資料
│Mutation│ → 修改資料
│Subscribe│ → 即時訂閱
└────────┘

💡 實際範例

REST API範例 (Express.js)

// Express.js REST API實作
const express = require('express');
const app = express();

app.use(express.json());

// 資料(實際中使用資料庫)
const users = [
{
id: 1,
name: '王小明',
email: 'wang@example.com',
age: 25
},
{
id: 2,
name: '李小華',
email: 'li@example.com',
age: 30
}
];

const posts = [
{
id: 1,
userId: 1,
title: 'REST API介紹',
content: 'REST是...'
},
{
id: 2,
userId: 1,
title: 'GraphQL介紹',
content: 'GraphQL是...'
}
];

// ========== GET: 獲取所有使用者 ==========
app.get('/api/users', (req, res) => {
res.json(users);
});

// ========== GET: 獲取特定使用者 ==========
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '找不到使用者' });
}
res.json(user);
});

// ========== POST: 建立使用者 ==========
app.post('/api/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
age: req.body.age
};
users.push(newUser);
res.status(201).json(newUser);
});

// ========== PUT: 更新使用者 ==========
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '找不到使用者' });
}

user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
user.age = req.body.age || user.age;

res.json(user);
});

// ========== DELETE: 刪除使用者 ==========
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: '找不到使用者' });
}

users.splice(index, 1);
res.status(204).send();
});

// ========== GET: 獲取使用者的貼文 ==========
app.get('/api/users/:id/posts', (req, res) => {
const userId = parseInt(req.params.id);
const userPosts = posts.filter(p => p.userId === userId);
res.json(userPosts);
});

// ========== GET: 獲取特定貼文 ==========
app.get('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: '找不到貼文' });
}
res.json(post);
});

app.listen(3000, () => {
console.log('REST API伺服器執行中: http://localhost:3000');
});

REST API客戶端使用

// ========== REST API使用 ==========

// 1. 獲取使用者列表
const response1 = await fetch('http://localhost:3000/api/users');
const users = await response1.json();
console.log(users);

// 2. 獲取特定使用者
const response2 = await fetch('http://localhost:3000/api/users/1');
const user = await response2.json();
console.log(user);

// 3. 建立使用者
const response3 = await fetch('http://localhost:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '陳小美',
email: 'chen@example.com',
age: 28
})
});
const newUser = await response3.json();

// 4. 更新使用者
const response4 = await fetch('http://localhost:3000/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
age: 26
})
});

// 5. 刪除使用者
await fetch('http://localhost:3000/api/users/1', {
method: 'DELETE'
});

// ========== 問題: 需要多次請求 ==========
// 如何同時獲取使用者和貼文?
const userResponse = await fetch('http://localhost:3000/api/users/1');
const user = await userResponse.json();

const postsResponse = await fetch('http://localhost:3000/api/users/1/posts');
const posts = await postsResponse.json();

console.log({ user, posts }); // 2次請求!

GraphQL範例 (Apollo Server)

// GraphQL伺服器實作
const { ApolloServer, gql } = require('apollo-server');

// 資料(實際中使用資料庫)
const users = [
{ id: 1, name: '王小明', email: 'wang@example.com', age: 25 },
{ id: 2, name: '李小華', email: 'li@example.com', age: 30 }
];

const posts = [
{ id: 1, userId: 1, title: 'REST API介紹', content: 'REST是...' },
{ id: 2, userId: 1, title: 'GraphQL介紹', content: 'GraphQL是...' },
{ id: 3, userId: 2, title: 'Node.js教學', content: 'Node是...' }
];

// ========== 模式定義 (Type Definitions) ==========
const typeDefs = gql`
# 使用者類型
type User {
id: Int!
name: String!
email: String!
age: Int
posts: [Post!]! # 使用者的貼文(關係)
}

# 貼文類型
type Post {
id: Int!
title: String!
content: String!
author: User! # 貼文作者(關係)
}

# 查詢(讀取資料)
type Query {
# 所有使用者
users: [User!]!

# 特定使用者
user(id: Int!): User

# 所有貼文
posts: [Post!]!

# 特定貼文
post(id: Int!): Post
}

# 變更(修改資料)
type Mutation {
# 建立使用者
createUser(name: String!, email: String!, age: Int): User!

# 更新使用者
updateUser(id: Int!, name: String, email: String, age: Int): User

# 刪除使用者
deleteUser(id: Int!): Boolean!

# 建立貼文
createPost(userId: Int!, title: String!, content: String!): Post!
}
`;

// ========== 解析器(資料獲取方法) ==========
const resolvers = {
Query: {
// 獲取所有使用者
users: () => users,

// 獲取特定使用者
user: (parent, args) => {
return users.find(u => u.id === args.id);
},

// 獲取所有貼文
posts: () => posts,

// 獲取特定貼文
post: (parent, args) => {
return posts.find(p => p.id === args.id);
}
},

Mutation: {
// 建立使用者
createUser: (parent, args) => {
const newUser = {
id: users.length + 1,
name: args.name,
email: args.email,
age: args.age
};
users.push(newUser);
return newUser;
},

// 更新使用者
updateUser: (parent, args) => {
const user = users.find(u => u.id === args.id);
if (!user) return null;

if (args.name) user.name = args.name;
if (args.email) user.email = args.email;
if (args.age) user.age = args.age;

return user;
},

// 刪除使用者
deleteUser: (parent, args) => {
const index = users.findIndex(u => u.id === args.id);
if (index === -1) return false;

users.splice(index, 1);
return true;
},

// 建立貼文
createPost: (parent, args) => {
const newPost = {
id: posts.length + 1,
userId: args.userId,
title: args.title,
content: args.content
};
posts.push(newPost);
return newPost;
}
},

// ========== 關係解析器 ==========
User: {
// 獲取使用者的貼文
posts: (parent) => {
return posts.filter(p => p.userId === parent.id);
}
},

Post: {
// 獲取貼文的作者
author: (parent) => {
return users.find(u => u.id === parent.userId);
}
}
};

// ========== 啟動伺服器 ==========
const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
console.log(`GraphQL伺服器執行中: ${url}`);
});

GraphQL客戶端使用

// ========== GraphQL查詢範例 ==========

// 1. 只查詢名字(防止Over-fetching)
query {
user(id: 1) {
name
}
}
// 回應: { "data": { "user": { "name": "王小明" } } }

// 2. 一次性查詢使用者和貼文(防止Under-fetching)
query {
user(id: 1) {
name
email
posts {
title
content
}
}
}
// 回應:
{
"data": {
"user": {
"name": "王小明",
"email": "wang@example.com",
"posts": [
{ "title": "REST API介紹", "content": "REST是..." },
{ "title": "GraphQL介紹", "content": "GraphQL是..." }
]
}
}
}

// 3. 同時查詢多個資源
query {
users {
name
}
posts {
title
}
}

// 4. 使用變數
query GetUser($userId: Int!) {
user(id: $userId) {
name
email
}
}
// 變數: { "userId": 1 }

// ========== Mutation(修改資料) ==========

// 1. 建立使用者
mutation {
createUser(name: "陳小美", email: "chen@example.com", age: 28) {
id
name
email
}
}

// 2. 更新使用者
mutation {
updateUser(id: 1, age: 26) {
id
name
age
}
}

// 3. 刪除使用者
mutation {
deleteUser(id: 1)
}

// ========== JavaScript客戶端 ==========
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables
})
});

return response.json();
}

// 查詢使用者和貼文
const query = `
query GetUserWithPosts($userId: Int!) {
user(id: $userId) {
name
email
posts {
title
}
}
}
`;

const data = await fetchGraphQL(query, { userId: 1 });
console.log(data);

使用Apollo Client整合React

// ========== Apollo Client設定 ==========
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation, gql } from '@apollo/client';

// 建立客戶端
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});

// 將Provider應用到應用程式
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}

// ========== 使用查詢 ==========
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
title
}
}
}
`;

function UserList() {
const { loading, error, data } = useQuery(GET_USERS);

if (loading) return <p>載入中...</p>;
if (error) return <p>錯誤: {error.message}</p>;

return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<ul>
{user.posts.map(post => (
<li key={post.title}>{post.title}</li>
))}
</ul>
</div>
))}
</div>
);
}

// ========== 使用Mutation ==========
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!, $age: Int) {
createUser(name: $name, email: $email, age: $age) {
id
name
email
}
}
`;

function CreateUserForm() {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER);

const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);

createUser({
variables: {
name: formData.get('name'),
email: formData.get('email'),
age: parseInt(formData.get('age'))
}
});
};

if (loading) return <p>建立中...</p>;
if (error) return <p>錯誤: {error.message}</p>;
if (data) return <p>建立完成: {data.createUser.name}</p>;

return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="姓名" required />
<input name="email" type="email" placeholder="電子郵件" required />
<input name="age" type="number" placeholder="年齡" />
<button type="submit">建立使用者</button>
</form>
);
}

// ========== 快取和最佳化 ==========
const GET_USER = gql`
query GetUser($id: Int!) {
user(id: $id) {
id
name
email
}
}
`;

function UserProfile({ userId }) {
const { loading, error, data, refetch } = useQuery(GET_USER, {
variables: { id: userId },
// 快取策略
fetchPolicy: 'cache-first', // 快取優先
// fetchPolicy: 'network-only', // 總是請求伺服器
// fetchPolicy: 'cache-and-network', // 顯示快取並更新
});

// 手動重新整理
const handleRefresh = () => {
refetch();
};

if (loading) return <p>載入中...</p>;
if (error) return <p>錯誤: {error.message}</p>;

return (
<div>
<h2>{data.user.name}</h2>
<p>{data.user.email}</p>
<button onClick={handleRefresh}>重新整理</button>
</div>
);
}

🤔 常見問題

Q1. 應該選擇REST API還是GraphQL?

A: 根據專案特點選擇:

✅ 選擇REST API的情況:
├─ 簡單的CRUD應用程式
├─ 資源結構清晰
├─ 快取很重要(利用HTTP快取)
├─ 團隊熟悉REST
├─ 檔案上傳/下載較多
└─ 例如: 部落格、電商基本功能

✅ 選擇GraphQL的情況:
├─ 複雜的資料關係
├─ 行動應用程式(資料節省很重要)
├─ 客戶端多樣(網頁、應用程式、平板)
├─ 希望快速前端開發
├─ 需要即時資料訂閱
└─ 例如: 社群媒體、儀表板、即時應用程式

🤝 也可以一起使用:
├─ 主要API使用GraphQL
├─ 檔案上傳使用REST
└─ 發揮各自優勢

Q2. 效能差異如何?

A:

// REST API效能特性
優點:
- 可利用HTTP快取
GET /api/users/1
Cache-Control: max-age=3600

- CDN快取簡單
- 簡單端點快速

缺點:
- Over-fetching傳輸不必要的資料
GET /api/users/1
回傳所有欄位(100KB)

- Under-fetching需要多次請求
GET /api/users/1 // 第1次請求
GET /api/users/1/posts // 第2次請求
GET /api/posts/1/comments // 第3次請求
// 總共3次請求!

// GraphQL效能特性
優點:
- 只有精確需要的資料(10KB)
query {
user(id: 1) {
name
email
}
}

- 一次請求獲取所有資料
query {
user(id: 1) {
name
posts {
title
comments { text }
}
}
}
// 1次請求完成!

缺點:
- HTTP快取困難(使用POST請求)
- 複雜查詢增加伺服器負擔
- N+1問題(使用DataLoader解決)

實際基準測試:
REST: 3次請求, 總共200KB, 300ms
GraphQL: 1次請求, 總共50KB, 150ms
→ 行動環境中GraphQL更有優勢

Q3. GraphQL的N+1問題是什麼?

A: 可以用DataLoader解決:

// ========== N+1問題 ==========
// 查詢10個使用者和各自的貼文

// 問題: 低效查詢
const resolvers = {
Query: {
users: () => {
return db.users.findAll(); // 1次查詢
}
},
User: {
posts: (user) => {
return db.posts.findByUserId(user.id); // 每個使用者1次!
}
}
};

// 總查詢: 1 + 10 = 11次
// 1次: 查詢使用者
// 10次: 查詢每個使用者的貼文

// ========== 使用DataLoader解決 ==========
const DataLoader = require('dataloader');

// 批次獲取貼文
const postLoader = new DataLoader(async (userIds) => {
// 一次性獲取所有貼文
const posts = await db.posts.findByUserIds(userIds);

// 按使用者分組
const postsByUser = {};
posts.forEach(post => {
if (!postsByUser[post.userId]) {
postsByUser[post.userId] = [];
}
postsByUser[post.userId].push(post);
});

// 回傳每個使用者的貼文
return userIds.map(id => postsByUser[id] || []);
});

const resolvers = {
User: {
posts: (user) => {
return postLoader.load(user.id); // 批次處理!
}
}
};

// 總查詢: 1 + 1 = 2次
// 1次: 查詢使用者
// 1次: 查詢所有貼文(批次)
// → 效能大幅提升!

// ========== 實際範例 ==========
const { ApolloServer } = require('apollo-server');
const DataLoader = require('dataloader');

const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
// 每個請求建立新的DataLoader
loaders: {
postLoader: new DataLoader(batchGetPosts),
userLoader: new DataLoader(batchGetUsers)
}
})
});

// 在解析器中使用
const resolvers = {
User: {
posts: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
};

Q4. REST API版本管理 vs GraphQL模式演化?

A:

// ========== REST API版本管理 ==========

// 方法1: URL中包含版本
GET /api/v1/users
GET /api/v2/users // 新版本

// 方法2: 在標頭指定版本
GET /api/users
Accept: application/vnd.myapp.v1+json

// 問題:
// - 維護多個版本
// - 客戶端需要更新
// - 舊版本支援負擔

// 範例: 欄位名稱更改
// v1
{
"name": "王小明"
}

// v2
{
"fullName": "王小明" // name → fullName
}

// ========== GraphQL模式演化 ==========

// 新增很容易
type User {
id: Int!
name: String!
email: String! # 現有
phone: String # 新增(新欄位)
address: Address # 新增(新類型)
}

// 更改: 使用@deprecated
type User {
id: Int!
name: String! @deprecated(reason: "Use fullName instead")
fullName: String! # 新欄位
email: String!
}

// 客戶端逐步更新
query {
user(id: 1) {
name # 仍然有效(deprecated)
fullName # 使用新欄位
}
}

// 優點:
// - 單個端點
// - 無需版本管理
// - 漸進式遷移
// - 客戶端可以在希望的時候更新

Q5. 如何處理即時資料?

A:

// ========== REST API: 輪詢或SSE ==========

// 1. 輪詢
setInterval(async () => {
const response = await fetch('/api/messages');
const messages = await response.json();
updateUI(messages);
}, 3000); // 每3秒請求一次
// 缺點: 低效,伺服器負擔大

// 2. Server-Sent Events (SSE)
const eventSource = new EventSource('/api/messages/stream');
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
updateUI(message);
};
// 缺點: 只能單向通訊

// ========== GraphQL: Subscription ==========

// 伺服器設定
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();

const typeDefs = gql`
type Message {
id: Int!
text: String!
userId: Int!
createdAt: String!
}

type Subscription {
messageAdded: Message!
}

type Mutation {
addMessage(text: String!): Message!
}
`;

const resolvers = {
Mutation: {
addMessage: (parent, args) => {
const message = {
id: messages.length + 1,
text: args.text,
userId: 1,
createdAt: new Date().toISOString()
};
messages.push(message);

// 通知訂閱者
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message
});

return message;
}
},

Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};

// 客戶端(React)
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messageAdded {
id
text
userId
createdAt
}
}
`;

function ChatRoom() {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);

useEffect(() => {
if (data) {
console.log('新訊息:', data.messageAdded);
addMessageToUI(data.messageAdded);
}
}, [data]);

return <div>{/* UI */}</div>;
}

// 優點:
// - 即時雙向通訊
// - 基於WebSocket
// - 高效

🎓 下一步

了解了REST API和GraphQL後,可以學習:

  1. 什麼是API? (文件編寫中) - API基本概念
  2. JWT令牌 (文件編寫中) - API驗證
  3. 什麼是WebSocket? (文件編寫中) - 即時通訊

實作練習

# ========== REST API實作(Express) ==========
mkdir rest-api-demo
cd rest-api-demo
npm init -y
npm install express

# 編寫server.js後
node server.js

# 測試
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"王小明","email":"wang@example.com"}'

# ========== GraphQL實作(Apollo Server) ==========
mkdir graphql-demo
cd graphql-demo
npm init -y
npm install apollo-server graphql

# 編寫server.js後
node server.js

# 開啟GraphQL Playground
# http://localhost:4000
# 可以在瀏覽器中執行查詢!

# ========== React + GraphQL ==========
npx create-react-app my-app
cd my-app
npm install @apollo/client graphql

# 設定Apollo Client後執行
npm start

🎬 總結

REST API和GraphQL各有優缺點:

  • REST API: 簡單、熟悉、快取容易
  • GraphQL: 靈活、高效、支援即時
  • 選擇標準: 專案複雜度、團隊經驗、效能需求
  • 混合使用: 根據需要同時使用兩種方式

用正確的API設計建構優秀的應用程式! 🔄