关于API 设计的几点记录

这次做 API 设计改造,从 REST 到 GraphQL,。API 设计不好,前端开发就痛苦。

REST API

基础设计

// 用户相关 API
GET /api/users           // 获取用户列表
GET /api/users/:id       // 获取用户详情
POST /api/users          // 创建用户
PUT /api/users/:id       // 更新用户
DELETE /api/users/:id    // 删除用户

// 订单相关 API
GET /api/orders          // 获取订单列表
GET /api/orders/:id      // 获取订单详情
POST /api/orders         // 创建订单
PUT /api/orders/:id      // 更新订单
DELETE /api/orders/:id   // 删除订单

RESTful 设计原则

// 1. 资源导向
// 好的设计
GET /api/users/:userId/orders
POST /api/users/:userId/orders

// 不好的设计
GET /api/getUserOrders/:userId
POST /api/createOrderForUser/:userId

// 2. HTTP 方法语义化
GET /api/users/:id        // 获取资源
POST /api/users           // 创建资源
PUT /api/users/:id        // 更新资源
DELETE /api/users/:id     // 删除资源

// 3. 状态码正确使用
200 OK                   // 成功
201 Created              // 资源创建成功
204 No Content           // 删除成功
400 Bad Request          // 请求错误
401 Unauthorized        // 未授权
403 Forbidden           // 无权限
404 Not Found            // 资源不存在
500 Internal Server Error // 服务器错误

// 4. 版本控制
GET /api/v1/users
GET /api/v2/users

// 或使用请求头
GET /api/users
Headers:
  Accept: application/vnd.api.v2+json

REST API 实现

from flask import Flask, request, jsonify
from flask_restful import Api, Resource

app = Flask(__name__)
api = Api(app)

# 用户资源
class UserResource(Resource):
    def get(self, user_id):
        user = get_user_from_db(user_id)
        return jsonify(user), 200
    
    def put(self, user_id):
        data = request.get_json()
        user = update_user_in_db(user_id, data)
        return jsonify(user), 200
    
    def delete(self, user_id):
        delete_user_from_db(user_id)
        return '', 204

class UserListResource(Resource):
    def get(self):
        users = get_all_users_from_db()
        return jsonify(users), 200
    
    def post(self):
        data = request.get_json()
        user = create_user_in_db(data)
        return jsonify(user), 201

# 注册路由
api.add_resource(UserListResource, '/api/users')
api.add_resource(UserResource, '/api/users/<string:user_id>')

GraphQL

基础 Schema

# 定义 Schema
type User {
  id: ID!
  name: String!
  email: String!
  orders: [Order!]!
}

type Order {
  id: ID!
  user: User!
  total: Float!
  status: String!
  items: [OrderItem!]!
}

type OrderItem {
  id: ID!
  product: Product!
  quantity: Int!
  price: Float!
}

type Product {
  id: ID!
  name: String!
  price: Float!
  description: String
}

type Query {
  user(id: ID!): User
  users(limit: Int = 10, offset: Int = 0): [User!]!
  order(id: ID!): Order
  orders(limit: Int = 10): [Order!]!
}

type Mutation {
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String, email: String): User!
  deleteUser(id: ID!): Boolean!
  createOrder(userId: ID!, items: [OrderItemInput!]!): Order!
  updateOrder(id: ID!, status: String): Order!
}

input OrderItemInput {
  productId: ID!
  quantity: Int!
}

GraphQL 实现

from graphene import ObjectType, Field, List, ID, String, Float, Int, Schema, Mutation
import graphene_sqlalchemy

# 定义 GraphQL 类型
class User(ObjectType):
    id = Field(ID, required=True)
    name = Field(String, required=True)
    email = Field(String, required=True)
    orders = List(lambda: Order)
    
    def resolve_orders(self, info):
        return get_orders_by_user_id(self.id)

class Product(ObjectType):
    id = Field(ID, required=True)
    name = Field(String, required=True)
    price = Field(Float, required=True)
    description = Field(String)

class OrderItem(ObjectType):
    id = Field(ID, required=True)
    quantity = Field(Int, required=True)
    price = Field(Float, required=True)
    product = Field(Product)

class Order(ObjectType):
    id = Field(ID, required=True)
    total = Field(Float, required=True)
    status = Field(String, required=True)
    items = List(lambda: OrderItem)

class Query(ObjectType):
    user = Field(User, id=ID(required=True))
    users = List(User, limit=Int(10), offset=Int(0))
    order = Field(Order, id=ID(required=True))
    orders = List(Order, limit=Int(10))
    
    def resolve_user(self, info, id):
        return get_user_by_id(id)
    
    def resolve_users(self, info, limit=10, offset=0):
        return get_all_users(limit, offset)
    
    def resolve_order(self, info, id):
        return get_order_by_id(id)
    
    def resolve_orders(self, info, limit=10):
        return get_all_orders(limit)

class CreateUser(Mutation):
    class Arguments:
        name = String(required=True)
        email = String(required=True)
    
    Output = User
    
    def mutate(self, info, name, email):
        user = create_user(name, email)
        return user

class Mutation(ObjectType):
    create_user = CreateUser.Field()

schema = Schema(query=Query, mutation=Mutation)

GraphQL 查询示例

# 简单查询
query {
  user(id: "1") {
    id
    name
    email
  }
}

# 嵌套查询
query {
  user(id: "1") {
    id
    name
    email
    orders {
      id
      total
      status
      items {
        quantity
        price
        product {
          name
          description
        }
      }
    }
  }
}

# 批量查询
query {
  users(limit: 10) {
    id
    name
    email
    orders {
      id
      total
      status
    }
  }
}

API 版本控制

URL 版本控制

# v1 API
@app.route('/api/v1/users/<int:user_id>', methods=['GET'])
def get_user_v1(user_id):
    user = get_user_from_db(user_id)
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email
    })

# v2 API
@app.route('/api/v2/users/<int:user_id>', methods=['GET'])
def get_user_v2(user_id):
    user = get_user_from_db(user_id)
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email,
        'phone': user.phone,  # 新增字段
        'avatar': user.avatar   # 新增字段
    })

请求头版本控制

@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    version = request.headers.get('API-Version', 'v1')
    
    if version == 'v1':
        return jsonify(get_user_v1_data(user_id))
    elif version == 'v2':
        return jsonify(get_user_v2_data(user_id))
    else:
        return jsonify({'error': 'Unsupported version'}), 400

踩过的坑

坑一:过度嵌套

GraphQL 查询过度嵌套,导致性能问题。

解决:限制查询深度,使用 DataLoader。

from graphene_dataloader import DataLoader

# 使用 DataLoader 避免 N+1 查询
def create_user_loader():
    return DataLoader(load_users_by_ids)

def load_users_by_ids(user_ids):
    # 批量加载用户
    users = User.query.filter(User.id.in_(user_ids)).all()
    return {user.id: user for user in users}

坑二:字段命名不规范

字段命名不规范,前端开发困难。

解决:遵循命名规范,提供文档。

// REST API 响应示例
{
  "data": {
    "id": "1",
    "userName": "Alice",
    "userEmail": "[email protected]",
    "createdAt": "2023-01-01T00:00:00Z"
  }
}

// GraphQL Schema 示例
type User {
  id: ID!
  userName: String!
  userEmail: String!
  createdAt: DateTime!
}

坑三:错误处理不统一

错误处理不统一,前端难以处理。

解决:统一错误格式。

# 统一错误响应格式
@app.errorhandler(Exception)
def handle_error(error):
    return jsonify({
        'error': {
            'code': error.code,
            'message': error.message,
            'details': error.details if hasattr(error, 'details') else None
        }
    }), error.status_code if hasattr(error, 'status_code') else 500

写在最后

API 设计这东西,不只是技术,是用户体验和团队协作。

解决了

  • API 设计规范
  • 版本控制
  • 文档完善

带来了

  • 复杂度增加
  • 维护成本
  • 学习成本

选型之前先评估:

  • 业务需求
  • 团队能力
  • 性能要求
  • 生态支持

REST 适合

  • 简单 CRUD 操作
  • 需要良好的 HTTP 缓存
  • 团队熟悉 REST

GraphQL 适合

  • 复杂数据查询
  • 需要灵活的数据获取
  • 减少网络请求

不是所有场景都需要 GraphQL,有时候 REST 就够用。


这次 API 设计改造花了一个月,从 REST 到 GraphQL。改造完成后,网络请求减少了 60%,前端开发效率提升了 40%。

可用性说明:本文发布于 2021 年 6 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。

版权声明: 本文首发于 指尖魔法屋-关于API 设计的几点记录https://blog.thinkmoon.cn/post/95-api-design-rest-graphql-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!