404 Not Found

404 Not Found


nginx

Phase 1 Comprehensive Exercise — Building the PriceTracker Basic API

フェーズ1の5つのレッスンで扱った概念は5つのパズルのピースのようなものです。今こそそれらを組み合わせて1つの完全な絵 — PriceTracker 基本 API — を完成させ, Bob のフロントエンドが実際にデータを利用できるようにしましょう。

1. 学ぶ内容


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

(1) ペインポイント:断片的な知識は製品に組み上げられない

Alice は最初の5つのレッスンを終えましたが, 各レッスンのサンプルは独立したスニペットであり, Path Parameters と Pydantic のサンプルは繋がっていません。Bob は待ちきれずに言いました。「必要なのは動く API であって, 散らかったデモの山じゃない!」Alice はルーティング設計, パラメータバリデーション, データバリデーション, レスポンスフィルタリングを1つの機能する API サービスに統合する必要があります。

(2) 実際の問題に対する総合的な解決策

このレッスンでは, 最初の5つのレッスンで扱ったすべての概念を PriceTracker 基本 API に統合します。これには完全な CRUD エンドポイント群, Pydantic モデルシステム, パラメータバリデーションチェーン, レスポンスフィルタリングロジックが含まれます。

PYTHON
# 完全な PriceTracker 基本 API 構造
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="PriceTracker API", version="0.1.0")

# モデル, ルート, バリデーション — すべて統合済み

(3) 成果

Alice は動く API サービスを持ち, Bob は Swagger UI ですべてのエンドポイントをテストできます。自動生成された OpenAPI ドキュメントにより, フロントエンド SDK が API を直接利用可能です。


3. API エンドポイントの総合設計

(1) フェーズ1 エンドポイント計画

100%
flowchart LR
    Client[Client] -->|GET /products| List[List Products]
    Client -->|POST /products| Create[Create Product]
    Client -->|GET /products/id| Detail[Product Detail]
    Client -->|PUT /products/id| Update[Update Product]
    Client -->|DELETE /products/id| Delete[Delete Product]
    Client -->|GET /prices| Search[Search Prices]
    Client -->|POST /prices| AddPrice[Add Price]
    
    List --> PM[ProductResponse]
    Create --> PM
    Detail --> PDP[ProductDetailResponse]
    Search --> PRM[PriceResponse]
    AddPrice --> PRM
エンドポイント メソッド Path Parameters Query Parameters Request Body レスポンスモデル
商品一覧 GET - category, sort, limit, offset - list[ProductResponse]
商品作成 POST - - ProductCreate ProductResponse
商品詳細 GET product_id - - ProductDetailResponse
商品更新 PUT product_id - ProductUpdate ProductResponse
商品削除 DELETE product_id - - dict
価格検索 GET - product_id, min_price, max_price - list[PriceResponse]
価格追加 POST - - PriceCreate PriceResponse

(1) ▶ サンプル:完全な Pydantic モデルシステム

PYTHON
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import Optional
from enum import Enum
from datetime import datetime

class Category(str, Enum):
    electronics = "electronics"
    clothing = "clothing"
    food = "food"
    books = "books"

class PriceInfo(BaseModel):
    amount: float = Field(gt=0, description="価格金額 (USD)")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    category: Category
    base_price: float = Field(gt=0, description="Base price in USD")
    description: Optional[str] = Field(None, max_length=2000)

class ProductUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=200)
    category: Optional[Category] = None
    base_price: Optional[float] = Field(None, gt=0)
    description: Optional[str] = Field(None, max_length=2000)

class ProductResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    category: str
    base_price: float

class ProductDetailResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    category: str
    base_price: float
    description: Optional[str] = None
    created_at: datetime

class PriceCreate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0, description="Price in USD")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
    source: str = Field(max_length=100)

    @field_validator("price")
    @classmethod
    def round_price(cls, v: float) -> float:
        return round(v, 2)

class PriceResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    product_id: int
    price: float
    currency: str
    source: str
    recorded_at: datetime

出力:

TEXT
# 関数定義成功

4. リクエスト・レスポンスライフサイクル

(1) 完全なワークフロー

100%
sequenceDiagram
    participant Client as Bob Frontend
    participant FastAPI as FastAPI Router
    participant Pydantic as Pydantic Validator
    participant Handler as View Function
    participant DB as In-Memory DB

    Client->>FastAPI: POST /products (JSON body)
    FastAPI->>Pydantic: Validate with ProductCreate
    Pydantic-->>FastAPI: Validated model instance
    FastAPI->>Handler: create_product(data: ProductCreate)
    Handler->>DB: Store product
    DB-->>Handler: Stored record
    Handler-->>FastAPI: Return full dict
    FastAPI->>Pydantic: Filter with ProductResponse
    Pydantic-->>Client: JSON response (filtered)

(1) ▶ サンプル:商品 CRUD エンドポイントの実装

PYTHON
from fastapi import FastAPI, Path, Query, HTTPException
from datetime import datetime

app = FastAPI(title="PriceTracker API", version="0.1.0")

# インメモリストレージ (フェーズ2で DB に置き換え)
products_db: dict[int, dict] = {}
prices_db: list[dict] = []
_product_counter = 0
_price_counter = 0

@app.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(product: ProductCreate):
    global _product_counter
    _product_counter += 1
    record = {
        "id": _product_counter,
        "name": product.name,
        "category": product.category.value,
        "base_price": product.base_price,
        "description": product.description,
        "created_at": datetime.utcnow(),
    }
    products_db[_product_counter] = record
    return record

@app.get("/products", response_model=list[ProductResponse])
async def list_products(
    category: Optional[Category] = Query(None),
    sort: str = Query("name", pattern="^(name|base_price)$"),
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    items = list(products_db.values())
    if category:
        items = [p for p in items if p["category"] == category.value]
    items.sort(key=lambda p: p.get(sort, ""))
    return items[offset : offset + limit]

@app.get("/products/{product_id}", response_model=ProductDetailResponse)
async def get_product(
    product_id: int = Path(gt=0, description="商品 ID"),
):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return products_db[product_id]

出力:

TEXT
# 関数定義成功

(2) ▶ サンプル:更新と削除エンドポイント

PYTHON
@app.put("/products/{product_id}", response_model=ProductResponse)
async def update_product(
    product_id: int = Path(gt=0),
    update: ProductUpdate = ...,
):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    record = products_db[product_id]
    update_data = update.model_dump(exclude_unset=True)
    record.update(update_data)
    return record

@app.delete("/products/{product_id}")
async def delete_product(product_id: int = Path(gt=0)):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    del products_db[product_id]
    return {"message": "Product deleted"}

出力:

TEXT
# 関数定義成功

5. 価格エンドポイントと実践的な組み合わせ戦略

(1) Query Parameters と Request Body の組み合わせ

(1) ▶ サンプル:価格検索と作成エンドポイント

PYTHON
@app.get("/prices", response_model=list[PriceResponse])
async def search_prices(
    product_id: Optional[int] = Query(None, gt=0),
    min_price: float = Query(0, ge=0, description="最低価格 (USD)"),
    max_price: float = Query(999999, ge=0, description="最高価格 (USD)"),
    limit: int = Query(50, ge=1, le=200),
    offset: int = Query(0, ge=0),
):
    results = prices_db
    if product_id:
        results = [p for p in results if p["product_id"] == product_id]
    results = [p for p in results if min_price <= p["price"] <= max_price]
    return results[offset : offset + limit]

@app.post("/prices", response_model=PriceResponse, status_code=201)
async def create_price(price: PriceCreate):
    global _price_counter
    if price.product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    _price_counter += 1
    record = {
        "id": _price_counter,
        "product_id": price.product_id,
        "price": price.price,
        "currency": price.currency,
        "source": price.source,
        "recorded_at": datetime.utcnow(),
    }
    prices_db.append(record)
    return record

出力:

TEXT
# 関数定義成功

6. レスポンスフィルタリングの実践ガイド:公開版 vs 管理者版

(1) ビジネスシナリオ:卸売価格の非表示

PriceTracker の小売ユーザーは小売価格のみ閲覧でき, 管理者は卸売価格と原価を閲覧できます。

(1) ▶ サンプル:マルチロール レスポンスモデル

PYTHON
class ProductPublicResponse(BaseModel):
    """公開ビュー - 卸売価格と原価を非表示"""
    id: int
    name: str
    category: str
    retail_price: float

class ProductAdminResponse(BaseModel):
    """管理者ビュー - 完全な価格チェーンを表示"""
    id: int
    name: str
    category: str
    retail_price: float
    wholesale_price: float
    cost_price: float
    margin_pct: float  # 利益率パーセント

# 価格階層を含む拡張インメモリストレージ
admin_products_db: dict[int, dict] = {}

@app.get("/products/{product_id}/public", response_model=ProductPublicResponse)
async def get_product_public(product_id: int = Path(gt=0)):
    if product_id not in admin_products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return admin_products_db[product_id]

@app.get("/products/{product_id}/admin", response_model=ProductAdminResponse)
async def get_product_admin(product_id: int = Path(gt=0)):
    if product_id not in admin_products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return admin_products_db[product_id]

出力:

TEXT
# 関数定義成功

❓ よくある質問

Q 再起動後, インメモリストレージのデータはどうなりますか?
A フェーズ1では学習を簡素化するためにインメモリストレージを使用しています。フェーズ2で PostgreSQL + SQLAlchemy に置き換わります。これは段階的な学習戦略です。
Q ProductCreateProductResponse はなぜ分離されているのですか?
A 関心の分離のためです。Create モデルには ID が含まれず (サーバーで生成される), Response モデルには ID が含まれます。これはセキュリティのベストプラクティスでもあります — クライアントが ID を指定すべきではありません。
Q PUT エンドポイントの exclude_unset=True の目的は何ですか?
A クライアントが変更したいフィールドのみ更新できるようにします。提供されなかったフィールドは上書きされず, None のままです。ProductUpdate のオプショナルフィールドと組み合わせることで, 部分更新が可能になります。
Q OpenAPI ドキュメントがフロントエンドアプリケーションで利用可能かどうかをテストするにはどうすればよいですか?
A /openapi.json にアクセスし, npx openapi-typescript で TypeScript 型を生成するか, Swagger Codegen で SDK を生成してください。
Q Enum パラメータの値はドキュメントにどのように表示されますか?
A FastAPI は Swagger UI のドロップダウンメニューにすべての Enum 値を自動表示するため, フロントエンド開発者の Bob にもすぐに分かります。
Q 複数のエンドポイントがモデルを共有する場合, 重複を避けるにはどうすればよいですか?
A モデル継承を使用してください。ProductBase(BaseModel) に共通フィールドを定義し, ProductCreate(ProductBase)ProductResponse(ProductBase) に継承・拡張させます。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):このレッスンのすべてのコードを1つの app/main.py にまとめ, サービスを起動して Swagger UI で3つの商品を作成し一覧を確認してください。ヒント:uvicorn app.main:app --reload
  2. 応用問題 (難易度 ⭐⭐):/products/{id}/prices エンドポイントを追加し, 指定された商品のすべての価格レコードを検索してください。sort (date/price)と limit の Query Parameters をサポートしてください。ヒント:product_id Path Parameter + sort/limit Query Parameters
  3. チャレンジ問題 (難易度 ⭐⭐⭐):ProductAdminProductPublic を使用してデュアルビューシステムを実装してください。完全な価格チェーンを含む商品データを作成し, 公開エンドポイントは小売価格のみを返し, 管理エンドポイントは小売価格, 卸売価格, 利益率を返すようにしてください。ヒント:2つの response_model インスタンス + 同じデータソース

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%