REST 的兩個問題

Over-fetching:API 返回的資料比你需要的多。GET /users/1 返回 user 的所有欄位,但你只需要顯示 nameavatar——多餘的資料浪費頻寬,在 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 章節。