mcpField
A
api-design-principles

api-design-principles

github.com/hermeticormus開発12477 回インストール5 週間前に更新
リポジトリ

api-design-principles について

api-design-principlesは、RESTおよびGraphQL APIの設計・レビューを行うチーム向けの学習・参照スキルです。リソースやスキーマのパターン、バージョン管理、ページネーション、エラー処理、ドキュメントの指針を紹介します。

主な機能

  1. 01RESTリソースとHTTPメソッドの設計
  2. 02GraphQLスキーマとリゾルバーのパターン
  3. 03APIバージョン管理の手法
  4. 04ページネーション、フィルタリング、エラー処理
  5. 05N+1問題に対するDataLoaderの指針
  6. 06設計チェックリストとテンプレート

活用シーン

  1. 01新しいRESTまたはGraphQL APIの計画
  2. 02実装前のAPI仕様レビュー
  3. 03一貫した規約に向けたエンドポイントの改修
  4. 04チームのAPI設計基準の策定

インストール

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

$npx skills add https://github.com/hermeticormus/libreuiux-claude-code --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 仕様を生成します

コメント