モダンPython実践 #1 FastAPI のはじめ方とセットアップ

読了 7分

モダン Python 基礎中級上級 21 編の道具すべてが集まるところです。FastAPI で 1 つのプロジェクト を作りながら、型ヒント、dataclass/Pydantic、async/await、依存性注入、テストまで — 今まで磨いた道具を一カ所に集めます。

  • #1 はじめ方とセットアップ、OpenAPI ← 今回
  • #2 ルーティング、Pydantic モデル、依存性注入
  • #3 DB 連携 — SQLAlchemy 2.x + Alembic
  • #4 認証 — OAuth2 パスワードフロー + JWT
  • #5 非同期とバックグラウンドタスク
  • #6 テストとデプロイ — pytest、Docker、Railway/Fly

作るのは小さな TODO API — ユーザーの会員登録 / ログイン、TODO の CRUD、非同期タスクの処理、テスト、デプロイまで。各回はその上に一段ずつ積み重ねる構成です。

なぜ FastAPI なのか #

Python の Web フレームワークはたくさんあります。Django、Flask、FastAPI、Litestar、Starlette など。それぞれにポジションがあります。

DjangoFlaskFastAPI
スタイルフルスタック (バッテリー同梱)マイクロマイクロ + 型
非同期部分的 (4.0+)外部ライブラリネイティブ
型ヒント活用補助的補助的コア動作
自動ドキュメント (OpenAPI)別ライブラリビルトイン
学習曲線非常になだらかなだらか
データ検証DRF + 自前自前Pydantic 自動

FastAPI の強みは 型ヒントがそのまま動作する ことです。関数シグネチャを書くと、それが即:

  • 入力検証
  • シリアライズ / デシリアライズ
  • OpenAPI スキーマ (Swagger UI 自動生成)
  • エディタの自動補完
  • 静的型チェック

この 5 つが一度に起こります。他のフレームワークではそれぞれを別の設定で組まなければなりません。モダン Python シリーズで磨いた型ヒントが最もよく活きるところ が FastAPI で、だからトラックの収束点として選びました。

プロジェクトのセットアップ #

基礎 #1 の uv の流れで始めます。

プロジェクトを作る
uv init todo-api --python 3.14
cd todo-api

依存関係のインストール #

実行に必要な依存関係
uv add "fastapi[standard]"

fastapi[standard] 一行に付いてくるもの:

  • FastAPI — フレームワーク本体
  • uvicorn — ASGI サーバー (開発 / プロダクション)
  • pydantic — データ検証
  • httpx — テスト用クライアント (次回以降)
  • email-validatorEmailStr 検証
  • python-multipart — フォーム / ファイルアップロード
  • fastapi-clifastapi CLI コマンド

次に開発用ツール。

開発ツール
uv add --dev pytest pytest-asyncio ruff pyright

上級 #7 で見た道具箱に加えて、FastAPI テストの標準は pytest + httpx の組み合わせです (#6 で詳しく)。

最初のエンドポイント #

app/main.py
from fastapi import FastAPI

app = FastAPI(
    title="Todo API",
    version="0.1.0",
    description="モダン Python 実践シリーズの例 API",
)

@app.get("/")
def read_root() -> dict[str, str]:
    return {"hello": "fastapi"}

これで終わりです。7 行の Python で HTTP サーバーになります。

実行 #

開発モード
uv run fastapi dev app/main.py

fastapi dev がやること:

  • uvicorn を立ち上げ
  • コード変更時に自動リロード
  • デバッグログ
  • LAN 公開しない (127.0.0.1 だけ)

ブラウザで http://127.0.0.1:8000 を開くと:

{"hello": "fastapi"}

より重要なもの — 自動ドキュメント #

同じサーバーの /docs に行ってみてください。Swagger UI が自動で立ち上がっています。エンドポイント一覧、入力スキーマ、レスポンスモデル、そのまま呼び出せる「Try it out」 ボタンまで。コードを 1 行も追加していないのに。

/redoc に行くと ReDoc スタイルのドキュメントもあります。

これが FastAPI の言う 自動ドキュメント の正体です。関数シグネチャ + 型ヒント + docstring がそのまま OpenAPI 仕様に変換されます。

より豊かなエンドポイント #

複数のメソッド / パス
from fastapi import FastAPI

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

@app.get("/")
def read_root() -> dict[str, str]:
    """ルート — ヘルスチェック用。"""
    return {"hello": "fastapi"}

@app.get("/items/{item_id}")
def read_item(item_id: int) -> dict[str, int]:
    """ID で単一アイテムを取得。"""
    return {"item_id": item_id}

@app.get("/items")
def list_items(skip: int = 0, limit: int = 10) -> dict[str, int]:
    """ページネーションのクエリパラメータ。"""
    return {"skip": skip, "limit": limit}

ここで起こる自動変換:

  • パスパラメータ {item_id} → 関数の引数 item_id: int
    • int でなければ 422 Unprocessable Entity が自動で返る
  • クエリパラメータ skiplimit → デフォルト値ありの関数引数
    • ?skip=10&limit=20 の形で自動パース
  • 戻り値の型 -> dict[str, int] → JSON レスポンス
  • docstring → Swagger UI の説明

int 型を書いただけで自動で変換 / 検証が行われるのが核心です。Flask なら:

🚫 Flask との比較
@app.route("/items/<int:item_id>")
def read_item(item_id):
    return jsonify({"item_id": item_id})

<int:item_id> のようにルーターに型を書き、検証失敗のレスポンスを自分で処理しなければなりません。FastAPI は 関数シグネチャがルーター仕様 です。

PathQuery — より細かい検証 #

高度な検証
from fastapi import FastAPI, Path, Query
from typing import Annotated

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(
    item_id: Annotated[int, Path(ge=1, le=10000, description="アイテム ID")],
    q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
):
    return {"item_id": item_id, "q": q}

Path()Query()検証ルールメタデータ を追加します。

  • ge=1>= 1
  • le=10000<= 10000
  • min_length=3 — 最小 3 文字
  • description=... — Swagger ドキュメントに表示

Annotated[T, ...] のパターンが 上級 #6 で見たそれです — 型にメタデータを束ねる標準。FastAPI のほとんどすべての依存性注入がこのパターンの上に作られています。

レスポンス — 自動シリアライズ #

dataclass / Pydantic モデルを返す
from datetime import datetime, timezone
from pydantic import BaseModel

class Todo(BaseModel):
    id: int
    title: str
    done: bool
    created_at: datetime

@app.get("/todos/1")
def get_todo() -> Todo:
    return Todo(
        id=1,
        title="FastAPI 学習",
        done=False,
        created_at=datetime.now(timezone.utc),
    )

レスポンス:

{
  "id": 1,
  "title": "FastAPI 学習",
  "done": false,
  "created_at": "2026-05-01T12:00:00+00:00"
}

datetime のような非 JSON 型も 自動変換 されます。Pydantic がシリアライズを担当します。これを自前で書くとルートごとに変換コードを書かなければなりませんが、FastAPI は戻り値の型を書くだけ。

Todo モデルの詳細は #2 で。

エラー — HTTPException #

エラーレスポンス
from fastapi import HTTPException

todos: dict[int, Todo] = {1: Todo(...)}

@app.get("/todos/{todo_id}")
def get_todo(todo_id: int) -> Todo:
    if todo_id not in todos:
        raise HTTPException(status_code=404, detail="Todo not found")
    return todos[todo_id]

raise HTTPException(...) 一行で JSON エラーレスポンス が返ります。

{"detail": "Todo not found"}

標準の HTTP ステータスコード (404、400、500 など) と一緒に。次回以降でユーザー定義の例外ハンドラも扱います。

プロジェクト構造 — 最初からきれいに #

小さな API でも最初から モジュール / パッケージ に分けると、基礎 #7 で見た流れがそのまま生きます。

基本構造 (今回のシリーズが埋めていく形)
todo-api/
├── pyproject.toml
├── app/
│   ├── __init__.py
│   ├── main.py            # FastAPI インスタンス、ルーター登録
│   ├── core/
│   │   ├── config.py      # 環境変数
│   │   └── security.py    # JWT、パスワード (#4)
│   ├── api/
│   │   ├── __init__.py
│   │   ├── deps.py        # 依存性注入 (#2、#4)
│   │   ├── todos.py       # TODO ルーター
│   │   └── auth.py        # 認証ルーター (#4)
│   ├── models/            # SQLAlchemy モデル (#3)
│   │   └── todo.py
│   ├── schemas/           # Pydantic スキーマ (#2)
│   │   └── todo.py
│   └── db/                # DB 接続 (#3)
│       └── session.py
├── alembic/               # マイグレーション (#3)
└── tests/                 # pytest (#6)
    └── test_todos.py

今回は app/main.py 一ファイルですが、次回からディレクトリを 1 つずつ埋めていきます。

環境変数 — pydantic-settings #

設定値 (DB URL、JWT シークレットなど) をコードに埋め込まず、環境変数に分離するのが標準です。FastAPI 陣営の標準ツールは pydantic-settings

インストール
uv add pydantic-settings
app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    app_name: str = "Todo API"
    database_url: str = "sqlite+aiosqlite:///./todo.db"
    jwt_secret: str = "dev-only-change-me"
    jwt_expire_minutes: int = 60

    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

settings = Settings()

.env ファイルから自動で読み、環境変数があればそれで上書きします。型検証まで 含むので、誤った環境変数は起動時点で止まります。

.env
JWT_SECRET=production-secret-here
DATABASE_URL=postgresql+asyncpg://user:pw@localhost/todo

.env は git から除外 (.gitignore に追加)。セキュリティに注意。

最初の回の成果物まとめ #

app/main.py — 今回の仕上げ
from fastapi import FastAPI, HTTPException
from app.core.config import settings

app = FastAPI(
    title=settings.app_name,
    version="0.1.0",
)

@app.get("/")
def read_root() -> dict[str, str]:
    return {"app": settings.app_name, "status": "ok"}

@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "healthy"}
実行
uv run fastapi dev app/main.py
# → http://127.0.0.1:8000
# → http://127.0.0.1:8000/docs

エンドポイント 2 つ + 自動ドキュメント + 環境変数の分離。次回の出発点です。

まとめ #

今回つかんだもの:

  • なぜ FastAPI — 型ヒントがそのまま検証 / シリアライズ / ドキュメント / 自動補完で動作
  • uv add "fastapi[standard]" 一行に uvicorn/pydantic/httpx すべて付いてくる
  • fastapi dev で自動リロードの開発モード
  • 関数シグネチャがルーター仕様 — パス / クエリパラメータの型変換が自動
  • /docs/redoc — OpenAPI/Swagger UI の自動生成
  • Annotated[T, Path(...)]Annotated[T, Query(...)] で細かい検証
  • Pydantic モデルを返す — datetime などを自動シリアライズ
  • HTTPException でエラーレスポンス
  • pydantic-settings で環境変数を分離
  • 最初からモジュール / パッケージ構造

次回(#2 ルーティング、Pydantic、依存性注入)では APIRouter でルートを分離し、Pydantic スキーマ を本格的に使い、依存性注入 でルート間の共通ロジック (認証済みユーザー、DB セッションなど) をきれいに解くパターンを扱います。

X