プロジェクト開発 — PriceTracker:青写真からコードへ
プロジェクトの開発は家を建てるようなもの - まず基礎 (インフラ)を築き, 次に骨組み (認証 + 製品)を立て, それから瓦やレンガ (価格 + 通知)を積み, 最後に内装 (レポート + エラー処理)を仕上げます。順序を間違えると, 手戻りのコストは倍増します。
1. 学ぶ内容
- モジュール開発の順序:インフラ → 認証 → 製品 → 価格 → 通知 → レポート
- 非同期ファーストの原則:ルーティングからデータベースまで, エンドツーエンドのasync/await
- エラー処理システム:カスタム例外クラス + グローバル例外ハンドラー + 標準化されたエラーレスポンス形式
- コード品質保証:pre-commitフック + mypy + ruff + pytestによる継続的検証
- Aliceシナリオの納品検証:PriceTrackerは100万の製品, 数千の同時ユーザー, 秒単位の価格更新を処理可能
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) 依存関係グラフ
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) グローバル例外ハンドラー
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にマージ。📖 まとめ
- 依存関係に基づくモジュール開発順序:インフラ → 認証 → 製品 → 価格 → 通知 → レポートで手戻りを回避
- カスタム例外システム + グローバルハンドラーで統一エラーレスポンス形式を実現:
{"error": {"code": "...", "message": "..."}} - 非同期ファーストの原則:全チェーンで
async/awaitを使用, 同期コードはrun_in_executorでラップ - コード品質保証:ruff (lint + フォーマット)+ mypy (型チェック)+ pytest (テスト)+ pre-commit (自動検証)
- PriceTracker納品検証:100万製品, 数千同時リクエスト, 秒単位のプッシュ通知, 標準化されたエラー形式
📝 練習問題
- 基本問題 (難易度 ⭐):PriceTrackerのカスタム例外階層 (PriceTrackerError → NotFoundError → SubscriptionRequiredError)を実装し, エンドポイントで
raise HTTPException(404)をraise NotFoundError("product", 42)に置き換えてください。ヒント:Exceptionを継承 - 応用問題 (難易度 ⭐⭐):グローバル例外ハンドラーを実装し, PriceTrackerError, RequestValidationError, HTTPExceptionの3つのハンドラーを登録して, すべてのエラーレスポンスが統一形式に従うことを確認してください。ヒント:
app.add_exception_handler()+format_error_response() - チャレンジ (難易度 ⭐⭐⭐):完全なコード品質ツールチェーンを構築 - pyproject.toml (ruff, mypy, pytest設定), .pre-commit-config.yaml - を設定し,
ruff check+mypy app/+pytestを実行してすべてのテストが合格することを確認してください。ヒント:uv add --dev ruff mypy pytest+pre-commit install
---|



