api-design-principles について
REST と GraphQL API の設計・レビューを支援するスキルです。バージョン管理、ページネーション、エラー処理、スキーマ設計のパターンを扱います。
主な機能
- 01REST リソースと HTTP メソッドのパターン
- 02GraphQL スキーマとリゾルバーの指針
- 03API バージョン管理の戦略
- 04ページネーションとエラー処理
- 05テンプレートとレビュー用チェックリスト
活用シーン
- 01REST または GraphQL API の新規設計
- 02実装前の API 仕様レビュー
- 03チーム内の API 標準策定
- 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.mdrest-api-template.pyreferences/graphql-schema-design.mdrest-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
- 一貫した命名:コレクションには複数形の名詞を使います(
/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 対応