api-design-principles について
REST と GraphQL の API 設計原則、パターン、確認項目をまとめた Skill です。API の設計や仕様レビュー、ドキュメント作成に携わるチーム向けに、バージョン管理、ページネーション、エラー処理、GraphQL スキーマなどを扱います。
主な機能
- 01REST のリソースと HTTP メソッド設計
- 02GraphQL スキーマとリゾルバーパターン
- 03API バージョン管理戦略
- 04ページネーション、フィルタリング、エラー処理
- 05テンプレート、チェックリスト、OpenAPI 生成スクリプト
活用シーン
- 01REST または GraphQL API の新規設計
- 02API 仕様のレビューとチーム標準の策定
- 03API バージョン移行の計画
- 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
- 一貫した命名:コレクションには複数形の名詞を使います(
/userではなく/users) - ステートレス:各リクエストに必要な情報をすべて含めます
- HTTP ステータスコードを正しく使う:2xx は成功、4xx はクライアントエラー、5xx はサーバーエラーです
- API のバージョン管理:最初から破壊的変更に備えます
- ページネーション:大規模なコレクションには必ずページネーションを適用します
- レート制限:レート制限で API を保護します
- ドキュメント:インタラクティブなドキュメントには OpenAPI/Swagger を使います
GraphQL API
- スキーマファースト:リゾルバーを書く前にスキーマを設計します
- N+1 を回避する:効率的なデータ取得には DataLoader を使います
- 入力値の検証:スキーマとリゾルバーの両方のレベルで検証します
- エラー処理:ミューテーションのペイロードで構造化されたエラーを返します
- ページネーション:カーソルベースのページネーション(Relay 仕様)を使います
- 非推奨化:段階的な移行には
@deprecatedディレクティブを使います - モニタリング:クエリの複雑さと実行時間を追跡します
よくある落とし穴
- データの過剰取得/不足取得(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 仕様を生成します
コメント
Markdown 対応