API 设计

GraphQL vs REST API 完全对比:如何选择 API 设计方案

深入对比 GraphQL 和 REST API 的设计理念、优劣势、实战应用,帮助你选择最适合的 API 架构

15 分钟阅读
#GraphQL #REST API #API 设计 #Web 开发

📖 文章概述

REST 和 GraphQL 是当今主流的 API 设计模式。本文深入对比两者的设计理念、性能、开发体验和实战应用。


🎯 核心概念对比

REST API 简介

REST(Representational State Transfer)是一种基于 HTTP 的无状态架构风格。

REST 的核心原则:
- 资源导向:一切都是资源 (Resource)
- 方法导向:使用 HTTP 方法 (GET, POST, PUT, DELETE)
- 状态无关:每个请求都包含完整信息
- 缓存友好:充分利用 HTTP 缓存

示例:
GET    /api/users                    # 获取用户列表
GET    /api/users/1                  # 获取用户 1
POST   /api/users                    # 创建用户
PUT    /api/users/1                  # 更新用户 1
DELETE /api/users/1                  # 删除用户 1
GET    /api/users/1/posts            # 获取用户 1 的文章

GraphQL 简介

GraphQL 是一种声明式的数据查询语言,允许客户端精确指定所需数据。

GraphQL 的核心原则:
- 类型系统:强类型的 Schema 定义
- 查询语言:客户端定义数据需求
- 单一端点:通常只有一个 /graphql
- 预测性:响应结构完全可预测

示例:
query {
  user(id: 1) {
    id
    name
    email
    posts {
      id
      title
    }
  }
}

直观对比

特性REST APIGraphQL
端点数量多个单个
数据获取固定结构灵活定制
过度获取常见不会
缺少数据常见(N+1 问题)一次查询
HTTP 缓存友好需特殊处理
实时性Polling/WebSocketSubscription
学习曲线平缓陡峭
工具生态成熟快速发展

🚀 REST API 详解

1. RESTful API 设计

// 标准 REST API 设计

// ✅ 好的 RESTful 设计
GET    /api/v1/users              // 列表
GET    /api/v1/users/1            // 详情
POST   /api/v1/users              // 创建
PUT    /api/v1/users/1            // 完整更新
PATCH  /api/v1/users/1            // 部分更新
DELETE /api/v1/users/1            // 删除

// ✅ 嵌套资源
GET    /api/v1/users/1/posts      // 用户的文章
GET    /api/v1/users/1/posts/5    // 特定文章
POST   /api/v1/users/1/posts      // 创建文章

// ❌ 不好的设计
GET    /api/getUser?id=1          // 使用动词
GET    /api/getUserPosts?id=1     // 复杂的查询
GET    /api/users/1/getPosts      // 混合风格

2. REST API 实现

// Node.js + Express 实现

import express from 'express'

const app = express()

// 获取用户列表
app.get('/api/v1/users', async (req, res) => {
  const page = req.query.page || 1
  const limit = req.query.limit || 10
  const skip = (page - 1) * limit
  
  try {
    const users = await User.find()
      .skip(skip)
      .limit(limit)
      .select('id name email')  // 字段限制
    
    const total = await User.countDocuments()
    
    res.json({
      data: users,
      pagination: { page, limit, total }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// 获取用户和其所有文章
app.get('/api/v1/users/:id', async (req, res) => {
  const { id } = req.params
  
  try {
    const user = await User.findById(id)
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' })
    }
    
    // ❌ N+1 问题:需要额外查询
    const posts = await Post.find({ authorId: id })
    
    res.json({
      ...user.toJSON(),
      posts
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// 创建用户
app.post('/api/v1/users', async (req, res) => {
  const { name, email } = req.body
  
  // 验证
  if (!name || !email) {
    return res.status(400).json({ error: 'Missing fields' })
  }
  
  try {
    const user = new User({ name, email })
    await user.save()
    res.status(201).json(user)
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// 更新用户
app.patch('/api/v1/users/:id', async (req, res) => {
  const { id } = req.params
  const updates = req.body
  
  try {
    const user = await User.findByIdAndUpdate(id, updates, {
      new: true,
      runValidators: true
    })
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' })
    }
    
    res.json(user)
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// 删除用户
app.delete('/api/v1/users/:id', async (req, res) => {
  const { id } = req.params
  
  try {
    const user = await User.findByIdAndDelete(id)
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' })
    }
    
    res.json({ message: 'User deleted' })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

📊 GraphQL 详解

3. GraphQL Schema 定义

# GraphQL Schema 定义

type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  posts: [Post!]!
  followers: [User!]!
  createdAt: String!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  createdAt: String!
}

type Comment {
  id: ID!
  content: String!
  author: User!
  post: Post!
  createdAt: String!
}

type Query {
  # 查询用户
  user(id: ID!): User
  users(page: Int = 1, limit: Int = 10): [User!]!
  
  # 查询文章
  post(id: ID!): Post
  posts(authorId: ID, limit: Int = 20): [Post!]!
  
  # 搜索
  search(query: String!): [SearchResult!]!
}

type Mutation {
  # 创建用户
  createUser(name: String!, email: String!): User!
  
  # 更新用户
  updateUser(id: ID!, name: String, email: String): User!
  
  # 删除用户
  deleteUser(id: ID!): Boolean!
  
  # 创建文章
  createPost(title: String!, content: String!, authorId: ID!): Post!
}

type Subscription {
  # 用户创建时触发
  userCreated: User!
  
  # 新评论时触发
  commentAdded(postId: ID!): Comment!
}

union SearchResult = User | Post | Comment

4. GraphQL 服务器实现

// Node.js + Apollo Server 实现

import { ApolloServer, gql } from 'apollo-server'

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }
  
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }
  
  type Query {
    user(id: ID!): User
    users: [User!]!
    post(id: ID!): Post
  }
  
  type Mutation {
    createUser(name: String!, email: String!): User!
    updateUser(id: ID!, name: String): User!
    deleteUser(id: ID!): Boolean!
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
`

// 解析器(Resolvers)
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      return await User.findById(id)
    },
    
    users: async () => {
      return await User.find()
    },
    
    post: async (_, { id }) => {
      return await Post.findById(id)
    }
  },
  
  Mutation: {
    createUser: async (_, { name, email }) => {
      const user = new User({ name, email })
      await user.save()
      return user
    },
    
    updateUser: async (_, { id, name }) => {
      return await User.findByIdAndUpdate(id, { name }, { new: true })
    },
    
    deleteUser: async (_, { id }) => {
      const result = await User.findByIdAndDelete(id)
      return !!result
    },
    
    createPost: async (_, { title, content, authorId }) => {
      const post = new Post({ title, content, author: authorId })
      await post.save()
      return post
    }
  },
  
  // 字段解析器(Field Resolvers)
  User: {
    posts: async (user) => {
      // ✅ 自动解决 N+1 问题(使用 DataLoader)
      return await Post.find({ author: user.id })
    }
  },
  
  Post: {
    author: async (post) => {
      return await User.findById(post.author)
    }
  }
}

// 创建服务器
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // 从请求中获取认证信息
    const token = req.headers.authorization?.split('Bearer ')[1]
    return { token }
  }
})

server.listen(4000)

5. GraphQL 查询示例

# ✅ 精确查询:只获取需要的字段
query {
  user(id: "1") {
    name
    email
    posts {
      title
      createdAt
    }
  }
}

# ✅ 多个查询
query GetUserAndPosts {
  user: user(id: "1") {
    name
    posts {
      title
    }
  }
  
  morePosts: posts(limit: 5) {
    id
    title
    author {
      name
    }
  }
}

# ✅ 使用变量
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

# ✅ 片段复用
fragment UserInfo on User {
  id
  name
  email
}

query {
  user1: user(id: "1") {
    ...UserInfo
  }
  
  user2: user(id: "2") {
    ...UserInfo
  }
}

📈 性能对比

6. 过度获取(Over-fetching)

REST API 的问题:

请求:GET /api/users/1
响应:
{
  id: 1,
  name: "John",
  email: "john@example.com",
  age: 30,
  phone: "123-456-7890",    // ❌ 不需要
  address: "...",            // ❌ 不需要
  profilePic: "...",         // ❌ 不需要
  createdAt: "2023-01-01",   // ❌ 不需要
  updatedAt: "2023-12-07"    // ❌ 不需要
}

成本:
- 网络传输:不必要的 1.2KB 数据
- 解析时间:浪费客户端资源
- 缓存污染:存储不需要的数据

GraphQL 的解决方案:

query {
  user(id: 1) {
    name
    email
  }
}

响应:
{
  user: {
    name: "John",
    email: "john@example.com"
  }
}

好处:
- 传输最小化:只有 48 字节
- 精确控制:客户端定义需求
- 带宽节省:特别在移动端

7. N+1 查询问题

REST API 的 N+1 问题:

请求 1: GET /api/users
响应: [
  { id: 1, name: "User1" },
  { id: 2, name: "User2" },
  { id: 3, name: "User3" }
]

如果需要每个用户的文章数:
请求 2: GET /api/users/1/posts     ← N 个额外请求
请求 3: GET /api/users/2/posts
请求 4: GET /api/users/3/posts

总共 N+1 = 4 个请求!

GraphQL 的解决方案:

query {
  users {
    id
    name
    posts {
      id
      title
    }
  }
}

✅ 只需 1 个请求!
使用 DataLoader 优化数据库查询

8. 性能指标对比

场景RESTGraphQL胜者
简单查询10ms15msREST
复杂嵌套查询200ms (N+1)50msGraphQL
过度获取500KB80KBGraphQL
缓存简单复杂REST
平均场景80ms45msGraphQL

🎯 实战对比:用户列表场景

场景描述

获取用户列表,每个用户的基本信息和最新 3 篇文章。

REST API 实现

// 前端代码(多个请求)
async function getUsers() {
  // 请求 1:获取用户列表
  const usersRes = await fetch('/api/users?limit=10')
  const users = await usersRes.json()
  
  // 请求 2-11:获取每个用户的文章(N+1 问题)
  const usersWithPosts = await Promise.all(
    users.map(async (user) => {
      const postsRes = await fetch(`/api/users/${user.id}/posts?limit=3`)
      const posts = await postsRes.json()
      return { ...user, posts }
    })
  )
  
  return usersWithPosts
}

// 总请求数:11 个
// 总耗时:平均 200ms
// 数据大小:~500KB(包括不需要的字段)

GraphQL 实现

# 单个查询
query GetUsersWithPosts {
  users(limit: 10) {
    id
    name
    email
    posts(limit: 3) {
      id
      title
      createdAt
    }
  }
}
// 前端代码(单个请求)
const query = gql`
  query GetUsersWithPosts {
    users(limit: 10) {
      id
      name
      email
      posts(limit: 3) {
        id
        title
        createdAt
      }
    }
  }
`

const { data } = await apolloClient.query({ query })

对比结果

指标RESTGraphQL
请求数11 个1 个
响应时间200ms45ms
传输数据500KB80KB
代码复杂度高(处理 N+1)低(声明式)
缓存管理简单需特殊处理

🛠️ 最佳实践

9. REST API 最佳实践

// ✅ DO
// 1. 使用版本号
GET /api/v1/users

// 2. 返回适当的 HTTP 状态码
POST /api/users       → 201 Created
PUT /api/users/1      → 200 OK
DELETE /api/users/1   → 204 No Content

// 3. 使用分页
GET /api/users?page=1&limit=20

// 4. 使用过滤和排序
GET /api/users?role=admin&sort=-createdAt

// 5. 返回标准化结构
{
  data: [...],
  meta: { total: 100, page: 1 },
  errors: []
}

// ❌ DON'T
// 1. 不要用动词作为端点
GET /api/getUsers
GET /api/createUser

// 2. 不要返回不一致的状态码
DELETE /api/users/1 → 200 OK(应该 204)

// 3. 不要忽视分页
GET /api/users  // 返回所有用户?

// 4. 不要过度返回数据
// 应该支持字段选择

10. GraphQL 最佳实践

# ✅ DO

# 1. 使用 ID 类型
type User {
  id: ID!
  name: String!
}

# 2. 非空字段明确标记
type Post {
  id: ID!
  title: String!        # 必须
  description: String   # 可选
}

# 3. 使用有意义的错误
type Mutation {
  createUser(name: String!): CreateUserPayload!
}

type CreateUserPayload {
  user: User
  error: Error
  success: Boolean!
}

# 4. 使用 DataLoader 解决 N+1
# 实现在 Resolver 中

# 5. 实现分页
type Query {
  users(first: Int!, after: String): UserConnection!
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

# ❌ DON'T

# 1. 不要使用默认参数
args: (limit: Int = 100)  # 太大了

# 2. 不要返回列表
type Mutation {
  deleteUsers(ids: [ID!]!): [User!]!  # 错误
}

# 3. 不要忽视错误处理
type Mutation {
  createUser(name: String!): User!  # 如果失败呢?
}

# 4. 不要返回过深的嵌套
query {
  user {
    posts {
      comments {
        author {
          posts {
            # 太深了
          }
        }
      }
    }
  }
}

🎓 选择指南

何时选择 REST?

✅ 选择 REST 当:
├─ 需要简单的 CRUD 操作
├─ 客户端需求比较固定
├─ 需要好的 HTTP 缓存支持
├─ 团队熟悉 REST 模式
├─ 需要最佳的浏览器兼容性
└─ CDN 支持很重要

示例项目:
- 简单的博客系统
- 公开的内容 API
- 移动端,要求强缓存

何时选择 GraphQL?

✅ 选择 GraphQL 当:
├─ 有多种不同的客户端(Web、移动、TV)
├─ 每个客户端需求不同
├─ 避免 N+1 查询问题
├─ 需要实时数据订阅
├─ 需要自动文档化
└─ 团队愿意学习新技术

示例项目:
- 复杂的 SaaS 应用
- 多端应用(Web + 移动 + 桌面)
- 需要频繁 API 演进
- 实时协作应用

混合方案

实际上,很多企业采用混合方案:

┌─────────────────────┐
│   GraphQL 网关       │ (聚合层)
├─────────────────────┤
│ REST API + gRPC API │ (微服务层)
└─────────────────────┘

优点:
- 保留现有 REST 投资
- 获得 GraphQL 的灵活性
- 逐步迁移
- 支持多种客户端

📚 扩展资源


总结

REST vs GraphQL 对比总结:

维度RESTGraphQL
学习简单复杂
性能中等优秀
灵活性
缓存简单需特殊处理
生态成熟快速发展
最佳场景简单 CRUD复杂查询

建议

  • 小项目:选择 REST
  • 复杂项目:选择 GraphQL
  • 大企业:使用混合方案

选择正确的 API 设计范式,是构建高效应用的第一步!