コンテンツにスキップ

依存性注入

FastAPI 風の型安全な依存性注入システムです。クエリパラメータ、パスパラメータ、リクエストボディ、認証情報を自動的に解析・変換・検証します。

🎯 メリット

  • 型安全: 型ヒントにより IDE の補完とエラー検出
  • 自動バリデーション: 不正な値は自動的にエラーレスポンス
  • コード削減: 手動のパラメータ処理が不要
  • テスト容易性: 関数の引数として直接テスト値を渡せる

📖 基本的な使用方法

Query(クエリパラメータ)

from lambapi import API, Query

@app.get("/search")
def search(
    q: str = Query(..., description="検索クエリ"),
    limit: int = Query(10, ge=1, le=100, description="結果数"),
    offset: int = Query(0, ge=0, description="オフセット")
):
    return {"query": q, "limit": limit, "offset": offset}

# GET /search?q=python&limit=20&offset=10

Path(パスパラメータ)

from lambapi import API, Path

@app.get("/users/{user_id}/posts/{post_id}")
def get_user_post(
    user_id: str = Path(..., description="ユーザー ID"),
    post_id: int = Path(..., gt=0, description="投稿 ID")
):
    return {"user_id": user_id, "post_id": post_id}

# GET /users/alice/posts/123

Body(リクエストボディ)

from lambapi import API, Body
from pydantic import BaseModel

class CreateUserRequest(BaseModel):
    name: str
    email: str
    age: Optional[int] = None

@app.post("/users")
def create_user(
    user_data: CreateUserRequest = Body(..., description="ユーザー作成データ")
):
    return {"message": f"ユーザー {user_data.name} を作成しました"}

# POST /users {"name": "Alice", "email": "alice@example.com"}

Authenticated(認証ユーザー)

from lambapi import API, Authenticated
from lambapi.auth import DynamoDBAuth

@app.get("/profile")
@auth.require_role("user")
def get_profile(
    user: User = Authenticated(..., description="認証されたユーザー")
):
    return {"user_id": user.id, "name": user.name}

🔧 バリデーション機能

数値バリデーション

@app.get("/products")
def get_products(
    min_price: float = Query(0, ge=0, description="最小価格"),
    max_price: float = Query(1000000, le=1000000, description="最大価格"), 
    rating: int = Query(5, gt=0, le=5, description="評価")
):
    return {"min_price": min_price, "max_price": max_price, "rating": rating}
パラメータ 説明
ge 以上(greater equal)
gt より大きい(greater than)
le 以下(less equal)
lt より小さい(less than)

文字列バリデーション

@app.get("/search")
def search(
    query: str = Query(..., min_length=1, max_length=100, description="検索クエリ"),
    category: str = Query("all", regex=r"^(books|electronics|clothing|all)$")
):
    return {"query": query, "category": category}
パラメータ 説明
min_length 最小文字数
max_length 最大文字数
regex 正規表現パターン

配列・リストのバリデーション

from typing import List

@app.post("/tags")
def create_tags(
    tags: List[str] = Query([], description="タグリスト"),
    ids: List[int] = Query([], description="ID リスト")
):
    return {"tags": tags, "ids": ids}

# GET /tags?tags=python&tags=web&ids=1&ids=2&ids=3

🔄 複合的な使用例

全ての依存性注入を組み合わせた例:

from lambapi import API, Query, Path, Body, Authenticated
from pydantic import BaseModel
from typing import Optional

class UpdatePostRequest(BaseModel):
    title: str
    content: str
    published: bool = False

@app.put("/users/{user_id}/posts/{post_id}")
@auth.require_role(["admin", "moderator"])
def update_user_post(
    # 認証ユーザー
    admin: User = Authenticated(..., description="管理者"),

    # パスパラメータ
    user_id: str = Path(..., description="対象ユーザー ID"),
    post_id: int = Path(..., gt=0, description="投稿 ID"),

    # クエリパラメータ  
    notify: bool = Query(True, description="通知を送信するか"),

    # リクエストボディ
    post_data: UpdatePostRequest = Body(..., description="投稿更新データ")
):
    return {
        "message": f"管理者 {admin.id} が投稿 {post_id} を更新しました",
        "user_id": user_id,
        "title": post_data.title,
        "notification_sent": notify
    }

⚡ パフォーマンス特性

関数シグネチャキャッシュ

# 関数シグネチャは起動時にキャッシュされる
@app.get("/cached")
def cached_handler(
    param1: str = Query(...),
    param2: int = Query(default=10)
):
    return {"param1": param1, "param2": param2}

型変換キャッシュ

# 型変換関数もキャッシュされ高速処理
@app.get("/converted")
def converted_handler(
    user_id: int = Path(...),  # str → int 変換
    active: bool = Query(True)  # str → bool 変換
):
    return {"user_id": user_id, "active": active}

🚨 エラーハンドリング

自動バリデーションエラー

# 不正なリクエスト例
# GET /products?limit=200  (limit は 1-100 の範囲)
# → 400 Bad Request: {"error": "validation_error", "message": "limit は 100 以下である必要があります"}

# POST /users  (body が空)
# → 400 Bad Request: {"error": "validation_error", "message": "必須フィールド 'name' が不足しています"}

# GET /users/abc/posts/0  (post_id は 1以上)
# → 400 Bad Request: {"error": "validation_error", "message": "post_id は 0 より大きい値である必要があります"}

カスタムバリデーション

from lambapi import ValidationError

@app.post("/custom-validation")
def custom_validation(data: dict = Body(...)):
    if data.get("age", 0) < 0:
        raise ValidationError("年齢は0以上である必要があります")

    return {"message": "バリデーション成功"}

🔄 従来システムとの互換性

新しい依存性注入と従来の方式を混在できます:

# 従来の書き方(引き続きサポート)
@app.get("/legacy")
def legacy_handler(request):
    query_param = request.query_params.get("q", "default")
    return {"query": query_param}

# 新しい依存性注入
@app.get("/modern")
def modern_handler(q: str = Query("default", description="クエリパラメータ")):
    return {"query": q}

📚 実用的な例

商品検索 API

@app.get("/products/search")
def search_products(
    # 検索条件
    q: str = Query(..., min_length=1, max_length=100, description="検索キーワード"),
    category: str = Query("all", description="商品カテゴリ"),

    # 価格フィルタ
    min_price: float = Query(0, ge=0, description="最低価格"),
    max_price: float = Query(999999, ge=0, description="最高価格"),

    # ソート・ページング
    sort_by: str = Query("relevance", regex=r"^(relevance|price|rating|newest)$"),
    page: int = Query(1, ge=1, description="ページ番号"),
    per_page: int = Query(20, ge=1, le=100, description="1ページあたりの件数")
):
    return {
        "query": q,
        "filters": {"category": category, "price_range": [min_price, max_price]},
        "sort": sort_by,
        "pagination": {"page": page, "per_page": per_page},
        "products": []  # 実際の商品データ
    }

ユーザー管理 API

from pydantic import BaseModel, EmailStr
from typing import Optional

class UserCreateRequest(BaseModel):
    username: str
    email: EmailStr
    full_name: str
    age: Optional[int] = None

class UserUpdateRequest(BaseModel):
    full_name: Optional[str] = None
    age: Optional[int] = None

@app.post("/users")
def create_user(user_data: UserCreateRequest = Body(...)):
    return {
        "message": "ユーザーを作成しました",
        "user": {
            "username": user_data.username,
            "email": user_data.email,
            "full_name": user_data.full_name,
            "age": user_data.age
        }
    }

@app.patch("/users/{user_id}")
def update_user(
    user_id: str = Path(..., description="ユーザー ID"),
    user_data: UserUpdateRequest = Body(...),
    notify: bool = Query(True, description="更新通知を送るか")
):
    updates = {}
    if user_data.full_name is not None:
        updates["full_name"] = user_data.full_name
    if user_data.age is not None:
        updates["age"] = user_data.age

    return {
        "message": "ユーザー情報を更新しました",
        "user_id": user_id,
        "updates": updates,
        "notification_sent": notify
    }

🔧 デバッグとテスト

テスト用の関数呼び出し

# 依存性注入を使った関数は直接テストできる
def test_search():
    result = search_products(
        q="laptop",
        category="electronics", 
        min_price=500,
        max_price=2000,
        sort_by="price",
        page=1,
        per_page=10
    )
    assert result["query"] == "laptop"
    assert result["filters"]["category"] == "electronics"

デバッグ用パラメータ出力

@app.get("/debug")
def debug_params(
    param1: str = Query("default"),
    param2: int = Query(0),
    param3: bool = Query(False)
):
    return {
        "received_params": {
            "param1": {"value": param1, "type": type(param1).__name__},
            "param2": {"value": param2, "type": type(param2).__name__},
            "param3": {"value": param3, "type": type(param3).__name__}
        }
    }