mcpField
A
api-design-principles

api-design-principles

github.com/ovachiever開発5654 回インストール49 週間前に更新
リポジトリ

api-design-principles について

REST と GraphQL API の設計・レビューを支援するスキルです。バージョン管理、ページネーション、エラー処理、スキーマ設計のパターンを扱います。

主な機能

  1. 01REST リソースと HTTP メソッドのパターン
  2. 02GraphQL スキーマとリゾルバーの指針
  3. 03API バージョン管理の戦略
  4. 04ページネーションとエラー処理
  5. 05テンプレートとレビュー用チェックリスト

活用シーン

  1. 01REST または GraphQL API の新規設計
  2. 02実装前の API 仕様レビュー
  3. 03チーム内の API 標準策定
  4. 04使いやすさを高めるエンドポイントの改修

インストール

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

$npx skills add https://github.com/ovachiever/droid-tings --skill api-design-principles

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

assets/
api-design-checklist.md
rest-api-template.py
references/
graphql-schema-design.md
rest-best-practices.md

詳細

API 設計の原則

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

このスキルを使う場面

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

コアコンセプト

1. RESTful 設計の原則

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

  • リソースには動詞ではなく名詞(ユーザー、注文、商品など)を使います
  • 操作には 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 仕様を生成します

コメント