404 Not Found

404 Not Found


nginx

フェーズ2総合演習 — PriceTrackerコア機能のクローズドループ実装

フェーズ2は自動車の最終組立のようなものです。エンジン (データベース), ステアリングホイール (ルーティング), シートベルト (認証), ダッシュボード (ミドルウェア)がすべて完成し, 今やそれらを組み立てて完成した車にします。

1. 学ぶ内容


2. Aliceのリアルストーリー

(1) ペインポイント:機能が散在し統合テストができない

Aliceはミドルウェア, 依存性注入, データベース, CRUD, JWT認証を個別に実装しましたが, 各機能は独立してテストされていました。すべての機能を統合しようとしたとき, CORSミドルウェアとJWT認証の実行順序が競合し, DIチェーンでDBセッションが2回作成され, Proユーザーのレート制限ロジックと権限チェックが互いに上書きし合うことが判明しました。

(2) 体系的な統合アプローチ

フェーズ2の総合演習では, すべてのモジュールを正しい階層と順序で統合します:CORS (最外層)→ レート制限 → 認証 → ビジネスロジック → データベース (最内層)。各層の責任が明確に定義され, 他の層から独立して動作します。

(3) 収益

統合されたPriceTrackerコアサービスが完全に稼働しました。Bobのフロントエンドがクロスドメインリソースに正常にアクセスし, すべての保護されたエンドポイントがJWTトークンを要求し, Proユーザーが一括インポートに問題なく対応し, Freeユーザーが正しくレート制限されています。


3. フェーズ2のアーキテクチャ概要

(1) 完全なリクエストフロー

100%
flowchart TD
    Client[Bob Frontend] --> CORS[CORS Middleware]
    CORS --> RateLimit[Rate Limit Middleware]
    RateLimit --> Router[FastAPI Router]
    Router --> AuthDI{Auth Required?}
    AuthDI -->|Yes| GetDB[get_db Dependency]
    AuthDI -->|No| Handler[Handler]
    GetDB --> GetUser[get_current_user]
    GetUser --> CheckSub[require_subscription]
    CheckSub --> Handler
    Handler --> Repo[Repository Layer]
    Repo --> DB[(PostgreSQL)]
    
    RateLimit -.->|429| Reject[Rate Limited]
    GetUser -.->|401| Unauth[Unauthorized]
    CheckSub -.->|403| Forbid[Forbidden]

(2) ルーティンググループポリシー

ルーティンググループ プレフィックス 認証 説明
public / なし health, docs
api_v1 /api/v1 JWT必須 すべてのビジネスエンドポイント
admin /api/v1/admin JWT + Admin 管理エンドポイント

4. 完全なコードの統合

(1) アプリケーションエントリポイントとミドルウェア設定

(1) ▶サンプル:main.pyの完全な設定

PYTHON
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

from app.api.routes import products, prices, auth
from app.core.config import settings

app = FastAPI(
    title=settings.app_name,
    version="1.0.0",
    description="PriceTracker SaaS API - E-commerce price tracking service",
)

# ミドルウェアの順序:最後に登録されたものが最内層 (リクエスト時に最初に実行)
# CORSは最外層にする (最後に登録)
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ルーターを含める - 最初にパブリック, 次に保護済み
app.include_router(auth.router, tags=["auth"])
app.include_router(
    products.router,
    prefix="/api/v1",
    dependencies=[Depends(get_current_user)],
    tags=["products"],
)
app.include_router(
    prices.router,
    prefix="/api/v1",
    dependencies=[Depends(get_current_user)],
    tags=["prices"],
)

@app.get("/health")
async def health_check():
    return {"status": "healthy", "service": "pricetracker"}

出力:

TEXT
# 関数定義成功

(2) 3層DIチェーンの完全な実装

(2) ▶サンプル:app/core/deps.py

PYTHON
from fastapi import Depends, HTTPException, Header
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from jose import jwt, JWTError

from app.db import async_session
from app.core.config import settings
from app.core.security import verify_password
from app.models import User
from sqlalchemy import select

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")

# レベル1:データベースセッション
async def get_db():
    async with async_session() as session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise

# レベル2:現在のユーザー
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
):
    credentials_exception = HTTPException(
        status_code=401,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(
            token, settings.secret_key, algorithms=[settings.algorithm]
        )
        username = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    stmt = select(User).where(User.email == username)
    result = await db.execute(stmt)
    user = result.scalar_one_or_none()
    if user is None:
        raise credentials_exception
    return user

# レベル3:サブスクリプション確認
def require_subscription(min_level: str = "free"):
    LEVELS = {"free": 0, "pro": 1, "enterprise": 2}

    async def check(user: User = Depends(get_current_user)):
        if LEVELS.get(user.subscription, 0) < LEVELS.get(min_level, 0):
            raise HTTPException(
                status_code=403,
                detail=f"Requires {min_level} plan. Current: {user.subscription}",
            )
        return user
    return check

出力:

TEXT
# 関数定義成功

(3) 権限検証プロセス

100%
flowchart LR
    Request[Request with Token] --> ParseToken[Parse JWT Token]
    ParseToken --> ValidToken{Token Valid?}
    ValidToken -->|No| Return401[Return 401]
    ValidToken -->|Yes| QueryUser[Query User from DB]
    QueryUser --> UserExists{User Found?}
    UserExists -->|No| Return401
    UserExists -->|Yes| CheckSub{Subscription Level?}
    CheckSub -->|Free| AllowBasic[Allow Basic Endpoints]
    CheckSub -->|Pro| AllowPro[Allow Pro Endpoints]
    CheckSub -->|Enterprise| AllowAll[Allow All Endpoints]

(3) ▶サンプル:保護されたプロダクトルーティング

PYTHON
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional

from app.core.deps import get_db, get_current_user, require_subscription
from app.schemas import ProductCreate, ProductResponse, ProductUpdate
from app.services import ProductService

router = APIRouter()

@router.get("/products", response_model=list[ProductResponse])
async def list_products(
    category: Optional[str] = Query(None),
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
    user=Depends(get_current_user),
):
    service = ProductService(db)
    return await service.list_products(category=category, skip=skip, limit=limit)

@router.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(
    product: ProductCreate,
    db: AsyncSession = Depends(get_db),
    user=Depends(get_current_user),
):
    service = ProductService(db)
    return await service.create_product(product, user_id=user.id)

@router.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(
    product_id: int = Path(gt=0),
    db: AsyncSession = Depends(get_db),
    user=Depends(get_current_user),
):
    service = ProductService(db)
    product = await service.get_product(product_id)
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    return product

出力:

TEXT
# 関数定義成功

(4) ▶サンプル:一括インポートエンドポイント (サブスクリプション制限付き)

PYTHON
@router.post("/products/{product_id}/prices/bulk")
async def bulk_import_prices(
    product_id: int = Path(gt=0),
    prices: list[PriceCreate] = ...,
    db: AsyncSession = Depends(get_db),
    user=Depends(require_subscription("free")),  # Freeユーザーでもインポート可能
):
    # Freeプラン:1回のインポートは最大1000件
    limits = {"free": 1000, "pro": 100000, "enterprise": 1000000}
    max_import = limits.get(user.subscription, 1000)
    
    if len(prices) > max_import:
        raise HTTPException(
            status_code=403,
            detail=f"Import limit: {max_import} for {user.subscription} plan. Upgrade at /pricing",
        )
    
    service = PriceService(db)
    return await service.bulk_import_prices(product_id, prices)

出力:

TEXT
# 関数定義成功

❓ よくある質問

Q CORSはミドルウェアの順序でなぜ最後に登録されるのですか?
A CORSはすべてのレスポンス (4xxエラーを含む)にCORSヘッダーを追加する必要があります。CORSが最外層でない場合, レート制限ミドルウェアが返す429レスポンスにCORSヘッダーが欠落し, フロントエンドがエラーメッセージを読み取れなくなる可能性があります。
Q ルーティンググループの依存性とエンドポイントの依存性は複数回実行されますか?
A いいえ。FastAPIはリクエストレベルで同じ依存性をキャッシュします。ルーティンググループのDepends(get_current_user)とエンドポイントのDepends(get_current_user)は同じ実行結果を共有します。
Q DIチェーンでget_dbが複数回宣言されると, 複数の接続が作られますか?
A いいえ。get_dbはリクエストごとに1回だけ実行され, すべての依存性が同じAsyncSessionを共有します。
Q FreeユーザーはProにアップグレードするにはどうすればよいですか?
A Stripe決済の統合 (本レッスンの範囲外)が必要です。ここではUser.subscriptionフィールドでシミュレーションしています。本番環境では決済コールバックで更新されます。
Q リクエストフロー全体をテストするにはどうすればよいですか?
A TestClient + dependency_overridesget_dbを置き換え, 段階的にテストします:トークンなし→401, Freeトークン→200 (ただし制限あり), Proトークン→フル権限。
Q フェーズ2完了後の次は何ですか?
A フェーズ3では高度な機能を紹介します。WebSocketリアルタイムプッシュ, Celery非同期タスク, Redisキャッシュ, ファイルアップロード, pytestテスト, OpenAPIカスタマイズです。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):フェーズ1のインメモリCRUD実装を非同期SQLAlchemy実装に置き換え, すべてのエンドポイントがDepends(get_db)でデータベースセッションを注入するようにします。ヒント:Repositoryパターン + AsyncSession
  2. 応用問題 (難易度 ⭐⭐):ルーティンググループを実装します。「public」グループ (/health, /auth/login)は認証不要, 「api/v1」下のすべてのビジネスエンドポイントはJWTで保護します。Swagger UIでログインと保護されたエンドポイントへのアクセスをテストします。ヒント:APIRouter(dependencies=[Depends(get_current_user)])
  3. チャレンジ (難易度 ⭐⭐⭐):完全な3層DIチェーン + 一括インポートエンドポイントを実装します。Proユーザーは100,000件の価格データをインポートでき, Freeユーザーは1,000件を超えると403エラーを受け取ります。Swagger UIで登録→ログイン→インポートのプロセスを完全に実演します。ヒント:require_subscription("free") + limits辞書

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%