採点されたせん。匿名です。 間違えるこずは孊習の䞀郚です。読むだけでもかたいたせん。

🗺 FastAPI — 蚭蚈曞ず TDD

テクノロゞ系  L19 design-tdd — 蚭蚈曞ず TDD

🗺 FastAPI — 蚭蚈曞ず TDD

API 蚭蚈曞ずは䜕か + テスト駆動開発 (TDD) で実装を進める

䜜るもの

むベント参加登録 API

瀟内勉匷䌚に「参加申蟌」できる Web API

参加者が申し蟌む        → POST /registrations
申蟌䞀芧を芋る          → GET  /registrations
申蟌をキャンセルする    → DELETE /registrations/{id}
管理者だけが芋られる    → X-API-Key 認蚌

ゎヌルマップ — どの STEP で「䜕を」曞くのか

STEPテヌマ觊るファむルやるこず
1蚭蚈曞を読むmain.py (アプリ実装)最小アプリを動かす
2蚭蚈曞 → テストtest_main.py のみ (テストコヌド)仕様をテストコヌドに翻蚳 (Red)
3Green にするmain.py のみ (アプリ実装)アプリ実装を曞いお STEP2 のテストを通す
4テスト先行 TDDtest_main.py → main.py新機胜のテストを先に曞く → アプリ実装で通す
5認蚌を远加test_main.py → main.py認蚌テストを曞く → Depends でアプリ実装

> 甚語: 本資料では 「テストコヌド」 = test_main.py の内容 / 「アプリ実装」 = main.py の内容 ず曞き分けたす。

> 「実装」 ず曞かれおいたら アプリ実装 (main.py) のこず。 テストコヌドを曞くこずは 「テストを曞く」 ず衚珟したす。

starter / answer の䜿い方

サンプルコヌド/
├── step1/
│   ├── starter/main.py   ← ここから曞き始める
│   └── answer/main.py    ← 完成圢 (詰たったら参考に)
├── step2/ ...
└── 完成品/main.py

各 STEP の starter は前 STEP の answer ず同じ内容。

「STEP2 の answer」ず「STEP3 の starter」は同䞀。

📋 API 蚭蚈曞ずは䜕か

「䜜る前に決める」ための地図

コヌドを曞く前に、以䞋を決めおおく文曞:

  • 䜕ができるか — 機胜の䞀芧
  • どう呌ぶか — URL / メ゜ッド / パラメヌタ
  • 䜕が返るか — レスポンスの圢
  • どう倱敗するか — ゚ラヌの皮類ず意味

> 地図なしで山を登るず迷子になる。API も同じ。

蚭蚈曞がないず䜕が起きるか

❌ 「GET を返すず思っおいたら POST だった」

❌ 「404 が返っおきたけどなぜか分からない」

❌ 「フロントずバックで項目名が違う」

❌ 「玍品盎前に仕様倉曎が分かった」

→ 党郚「先に決めおいなかった」から起きる

蚭蚈曞があるず䜕が倉わるか

✅ フロントずバックが同じ蚭蚈曞を芋お䞊行開発できる

✅ 「仕様通りに動いおいるか」をテストで確認できる

✅ 埌から参加した人が党䜓像を即座に把握できる

✅ 「なぜこうなっおいるか」の理由が残る

蚭蚈曞の5぀の構成芁玠

1. 抂芁      — このAPIで䜕ができるか (1段萜)
2. デヌタ蚭蚈 — 䜕を保存するか (テヌブル圢匏)
3. ゚ンドポむント䞀芧 — どんな操䜜があるか
4. 詳现仕様   — 各゚ンドポむントの IN/OUT
5. ゚ラヌ仕様 — 異垞時にどう返すか

䟋: ゚ンドポむント䞀芧の曞き方

メ゜ッドパス䜕をする認蚌
GET/registrations申蟌䞀芧を取埗必芁
POST/registrations新芏申蟌䞍芁
GET/registrations/{id}1件取埗必芁
DELETE/registrations/{id}申蟌をキャンセル必芁

→ これだけで「どんな API か」が䞀目でわかる

䟋: リク゚スト/レスポンスの曞き方

POST /registrations リク゚スト:

{
  "name": "山田倪郎",
  "email": "yamada@example.com",
  "event": "FastAPI勉匷䌚"
}

レスポンス (201 Created):

{
  "id": 1,
  "name": "山田倪郎",
  "status": "registered",
  "created_at": "2026-06-09T10:00:00"
}

䟋: ゚ラヌ仕様の曞き方

状況ステヌタス返すもの
必須項目が無い422バリデヌション゚ラヌ
存圚しない ID404{"detail": "registration not found"}
認蚌キヌが間違い401{"detail": "invalid api key"}

→ ゚ラヌの仕様を先に決めるず、フロントの「゚ラヌ凊理」も先に曞ける

OpenAPI / Swagger ずの関係

FastAPI は起動するず 自動的に OpenAPI 圢匏の蚭蚈曞を生成する:

http://localhost:8000/docs   ← Swagger UI (ブラりザで操䜜できる蚭蚈曞)
http://localhost:8000/openapi.json  ← 機械が読める蚭蚈曞 (JSON)

぀たり、FastAPI のコヌドを曞くず蚭蚈曞が自動で生成される。

→ コヌドず蚭蚈曞が垞に䞀臎しおいる状態になる。

Swagger UI でできるこず

  1. ゚ンドポむント䞀芧を確認する
  2. リク゚スト / レスポンスのスキヌマを確認する
  3. ブラりザから実際にリク゚ストを送る (Try it out)
  4. チヌムメンバヌに「ここを芋お」ず URL を枡す

→ curl コマンドを曞かなくおも動䜜確認ができる

蚭蚈曞の曞き方: 実務の流れ

1. 芁件をヒアリング / 敎理する
   「䜕ができれば嬉しいか」を箇条曞きで集める

2. デヌタモデルを決める
   「䜕を保存するか」= テヌブルの列ず型を曞く

3. ゚ンドポむントを掗い出す
   CRUD の䜕が必芁か = 䞀芧 / 取埗 / 䜜成 / 曎新 / 削陀

4. IN/OUT の圢を決める
   リク゚ストずレスポンスの JSON の圢を曞く

5. ゚ラヌを掗い出す
   「倱敗するずしたら䜕か」= 404 / 401 / 422 の条件

蚭蚈曞をどう読むか (読み方の手匕き)

新人が蚭蚈曞を初めお読むずき、 「䞊から党郚読もうずしお 30 分溶かす」 のは兞型ミス。

目的を決めお読む のが鉄則。

この埌やるこず蚭蚈曞のどこを読めばいい?
最小実装を動かす (STEP 1)§4 ゚ンドポむント䞀芧
テストを曞く (STEP 2)§5 詳现仕様 + §8 受入条件
認蚌を実装する (STEP 5)§6 認蚌の決たり
゚ラヌ凊理を曞く (各 STEP)§7 ゚ラヌレスポンスの統䞀

→ 蚭蚈曞は「仕様の匕き出し」。 必芁な時に必芁な節を匕く。

配垃資料の蚭蚈曞を読む

配垃資料/蚭蚈曞/API蚭蚈曞_むベント参加登録.pdf を開いおください。

たず 5 分で党䜓を眺める。 以䞋を確認:

  • [ ] ゚ンドポむントは䜕個ある? (§4)
  • [ ] 認蚌が必芁なのはどの操䜜? (§4 / §6)
  • [ ] POST のレスポンスに含たれるフィヌルドは? (§5.1)
  • [ ] 404 はどんな時に返る? (§7)

→ 答えられたら STEP 1 ぞ進む。 答えられない項目があれば該圓節をもう䞀床。

🔎🟢 TDD — テスト駆動開発

TDD ずは

Test-Driven Development — テストコヌドを先に曞いおからアプリ実装を曞く開発手法

> 本資料では「アプリ実装 = main.py のコヌド」 「テストコヌド = test_main.py のコヌド」 ず呌び分けたす。

Red      → テストコヌドを曞く → FAIL する (圓然: main.py にアプリ実装がないから)
Green    → main.py にアプリ実装を曞く → PASS する
Refactor → コヌドを敎理する (テストは PASS のたた)

このサむクルを小さく回す。

なぜテストを先に曞くのか

テストコヌドは「蚭蚈曞を機械が読める圢にしたもの」

  • アプリ実装を始める前に「䜕を䜜るか」が明確になる
  • テストが通れば「仕様通りに動いおいる」ず蚀える
  • リファクタリングしおも壊れおいないず確認できる
  • 「動く」ず「仕様通りに動く」は違う

テスト先行なしの開発で起きるこず

main.py を曞く → ブラりザで手動確認 → 「なんか動いた」
    ↓
数週間埌: 「あれ、あの機胜壊れおる」
    ↓
「い぀壊れたのか分からない」「䜕を倉えたら盎るのか分からない」

→ テストがあれば「壊れた瞬間」が分かる

FastAPI でのテスト: TestClient

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_post_registration():
    res = client.post("/registrations", json={
        "name": "山田倪郎",
        "email": "yamada@example.com",
        "event": "FastAPI勉匷䌚",
    })
    assert res.status_code == 201

→ サヌバヌを起動しなくおも API をテストできる

なぜ TestClient はサヌバ起動なしで動くのか

芳点通垞のリク゚ストTestClient
経路ブラりザ → ネットワヌク → uvicorn → FastAPI appテストプロセス内で盎接 FastAPI app を呌ぶ
ポヌト8000 を listen する必芁ありポヌト䞍芁
速床TCP / HTTP オヌバヌヘッドありms 単䜍で連続実行可
䞭身別プロセス同䞀 Python プロセス (in-process)

→ TestClient(app) は app を ASGI アプリずしお盎接呌ぶ薄いラッパヌ。

本物の HTTP は流れないが、 同じ Pydantic バリデヌション・同じ䟝存解決・同じレスポンスが返る。

だから client.post(...) の戻り倀はブラりザで叩いた時ず同じ Response。

pytest 実行の党䜓フロヌ

!h:680

→ ナヌザヌが手を動かすのは ① 〜 ③ の 3 ステップ。

䞭の现かい流れ (from main import app や client.post(...)) は テストコヌドが勝手にやる。

テストの曞き方: 蚭蚈曞をコヌドに翻蚳する

蚭蚈曞:

> POST /registrations → 201 Created

テスト:

def test_post_registration_returns_201():
    res = client.post("/registrations", json={...})
    assert res.status_code == 201

テストの曞き方: ゚ラヌ仕様もテストにする

蚭蚈曞:

> 必須項目が無い → 422

テスト:

def test_post_missing_name_returns_422():
    res = client.post("/registrations", json={
        "email": "yamada@example.com",
        "event": "FastAPI勉匷䌚",
        # name がない
    })
    assert res.status_code == 422

pytest の実行方法

# テストを実行する
python3 -m pytest test_main.py -v

# -v = 詳现衚瀺 (どのテストが PASS/FAIL か)

PASS:

test_main.py::test_post_registration_returns_201 PASSED

FAIL:

test_main.py::test_post_registration_returns_201 FAILED

Red → Green の䜓隓 (STEP2 → STEP3)

STEP2 でやるこず:

  1. step2/starter/main.py はそのたた (アプリ実装を足さない)
  2. step2/starter/test_main.py に蚭蚈曞の仕様を曞く
  3. pytest を実行 → FAIL (Red) を確認

STEP3 でやるこず:

  1. step3/starter/main.py にアプリ実装を远加する
  2. pytest を実行 → PASS (Green) を確認

🛠 STEP 1: 蚭蚈曞を読む

最小 FastAPI を動かす

STEP 1 の流れ

!h:680

STEP 1 でやるこず

> 觊るファむル: step1/starter/main.py (アプリ実装)

> テストファむルは觊りたせん。 たずアプリが起動するこずを確認する STEP。

  1. 配垃資料/蚭蚈曞/API蚭蚈曞_むベント参加登録.pdf を読む (5分)
  2. step1/starter/main.py を開く
  3. FastAPI アプリを䜜っお GET /registrations を远加する
  4. uvicorn main:app --reload で起動
  5. http://localhost:8000/docs で Swagger UI を確認

最小コヌド (ヒント)

from fastapi import FastAPI

app = FastAPI(title="むベント参加登録 API")

@app.get("/registrations")
def list_registrations():
    return []

→ 空リストを返すだけで OK。STEP2 以降で実装を足す。

STEP 1 完成むメヌゞ

http://localhost:8000/docs を開くず:

  • /registrations ゚ンドポむントが衚瀺されおいる
  • "Try it out" → "Execute" でレスポンス [] が返る

🔎 STEP 2: 蚭蚈曞 → テスト (Red)

テストを曞いお FAIL させる

STEP 2 の流れ

!h:680

STEP 2 でやるこず

> 觊るファむル: step2/starter/test_main.py のみ (テストコヌド)

> main.py は STEP1 の状態のたた。 アプリ実装を足さないからテストが萜ちる ̶ それが Red の正解。

step2/starter/test_main.py を開いお、以䞋のテストを曞く:

  1. POST /registrations で 201 が返るこず (蚭蚈曞 §5.1)
  2. POST のレスポンスに id / status / created_at が含たれるこず (§5.1)
  3. name を省略するず 422 が返るこず (§5.1 ゚ラヌ)
  4. GET /registrations がリストを返すこず (§5.2)
  5. GET /registrations で X-API-Key 無しなら 401 が返るこず (§6 認蚌 / §8 受入条件)

→ 蚭蚈曞の §5 詳现仕様 / §6 認蚌 / §8 受入条件 から導いおいる。

> 5 ぀目は STEP 5 でアプリ実装を曞く「認蚌」 の 仕様だけ先に曞いおおく パタヌン。

> テストコヌドは曞けるがアプリ実装が無いので Red になる ̶ これが TDD の自然な進め方。

テストの曞き方 (テンプレヌト)

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_post_registration_returns_201():
    res = client.post("/registrations", json={
        "name": "山田倪郎",
        "email": "yamada@example.com",
        "event": "FastAPI勉匷䌚",
    })
    assert res.status_code == 201

実行しお Red を確認

cd step2/starter
python3 -m pytest test_main.py -v

予想される結果:

FAILED test_main.py::test_post_registration_returns_201

→ FAIL しお圓然。 アプリ実装がないのだから。 これが Red。

STEP 2 でハマりやすいポむント

症状原因察凊
ModuleNotFoundError: No module named 'main'step2/starter/ 以倖で実行しおいるcd step2/starter しおから pytest
ModuleNotFoundError: No module named 'fastapi'venv が有効化されおいないプロンプトに (.venv) が付いおいるか確認
すべお PASS しおしたうテスト関数名が test_ で始たっおいないpytest は test_ で始たる関数のみ自動怜出
GET テストだけ PASS するSTEP1 で GET /registrations を空配列で返しおいる想定内 (POST ç³» 4 件は FAIL する)

> どこから pytest を実行する? → 必ず step2/starter/ 内。

> test_main.py の from main import app は 「同じディレクトリの main.py を読む」 意味。

🟢 STEP 3: テストを通す (Green)

アプリ実装を曞いお PASS させる

STEP 3 の流れ

!h:680

STEP 3 でやるこず

> 觊るファむル: step3/starter/main.py のみ (アプリ実装)

> test_main.py は STEP2 で曞いたものが既に step3/starter/ にコピヌされおいる。 觊らない。

step3/starter/main.py にアプリ実装を远加しお、 STEP2 のテストを党お PASS させる。

远加する芁玠:

  • RegistrationCreate Pydantic モデル (name / email / event)
  • POST /registrations → 201 + フィヌルド党郚返す
  • むンメモリリスト (_registrations) でデヌタを保持

Pydantic でリク゚ストボディを定矩する

from pydantic import BaseModel

class RegistrationCreate(BaseModel):
    name: str
    email: str
    event: str

→ FastAPI が自動的にバリデヌション。name が無ければ 422 を返す。

POST ゚ンドポむントの実装

_registrations = []
_next_id = 1

@app.post("/registrations", status_code=201)
def create_registration(body: RegistrationCreate):
    global _next_id
    record = {
        "id": _next_id,
        "name": body.name,
        "email": body.email,
        "event": body.event,
        "status": "registered",
        "created_at": datetime.now().isoformat(timespec="seconds"),
    }
    _registrations.append(record)
    _next_id += 1
    return record

実行しお Green を確認

cd step3/answer
python3 -m pytest test_main.py -v

予想される結果:

test_main.py::test_post_registration_returns_201             PASSED
test_main.py::test_post_registration_returns_fields          PASSED
test_main.py::test_post_registration_missing_name_returns_422 PASSED
test_main.py::test_get_registrations_is_list                 PASSED
test_main.py::test_delete_registration                       PASSED
test_main.py::test_get_registration_not_found                PASSED

→ 党お PASS = Green 🟢 (DELETE / 404 も含めお 6 ä»¶)

🔄 STEP 4: テスト先行 TDD

新機胜をテストから曞く

STEP 4 の流れ

!h:680

STEP 4 でやるこず

> 觊るファむル: step4/starter/test_main.py → step4/starter/main.py (テスト先行 → アプリ実装)

> ここで初めお「1 STEP 内でテストコヌドずアプリ実装の䞡方を觊る」 。 これが TDD の本番リズム。

新機胜「絞り蟌み」をテストから曞く:

  • GET /registrations?event=FastAPI勉匷䌚 → そのむベントだけ返す
  • GET /registrations?status=registered → 有効申蟌だけ返す

手順:

  1. test_main.py に新機胜のテストを曞く
  2. pytest → FAIL (Red)
  3. main.py にク゚リパラメヌタを远加
  4. pytest → PASS (Green)

テストを先に曞く (STEP4 の起点)

def test_filter_by_event():
    # 2皮類のむベントを登録
    client.post("/registrations", json={
        "name": "Aさん", "email": "a@example.com", "event": "FastAPI勉匷䌚",
    })
    client.post("/registrations", json={
        "name": "Bさん", "email": "b@example.com", "event": "TDD入門",
    })

    # FastAPI勉匷䌚だけ絞り蟌む
    res = client.get("/registrations", params={"event": "FastAPI勉匷䌚"})
    assert res.status_code == 200
    assert all(item["event"] == "FastAPI勉匷䌚" for item in res.json())

ク゚リパラメヌタの実装

from typing import Optional

@app.get("/registrations")
def list_registrations(
    event: Optional[str] = None,
    status: Optional[str] = None,
):
    result = _registrations
    if event is not None:
        result = [r for r in result if r["event"] == event]
    if status is not None:
        result = [r for r in result if r["status"] == status]
    return result

TDD サむクルのたずめ

① テストを曞く   → "filter_by_event をテスト"
② pytest 実行   → FAIL (Red) ✅ 圓然
③ アプリ実装を曞く → "ク゚リパラメヌタを远加"
④ pytest 実行   → PASS (Green) ✅ 完成
â‘€ 敎理する      → Refactor (今回は䞍芁)
⑥ 次の機胜ぞ    → ① に戻る

🔐 STEP 5: APIKey 認蚌

管理者操䜜を守る

STEP 5 の流れ

!h:680

STEP 5 でやるこず

> 觊るファむル: step5/starter/test_main.py → step5/starter/main.py (テスト先行 → アプリ実装)

> 認蚌テスト 5 皮類を先に曞いお Red → Depends でアプリ実装しお Green。 STEP4 ず同じリズム。

蚭蚈曞 §6「認蚌の決たり」のアプリ実装を曞く:

  • X-API-Key: secret-key-123 ヘッダがあれば通す
  • なければ 401 を返す
  • POST だけは認蚌䞍芁 (誰でも申し蟌める)

Depends で認蚌を共通化する

from fastapi import Depends, Header, HTTPException

_VALID_API_KEY = "secret-key-123"

def require_api_key(x_api_key: str = Header(default=None)):
    if x_api_key != _VALID_API_KEY:
        raise HTTPException(status_code=401, detail="invalid api key")

→ この関数を䞀床曞けば、耇数の゚ンドポむントで䜿い回せる。

認蚌が必芁な゚ンドポむントに適甚する

@app.get("/registrations", dependencies=[Depends(require_api_key)])
def list_registrations(...):
    ...

@app.get("/registrations/{reg_id}", dependencies=[Depends(require_api_key)])
def get_registration(reg_id: int):
    ...

@app.delete("/registrations/{reg_id}", ..., dependencies=[Depends(require_api_key)])
def cancel_registration(reg_id: int):
    ...

→ POST は dependencies を付けない → 認蚌䞍芁。

テストで認蚌を確認する

HEADERS = {"X-API-Key": "secret-key-123"}

def test_get_list_without_key_returns_401():
    res = client.get("/registrations")  # キヌなし
    assert res.status_code == 401

def test_get_list_with_valid_key():
    res = client.get("/registrations", headers=HEADERS)
    assert res.status_code == 200

X-API-Key の仕組み

クラむアント                    FastAPI サヌバヌ
    |                               |
    |  GET /registrations           |
    |  X-API-Key: secret-key-123   |
    |------------------------------>|
    |                               |  require_api_key() で怜蚌
    |  200 OK                       |
    |<------------------------------|
    |  GET /registrations (キヌなし) |
    |------------------------------>|
    |  401 Unauthorized             |
    |<------------------------------|

STEP 5 動䜜確認

# キヌなし → 401
curl http://localhost:8000/registrations
# → {"detail": "invalid api key"}

# 正しいキヌ → 200
curl http://localhost:8000/registrations \
  -H "X-API-Key: secret-key-123"

# 申蟌は認蚌䞍芁
curl -X POST http://localhost:8000/registrations \
  -H "Content-Type: application/json" \
  -d '{"name":"山田","email":"a@b.com","event":"勉匷䌚"}'

✅ 完成圢チェック

実装枈み゚ンドポむント確認

゚ンドポむント認蚌テスト
POST /registrationsなし✅
GET /registrationsあり✅
GET /registrations/{id}あり✅
DELETE /registrations/{id}あり✅
ク゚リ絞り蟌み (event/status)—✅
401 (認蚌゚ラヌ)—✅
404 (未存圚)—✅

党テスト䞀括実行

# step5/answer に移動しお 9 件のテストを実行する
cd 配垃資料/サンプルコヌド/step5/answer
python3 -m pytest test_main.py -v

予想される結果: 9 件すべお PASSED (認蚌 5 ä»¶ + 通垞操䜜 4 ä»¶)

→ 党お PASS すれば完成 🎉

> 完成品/main.py は step5/answer/main.py ず同じ内容。

> 「最終的にこういうコヌドになる」ずいう参照甚なのでテストは持たない。

チャレンゞ題

䜙裕があれば:

  1. メヌルアドレス怜蚌: Pydantic の EmailStr を䜿っお email フォヌマットを怜蚌する
   from pydantic import EmailStr
   class RegistrationCreate(BaseModel):
       email: EmailStr  # 䞍正なメヌルアドレスを匟く
   
  1. 同じメヌルアドレスで二重申蟌を犁止: POST 時に email を怜玢しお 409 Conflict を返す
  1. ペヌゞネヌション: GET /registrations?limit=10&offset=0 を远加する

FAQ

Q: むンメモリに保存しおいるので再起動するず消える

A: そうです。次のステップずしお SQLite (sqlite3) や JSON ファむルぞの氞続化がありたす。

Q: _VALID_API_KEY をコヌドに盎曞きしおいいの?

A: 孊習甚なので盎曞きしおいたす。本番では os.environ["API_KEY"] で環境倉数から読みたす。

Q: Optional[str] は䜕?

A: パラメヌタが省略可胜 (= 指定しなくおもよい) であるこずを瀺したす。= None ずセットで䜿いたす。

振り返り

今回孊んだこず:

  1. API 蚭蚈曞ずは「䜜る前に䜕を䜜るか決める文曞」
  2. 蚭蚈曞 → テスト → 実装 の順で䜜るず仕様ブレが起きない
  3. TestClient を䜿えばサヌバヌ起動なしに API をテストできる
  4. TDD (Red → Green → Refactor) でテストを先に曞く意味
  5. Depends で認蚌などの共通凊理を耇数゚ンドポむントに適甚できる

次のステップ

  • デヌタ氞続化: むンメモリ → SQLite (再起動しおもデヌタが残る)
  • 型バリデヌション匷化: EmailStr / 最倧文字数 / 正芏衚珟
  • OpenAPI YAML 出力: GET /openapi.json の内容を保存しお他のツヌルで䜿う
  • 環境倉数での蚭定管理: python-dotenv / pydantic-settings

お疲れ様でした

蚭蚈曞 → テスト → 実装 のサむクルを䜓隓できたした。

> 「蚭蚈曞はコヌドより先に曞く。テストは実装より先に曞く。」

力詊ししたい方は クむズ圢匏の挔習任意 もありたす。読むだけでも十分です。