🔄 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: '