404 Not Found

404 Not Found


nginx

プロジェクト開発 — PriceTracker:青写真からコードへ

プロジェクトの開発は家を建てるようなもの - まず基礎 (インフラ)を築き, 次に骨組み (認証 + 製品)を立て, それから瓦やレンガ (価格 + 通知)を積み, 最後に内装 (レポート + エラー処理)を仕上げます。順序を間違えると, 手戻りのコストは倍増します。

1. 学ぶ内容


2. Aliceの実話

(1) 悩み:開発順序の混乱による手戻り

Aliceは最初に価格インポート機能を開発しましたが, 後に認証と権限チェックが必要だと気づき, 15のエンドポイントを遡って修正しました。その後, エラーレスポンスの形式が不統一であることが発覚し (一部は{"error": "..."}, 他は{"detail": "..."}を返す), Bobはフロントエンドで2セットの解析ロジックを書くはめになりました。開発順序の混乱により, 30%の時間が手戻りに費やされました。

(2) モジュール開発による解決策

依存関係に基づいて開発順序を決定します:インフラ (DB/Redis/設定)→ 認証 (JWT/権限)→ 製品 (CRUD)→ 価格 (インポート/プッシュ)→ 通知 → レポート。各モジュールが完成しテストに合格してから次に進み, 絶対に手戻りはしません。

(3) 成果

開発の手戻り率は30%から5%に低下し, エラーレスポンス形式が標準化され (すべてのエラーが{"error": {"code": "...", "message": "..."}}を返す), Bobのフロントエンドは1セットの解析ロジックだけで済むようになりました。


3. モジュール開発の順序

(1) 依存関係グラフ

100%
graph TD
    Infra[インフラ: DB/Redis/Config] --> Auth[Auth: JWT/Permissions]
    Auth --> Products[Products: CRUD]
    Products --> Prices[Prices: Import/Push]
    Auth --> Subscriptions[Subscriptions: Plans]
    Prices --> Notifications[Notifications: Alerts]
    Subscriptions --> Notifications
    Products --> Reports[Reports: Analytics]
    Prices --> Reports
順序 モジュール 依存関係 推定工数
1 インフラ なし 1日
2 認証 インフラ 1日
3 製品CRUD 認証 1日
4 価格インポート/照会 製品 1日
5 サブスクリプションプラン 認証 0.5日
6 通知・アラート 価格 + サブスクリプション 1日
7 レポート統計 製品 + 価格 0.5日

4. エラー処理システム

(1) カスタム例外クラス

(1) ▶ サンプル:PriceTracker例外システム

PYTHON
# app/core/exceptions.py
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

class PriceTrackerError(Exception):
    """PriceTrackerの基本例外"""
    def __init__(self, code: str, message: str, status_code: int = 500):
        self.code = code
        self.message = message
        self.status_code = status_code

class NotFoundError(PriceTrackerError):
    def __init__(self, resource: str, resource_id: int | str):
        super().__init__(
            code=f"{resource.upper()}_NOT_FOUND",
            message=f"{resource} with id '{resource_id}' not found",
            status_code=404,
        )

class SubscriptionRequiredError(PriceTrackerError):
    def __init__(self, required_plan: str, current_plan: str):
        super().__init__(
            code="SUBSCRIPTION_REQUIRED",
            message=f"This feature requires {required_plan} plan. Current: {current_plan}",
            status_code=403,
        )

class ImportLimitError(PriceTrackerError):
    def __init__(self, limit: int, plan: str):
        super().__init__(
            code="IMPORT_LIMIT_EXCEEDED",
            message=f"Import limit: {limit} records for {plan} plan",
            status_code=403,
        )

出力:

TEXT
# 関数定義成功

(2) グローバル例外ハンドラー

100%
flowchart TD
    A[例外発生] --> B{例外タイプ?}
    B -->|PriceTrackerError| C[標準レスポンスをフォーマット]
    B -->|RequestValidationError| D[バリデーションレスポンスをフォーマット]
    B -->|HTTPException| E[HTTPレスポンスをフォーマット]
    B -->|その他| F[500レスポンスをフォーマット]
    
    C --> G[JSONResponse: code + message]
    D --> G
    E --> G
    F --> G

(2) ▶ サンプル:グローバル例外ハンドラーの登録

PYTHON
# app/core/exception_handlers.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from app.core.exceptions import PriceTrackerError

def format_error_response(code: str, message: str, status_code: int, details=None):
    return JSONResponse(
        status_code=status_code,
        content={
            "error": {
                "code": code,
                "message": message,
                "details": details,
            }
        },
    )

async def pricetracker_error_handler(request: Request, exc: PriceTrackerError):
    return format_error_response(exc.code, exc.message, exc.status_code)

async def validation_error_handler(request: Request, exc: RequestValidationError):
    return format_error_response(
        code="VALIDATION_ERROR",
        message="Request validation failed",
        status_code=422,
        details=exc.errors(),
    )

async def http_error_handler(request: Request, exc: HTTPException):
    return format_error_response(
        code="HTTP_ERROR",
        message=str(exc.detail),
        status_code=exc.status_code,
    )

# ハンドラーの登録
def register_exception_handlers(app: FastAPI):
    app.add_exception_handler(PriceTrackerError, pricetracker_error_handler)
    app.add_exception_handler(RequestValidationError, validation_error_handler)
    app.add_exception_handler(HTTPException, http_error_handler)

出力:

TEXT
# 関数定義成功

5. 非同期ファーストの原則

(1) フルパス非同期チェックリスト

レベル 同期 ❌ 非同期 ✅
ルーティング def get_xxx() async def get_xxx()
データベース Session + session.execute() AsyncSession + await session.execute()
HTTPクライアント requests.get() httpx.AsyncClient().get()
Redis redis.Redis() redis.asyncio.Redis()
ファイルI/O open() + read() aiofiles.open() + await read()
タスク 即時実行 Celery非同期タスク

(1) ▶ サンプル:完全非同期エンドポイントの実装

PYTHON
# すべての層が非同期
@app.get("/api/v1/products/{product_id}", response_model=ProductDetailResponse)
async def get_product_detail(
    product_id: int = Path(gt=0),
    db: AsyncSession = Depends(get_db),      # 非同期DB
    redis: Redis = Depends(get_redis),         # 非同期Redis
    user: User = Depends(get_current_user),     # 非同期認証
):
    # 1. キャッシュを確認 (非同期)
    cached = await redis.get(f"product:{product_id}")
    if cached:
        return json.loads(cached)

    # 2. Eager loadingでDBを照会 (非同期)
    stmt = (
        select(Product)
        .options(selectinload(Product.prices))
        .where(Product.id == product_id)
    )
    result = await db.execute(stmt)
    product = result.scalar_one_or_none()

    if not product:
        raise NotFoundError("product", product_id)

    # 3. 結果をキャッシュ (非同期)
    data = ProductDetailResponse.model_validate(product).model_dump()
    await redis.setex(f"product:{product_id}", 300, json.dumps(data))

    return data

出力:

TEXT
# 関数定義成功

6. コード品質保証

(1) ツールチェーン設定

(1) ▶ サンプル:pyproject.toml品質ツール設定

TOML
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM"]

[tool.mypy]
python_version = "3.12"
strict = true
plugins = ["pydantic.mypy"]

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

出力:

TEXT
CI/CDパイプライン設定を読み込みました
パイプラインステータス:合格
テスト:12合格, 0不合格

(2) ▶ サンプル:pre-commit設定

YAML
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        additional_dependencies: [pydantic, sqlalchemy, fastapi]

出力:

TEXT
CI/CDパイプラインを読み込みました
パイプラインステータス:合格
テスト:12合格, 0不合格

(2) 品質ツールの比較

ツール 目的 実行タイミング
ruff lint + フォーマット pre-commit + CI
mypy 型チェック pre-commit + CI
pytest テスト CI + 手動
safety 依存関係セキュリティスキャン CI
bandit コードセキュリティチェック CI

❓ よくある質問

Q 開発順序は本当に重要ですか?
A 非常に重要です。依存されるモジュールを先に開発し, 後から開発するモジュールは完成した機能を直接利用できるため, 手戻りを回避できます。インフラ → 認証 → ビジネスロジックは黄金則です。
Q カスタム例外とHTTPExceptionはどう使い分けますか?
A ビジネスエラーにはカスタム例外 (codeとmessageを含む)を使用し, HTTPレベルのエラーにはHTTPExceptionを使用します。カスタム例外はグローバルハンドラーで統一的にフォーマットされます。
Q pre-commitは遅すぎませんか?
A Ruffは非常に高速 (Rust実装)で, Mypyはやや遅いですがインクリメンタルチェックが高速です。合計5秒未満で, コミットのたびにコード品質が保証されるため, CI失敗後に修正するよりはるかに効率的です。
Q mypyのstrictモードは価値がありますか?
A はい, 価値があります。strictモードはより多くの型エラーを検出します。初期設定の労力は高いですが, 長期的にはランタイムバグを減らせます。FastAPI + Pydanticの型ヒントはstrictモードと自然に相性が良いです。
Q 「100万アイテム + 数千の同時リクエスト」はどう検証しますか?
A Locustやk6で負荷テストを実行します。まず100万のテストレコードを準備し (INSERT INTO products SELECT generate_series(1, 1000000), ...), その後数千の同時クエリをシミュレートします。
Q コードレビューのプロセスはどう設計すべきですか?
A Bob/AliceがPRを提出 → CIが自動的にlintとテストを実行 → Charlieがコードレビュー → developにマージ → stagingに自動デプロイ → 検証後, mainにマージ。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):PriceTrackerのカスタム例外階層 (PriceTrackerError → NotFoundError → SubscriptionRequiredError)を実装し, エンドポイントでraise HTTPException(404)raise NotFoundError("product", 42)に置き換えてください。ヒント:Exceptionを継承
  2. 応用問題 (難易度 ⭐⭐):グローバル例外ハンドラーを実装し, PriceTrackerError, RequestValidationError, HTTPExceptionの3つのハンドラーを登録して, すべてのエラーレスポンスが統一形式に従うことを確認してください。ヒント:app.add_exception_handler() + format_error_response()
  3. チャレンジ (難易度 ⭐⭐⭐):完全なコード品質ツールチェーンを構築 - pyproject.toml (ruff, mypy, pytest設定), .pre-commit-config.yaml - を設定し, ruff check + mypy app/ + pytestを実行してすべてのテストが合格することを確認してください。ヒント:uv add --dev ruff mypy pytest + pre-commit install

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%