mcpField
A
api-design-principles

api-design-principles

github.com/boshi-xixixi开发28039 次安装24 周前更新
代码仓库

关于 api-design-principles

此 Skill 汇集 REST 与 GraphQL API 的设计原则、模式和检查要点,面向 API 设计、评审及文档编写工作。内容涵盖版本管理、分页、错误处理和 GraphQL Schema 等实践。

主要功能

  1. 01REST 资源与 HTTP 方法设计
  2. 02GraphQL Schema 与解析器模式
  3. 03API 版本管理策略
  4. 04分页、过滤与错误处理
  5. 05提供模板、检查清单和 OpenAPI 生成脚本

适用场景

  1. 01设计新的 REST 或 GraphQL API
  2. 02评审 API 规范并制定团队标准
  3. 03规划 API 版本迁移
  4. 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
  1. 命名一致:集合使用复数名词(使用 /users,而非 /user)
  2. 无状态:每个请求都包含所有必要信息
  3. 正确使用 HTTP 状态码:2xx 表示成功,4xx 表示客户端错误,5xx 表示服务端错误
  4. 为 API 设置版本:从一开始就为不兼容变更做好规划
  5. 分页:始终对大型集合进行分页
  6. 速率限制:通过速率限制保护 API
  7. 文档:使用 OpenAPI/Swagger 编写交互式文档
GraphQL API
  1. Schema 优先:先设计 Schema,再编写解析器
  2. 避免 N+1 问题:使用 DataLoader 高效获取数据
  3. 输入验证:在 Schema 和解析器层进行验证
  4. 错误处理:在变更载荷中返回结构化错误
  5. 分页:使用基于游标的分页(Relay 规范)
  6. 弃用:使用 @deprecated 指令逐步迁移
  7. 监控:跟踪查询复杂度和执行时间

常见陷阱

  • 过度获取/获取不足(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