REST 的兩個問題
Over-fetching:API 返回的資料比你需要的多。GET /users/1 返回 user 的所有欄位,但你只需要顯示 name 和 avatar——多餘的資料浪費頻寬,在 mobile 環境特別明顯。
Under-fetching:一個操作需要多次 API 呼叫。顯示用戶的 profile + 最近訂單 + 通知,需要 GET /users/1 + GET /users/1/orders?limit=5 + GET /users/1/notifications?unread=true——三次 round trip。
GraphQL 的解法:客戶端精確描述自己需要什麼,一次呼叫。
基本語法
Query(讀取):
query GetUserProfile {
user(id: "1") {
name
avatar
recentOrders(limit: 5) {
id
total
status
}
}
}Mutation(寫入):
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
id
total
status
}
}Subscription(實時推送):
subscription OnOrderUpdated($orderId: ID!) {
orderUpdated(orderId: $orderId) {
status
updatedAt
}
}Schema:後端定義型別,前端查詢欄位
GraphQL 有強型別的 schema 定義:
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
createdAt: String!
}
enum OrderStatus {
PENDING
SHIPPED
DELIVERED
}
type Query {
user(id: ID!): User
orders(status: OrderStatus): [Order!]!
}Schema 是前後端的合約——前端工程師知道能查什麼、每個欄位是什麼型別;後端工程師根據 schema 實作 resolver。
和 REST 的分工建議
GraphQL 不是 REST 的替代品——它解決了特定的問題,也帶來了新的複雜度(N+1 問題、caching 更複雜、error handling 不同)。
GraphQL 適合:
- 前端需要靈活地組合多種資料(BFF,Backend for Frontend)
- Mobile app(頻寬敏感,精確拿資料)
- 多種 client 有不同資料需求(web / mobile / 第三方)
REST 適合:
- 簡單的 CRUD API
- 公開 API(GraphQL 的 introspection 會暴露整個 schema)
- 已有成熟 REST client 生態的場景
GraphQL 特有的安全問題
REST 可以在 route 層加 middleware(app.get('/admin/users', requireAdmin, handler)),GraphQL 把所有操作集中在一個 endpoint(POST /graphql),這讓路由層的防護失效——授權邏輯必須移到 resolver 層。
授權(Authorization):每個 Resolver 自己負責
// 錯誤:以為 middleware 擋住了整個 GraphQL
app.use('/graphql', requireLogin, graphqlServer) // ❌ 只擋未登入,不擋越權
// 正確:resolver 內部根據角色檢查
const resolvers = {
Query: {
adminUsers: (_, __, context) => {
if (context.user?.role !== 'admin') throw new ForbiddenError('Not authorized')
return db.users.findAll()
},
// 欄位層級的授權:email 只有自己能看
user: (_, { id }, context) => {
const user = db.users.find(id)
if (user.id !== context.user?.id && context.user?.role !== 'admin') {
return { ...user, email: null } // 遮蔽敏感欄位
}
return user
}
}
}graphql-shield library 讓你把 authorization rule 統一管理,不散在每個 resolver 裡:
const permissions = shield({
Query: {
adminUsers: isAdmin,
user: or(isSelf, isAdmin),
}
})JWT 整合:在 Context 解析
const server = new ApolloServer({
context: ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '')
const user = token ? verifyJwt(token) : null
return { user, db } // 所有 resolver 透過 context.user 取得登入狀態
}
})生產環境關閉 Introspection
Introspection 讓任何人都能列舉你的整個 schema(所有 type、欄位、關係)——等同於把 API 設計完整暴露給攻擊者做偵查:
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production'
})Query Complexity / Depth 限制
GraphQL 的靈活性讓 client 可以寫出極深的 nested query,引爆 N+1 甚至 DoS:
query Bomb {
users {
friends { friends { friends { orders { items { product { reviews { author {
friends { friends { ... } }
} } } } } } } }
}
}import depthLimit from 'graphql-depth-limit'
import { createComplexityRule } from 'graphql-query-complexity'
const server = new ApolloServer({
validationRules: [
depthLimit(6),
createComplexityRule({ maximum: 1000 })
]
})深入後端 GraphQL 實作在 backend/api-design B09 章節。