mcpField
A
api-design-principles

api-design-principles

github.com/boshi-xixixi開発28039 回インストール24 週間前に更新
リポジトリ

api-design-principles について

REST と GraphQL の API 設計原則、パターン、確認項目をまとめた Skill です。API の設計や仕様レビュー、ドキュメント作成に携わるチーム向けに、バージョン管理、ページネーション、エラー処理、GraphQL スキーマなどを扱います。

主な機能

  1. 01REST のリソースと HTTP メソッド設計
  2. 02GraphQL スキーマとリゾルバーパターン
  3. 03API バージョン管理戦略
  4. 04ページネーション、フィルタリング、エラー処理
  5. 05テンプレート、チェックリスト、OpenAPI 生成スクリプト

活用シーン

  1. 01REST または GraphQL API の新規設計
  2. 02API 仕様のレビューとチーム標準の策定
  3. 03API バージョン移行の計画
  4. 04開発者向け API ドキュメントの作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/boshi-xixixi/traeskill --skill api-design-principles

インストール後、Claude Code で/api-design-principlesを入力して読み込まれたか確認します。

同梱ファイルAGENTS.md

詳細

API 設計の原則

直感的でスケーラブル、保守しやすく、開発者に喜ばれ、長く使い続けられる API を構築するために、REST と GraphQL の API 設計原則を習得しましょう。

このスキルを使う場面

  • 新しい REST または GraphQL API を設計するとき
  • 既存の API を使いやすくするためにリファクタリングするとき
  • チームの API 設計標準を確立するとき
  • 実装前に API 仕様をレビューするとき
  • API パラダイム間を移行するとき(REST から GraphQL など)
  • 開発者にとって使いやすい API ドキュメントを作成するとき
  • 特定のユースケース(モバイル、サードパーティとの連携など)に合わせて API を最適化するとき

インタラクティブな設計プロセス

  • 要件を明確にする:設計を確定する前に、ドメインと制約を理解していることを確認します。
  • フィードバックループ:利用可能な場合は、mcp-feedback-enhanced ツール(例:ask_followup_question)を使って、確認の質問をしたり、レビュー用に設計案を提示したりします。利用できない場合は、通常のチャットを使います。

中核となる概念

1. RESTful 設計原則

リソース指向アーキテクチャ

  • リソースには動詞ではなく名詞を使います(users、orders、products など)
  • アクションには HTTP メソッドを使います(GET、POST、PUT、PATCH、DELETE)
  • URL はリソースの階層を表します
  • 命名規則に一貫性を持たせます

HTTP メソッドの意味:

  • GET:リソースを取得します(べき等、安全)
  • POST:新しいリソースを作成します
  • PUT:リソース全体を置き換えます(べき等)
  • PATCH:リソースの一部を更新します
  • DELETE:リソースを削除します(べき等)
2. GraphQL 設計原則

スキーマファースト開発

  • 型でドメインモデルを定義します
  • データの読み取りにはクエリを使います
  • データの変更にはミューテーションを使います
  • リアルタイム更新にはサブスクリプションを使います

クエリの構造:

  • クライアントは必要なデータだけをリクエストします
  • 単一のエンドポイントで複数の操作を行います
  • 厳密に型付けされたスキーマを使います
  • イントロスペクションが組み込まれています
3. API のバージョン管理戦略

URL によるバージョン管理:

/api/v1/users
/api/v2/users

ヘッダーによるバージョン管理:

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.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. 一貫した命名:コレクションには複数形の名詞を使います(/user ではなく /users)
  2. ステートレス:各リクエストに必要な情報をすべて含めます
  3. HTTP ステータスコードを正しく使う:2xx は成功、4xx はクライアントエラー、5xx はサーバーエラーです
  4. API のバージョン管理:最初から破壊的変更に備えます
  5. ページネーション:大規模なコレクションには必ずページネーションを適用します
  6. レート制限:レート制限で API を保護します
  7. ドキュメント:インタラクティブなドキュメントには OpenAPI/Swagger を使います
GraphQL API
  1. スキーマファースト:リゾルバーを書く前にスキーマを設計します
  2. N+1 を回避する:効率的なデータ取得には DataLoader を使います
  3. 入力値の検証:スキーマとリゾルバーの両方のレベルで検証します
  4. エラー処理:ミューテーションのペイロードで構造化されたエラーを返します
  5. ページネーション:カーソルベースのページネーション(Relay 仕様)を使います
  6. 非推奨化:段階的な移行には @deprecated ディレクティブを使います
  7. モニタリング:クエリの複雑さと実行時間を追跡します

よくある落とし穴

  • データの過剰取得/不足取得(REST):GraphQL では解消されますが、DataLoader が必要です
  • 破壊的変更:API のバージョン管理、または非推奨化の戦略を使います
  • エラー形式の不統一:エラーレスポンスを標準化します
  • レート制限の欠如:制限のない API は悪用される危険があります
  • ドキュメントの不備:ドキュメントのない API は開発者を困らせます
  • HTTP のセマンティクスを無視する:べき等な操作に POST を使うと、想定に反する動作になります
  • 密結合:API の構造をデータベーススキーマに合わせるべきではありません

リソース

  • 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 仕様を生成します

コメント