关于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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。