关于 api-design-principles
此 Skill 汇集 REST 与 GraphQL API 的设计原则、模式和检查要点,面向 API 设计、评审及文档编写工作。内容涵盖版本管理、分页、错误处理和 GraphQL Schema 等实践。
主要功能
- 01REST 资源与 HTTP 方法设计
- 02GraphQL Schema 与解析器模式
- 03API 版本管理策略
- 04分页、过滤与错误处理
- 05提供模板、检查清单和 OpenAPI 生成脚本
适用场景
- 01设计新的 REST 或 GraphQL API
- 02评审 API 规范并制定团队标准
- 03规划 API 版本迁移
- 04编写面向开发者的 API 文档
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ boshi-xixixi/ traeskill --skill api-design-principles安装后在 Claude Code 中输入/api-design-principles确认已加载。
附带文件AGENTS.md
详细介绍
API 设计原则
掌握 REST 和 GraphQL API 设计原则,构建直观、可扩展且易于维护的 API,让开发者用得顺心,经得起时间考验。
适用场景
- 设计新的 REST 或 GraphQL API
- 重构现有 API,提升易用性
- 为团队制定 API 设计标准
- 在实现前审查 API 规范
- 在不同 API 范式之间迁移(如从 REST 迁移到 GraphQL)
- 编写便于开发者使用的 API 文档
- 针对特定用例(移动端、第三方集成)优化 API
交互式设计流程
- 明确需求:敲定设计前,确保你了解相关领域和限制条件。
- 反馈循环:如果可以使用
mcp-feedback-enhanced工具(例如ask_followup_question),请用它提出澄清问题或提供设计选项供评审。如果无法使用,请通过常规聊天交流。
核心概念
1. RESTful 设计原则
面向资源的架构
- 资源是名词(用户、订单、产品),而非动词
- 使用 HTTP 方法执行操作(GET、POST、PUT、PATCH、DELETE)
- URL 表示资源层级
- 采用一致的命名约定
HTTP 方法语义:
GET:获取资源(幂等、安全)POST:创建新资源PUT:替换整个资源(幂等)PATCH:部分更新资源DELETE:删除资源(幂等)
2. GraphQL 设计原则
以 Schema 为先的开发
- 类型定义领域模型
- 查询用于读取数据
- 变更用于修改数据
- 订阅用于实时更新
查询结构:
- 客户端只请求所需的数据
- 单一端点,支持多种操作
- 强类型 Schema
- 内置自省功能
3. API 版本管理策略
URL 版本管理:
/api/v1/users
/api/v2/users
Header 版本管理:
Accept: application/vnd.api+json; version=1
查询参数版本管理:
/api/users?version=1
REST API 设计模式
模式 1:资源集合设计
# Good: Resource-oriented endpoints
GET /api/users # List users (with pagination)
POST /api/users # Create user
GET /api/users/{id} # Get specific user
PUT /api/users/{id} # Replace user
PATCH /api/users/{id} # Update user fields
DELETE /api/users/{id} # Delete user
# Nested resources
GET /api/users/{id}/orders # Get user's orders
POST /api/users/{id}/orders # Create order for user
# Bad: Action-oriented endpoints (avoid)
POST /api/createUser
POST /api/getUserById
POST /api/deleteUser
模式 2:分页与筛选
from typing import List, Optional
from pydantic import BaseModel, Field
class PaginationParams(BaseModel):
page: int = Field(1, ge=1, description="Page number")
page_size: int = Field(20, ge=1, le=100, description="Items per page")
class FilterParams(BaseModel):
status: Optional[str] = None
created_after: Optional[str] = None
search: Optional[str] = None
class PaginatedResponse(BaseModel):
items: List[dict]
total: int
page: int
page_size: int
pages: int
@property
def has_next(self) -> bool:
return self.page < self.pages
@property
def has_prev(self) -> bool:
return self.page > 1
# FastAPI endpoint example
from fastapi import FastAPI, Query, Depends
app = FastAPI()
@app.get("/api/users", response_model=PaginatedResponse)
async def list_users(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
status: Optional[str] = Query(None),
search: Optional[str] = Query(None)
):
# Apply filters
query = build_query(status=status, search=search)
# Count total
total = await count_users(query)
# Fetch page
offset = (page - 1) * page_size
users = await fetch_users(query, limit=page_size, offset=offset)
return PaginatedResponse(
items=users,
total=total,
page=page,
page_size=page_size,
pages=(total + page_size - 1) // page_size
)
模式 3:错误处理与状态码
from fastapi import HTTPException, status
from pydantic import BaseModel
class ErrorResponse(BaseModel):
error: str
message: str
details: Optional[dict] = None
timestamp: str
path: str
class ValidationErrorDetail(BaseModel):
field: str
message: str
value: Any
# Consistent error responses
STATUS_CODES = {
"success": 200,
"created": 201,
"no_content": 204,
"bad_request": 400,
"unauthorized": 401,
"forbidden": 403,
"not_found": 404,
"conflict": 409,
"unprocessable": 422,
"internal_error": 500
}
def raise_not_found(resource: str, id: str):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"error": "NotFound",
"message": f"{resource} not found",
"details": {"id": id}
}
)
def raise_validation_error(errors: List[ValidationErrorDetail]):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"error": "ValidationError",
"message": "Request validation failed",
"details": {"errors": [e.dict() for e in errors]}
}
)
# Example usage
@app.get("/api/users/{user_id}")
async def get_user(user_id: str):
user = await fetch_user(user_id)
if not user:
raise_not_found("User", user_id)
return user
模式 4:HATEOAS(超媒体作为应用状态引擎)
class UserResponse(BaseModel):
id: str
name: str
email: str
_links: dict
@classmethod
def from_user(cls, user: User, base_url: str):
return cls(
id=user.id,
name=user.name,
email=user.email,
_links={
"self": {"href": f"{base_url}/api/users/{user.id}"},
"orders": {"href": f"{base_url}/api/users/{user.id}/orders"},
"update": {
"href": f"{base_url}/api/users/{user.id}",
"method": "PATCH"
},
"delete": {
"href": f"{base_url}/api/users/{user.id}",
"method": "DELETE"
}
}
)
GraphQL 设计模式
模式 1:Schema 设计
# schema.graphql
# Clear type definitions
type User {
id: ID!
email: String!
name: String!
createdAt: DateTime!
# Relationships
orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!
profile: UserProfile
}
type Order {
id: ID!
status: OrderStatus!
total: Money!
items: [OrderItem!]!
createdAt: DateTime!
# Back-reference
user: User!
}
# Pagination pattern (Relay-style)
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type OrderEdge {
node: Order!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Enums for type safety
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
# Custom scalars
scalar DateTime
scalar Money
# Query root
type Query {
user(id: ID!): User
users(first: Int = 20, after: String, search: String): UserConnection!
order(id: ID!): Order
}
# Mutation root
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
createOrder(input: CreateOrderInput!): CreateOrderPayload!
}
# Input types for mutations
input CreateUserInput {
email: String!
name: String!
password: String!
}
# Payload types for mutations
type CreateUserPayload {
user: User
errors: [Error!]
}
type Error {
field: String
message: String!
}
模式 2:解析器设计
from typing import Optional, List
from ariadne import QueryType, MutationType, ObjectType
from dataclasses import dataclass
query = QueryType()
mutation = MutationType()
user_type = ObjectType("User")
@query.field("user")
async def resolve_user(obj, info, id: str) -> Optional[dict]:
"""Resolve single user by ID."""
return await fetch_user_by_id(id)
@query.field("users")
async def resolve_users(
obj,
info,
first: int = 20,
after: Optional[str] = None,
search: Optional[str] = None
) -> dict:
"""Resolve paginated user list."""
# Decode cursor
offset = decode_cursor(after) if after else 0
# Fetch users
users = await fetch_users(
limit=first + 1, # Fetch one extra to check hasNextPage
offset=offset,
search=search
)
# Pagination
has_next = len(users) > first
if has_next:
users = users[:first]
edges = [
{
"node": user,
"cursor": encode_cursor(offset + i)
}
for i, user in enumerate(users)
]
return {
"edges": edges,
"pageInfo": {
"hasNextPage": has_next,
"hasPreviousPage": offset > 0,
"startCursor": edges[0]["cursor"] if edges else None,
"endCursor": edges[-1]["cursor"] if edges else None
},
"totalCount": await count_users(search=search)
}
@user_type.field("orders")
async def resolve_user_orders(user: dict, info, first: int = 20) -> dict:
"""Resolve user's orders (N+1 prevention with DataLoader)."""
# Use DataLoader to batch requests
loader = info.context["loaders"]["orders_by_user"]
orders = await loader.load(user["id"])
return paginate_orders(orders, first)
@mutation.field("createUser")
async def resolve_create_user(obj, info, input: dict) -> dict:
"""Create new user."""
try:
# Validate input
validate_user_input(input)
# Create user
user = await create_user(
email=input["email"],
name=input["name"],
password=hash_password(input["password"])
)
return {
"user": user,
"errors": []
}
except ValidationError as e:
return {
"user": None,
"errors": [{"field": e.field, "message": e.message}]
}
模式 3:DataLoader(防止 N+1 问题)
from aiodataloader import DataLoader
from typing import List, Optional
class UserLoader(DataLoader):
"""Batch load users by ID."""
async def batch_load_fn(self, user_ids: List[str]) -> List[Optional[dict]]:
"""Load multiple users in single query."""
users = await fetch_users_by_ids(user_ids)
# Map results back to input order
user_map = {user["id"]: user for user in users}
return [user_map.get(user_id) for user_id in user_ids]
class OrdersByUserLoader(DataLoader):
"""Batch load orders by user ID."""
async def batch_load_fn(self, user_ids: List[str]) -> List[List[dict]]:
"""Load orders for multiple users in single query."""
orders = await fetch_orders_by_user_ids(user_ids)
# Group orders by user_id
orders_by_user = {}
for order in orders:
user_id = order["user_id"]
if user_id not in orders_by_user:
orders_by_user[user_id] = []
orders_by_user[user_id].append(order)
# Return in input order
return [orders_by_user.get(user_id, []) for user_id in user_ids]
# Context setup
def create_context():
return {
"loaders": {
"user": UserLoader(),
"orders_by_user": OrdersByUserLoader()
}
}
最佳实践
REST API
- 命名一致:集合使用复数名词(使用
/users,而非/user) - 无状态:每个请求都包含所有必要信息
- 正确使用 HTTP 状态码:2xx 表示成功,4xx 表示客户端错误,5xx 表示服务端错误
- 为 API 设置版本:从一开始就为不兼容变更做好规划
- 分页:始终对大型集合进行分页
- 速率限制:通过速率限制保护 API
- 文档:使用 OpenAPI/Swagger 编写交互式文档
GraphQL API
- Schema 优先:先设计 Schema,再编写解析器
- 避免 N+1 问题:使用 DataLoader 高效获取数据
- 输入验证:在 Schema 和解析器层进行验证
- 错误处理:在变更载荷中返回结构化错误
- 分页:使用基于游标的分页(Relay 规范)
- 弃用:使用
@deprecated指令逐步迁移 - 监控:跟踪查询复杂度和执行时间
常见陷阱
- 过度获取/获取不足(REST):GraphQL 可以解决此问题,但需要 DataLoader
- 不兼容变更:对 API 进行版本管理或采用弃用策略
- 错误格式不一致:统一错误响应格式
- 缺少速率限制:不设限制的 API 容易遭到滥用
- 文档不完善:缺少文档会令开发者感到困扰
- 忽视 HTTP 语义:对幂等操作使用 POST 会违背预期
- 耦合过紧:API 结构不应照搬数据库 Schema
资源
- references/rest-best-practices.md:REST API 设计综合指南
- references/graphql-schema-design.md:GraphQL 模式的常见模式和反模式
- references/api-versioning-strategies.md:版本管理方法和迁移路径
- assets/rest-api-template.py:FastAPI REST API 模板
- assets/graphql-schema-template.graphql:完整的 GraphQL 模式示例
- assets/api-design-checklist.md:实施前审核清单
- scripts/openapi-generator.py:从代码生成 OpenAPI 规范
评论
支持 Markdown