JetBrains Air+CodexでFastAPIのAPI追加とテストまで進める
はじめに
前回は、Windows 11にJetBrains Airを入れ、Codexへ小さなPython修正を依頼するところまで確認しました。
今回は少し範囲を広げます。FastAPIとDocker Composeで動くスターター版を用意し、Air上のCodexへ次の作業をまとめて依頼します。
- 降雨集計APIの追加
- Pydanticによる入力チェック
- サービス関数の修正
- pytestによる単体テストとAPIテスト
- Docker Composeのテスト用サービス追加
- READMEの更新
いきなり実務プロジェクトへ適用するのではなく、まずは小さなAPIで「依頼、Plan確認、実装、テスト、差分確認」の流れを一通り試します。
この記事は2026年7月27日時点のJetBrains Airを前提にしています。
今回作るもの
入力された時間雨量から、合計値、最大値、平均値を返すAPIを作ります。
追加するAPIは次のとおりです。
POST /api/v1/rainfall/summary
リクエスト例です。
{
"rainfall_mm": [0.0, 2.5, 10.0, 5.5]
}
レスポンスは次の形にします。
{
"total_mm": 18.0,
"max_mm": 10.0,
"average_mm": 4.5
}
使用する環境
| 項目 | 内容 |
|---|---|
| OS | Windows 11 |
| 開発環境 | JetBrains Air |
| エージェント | OpenAI Codex |
| 実行環境 | Git Worktree |
| Python | 3.13 |
| Web API | FastAPI |
| コンテナ | Docker Desktop / Docker Compose |
| テスト | pytest / FastAPI TestClient |
Airでは、タスクごとにエージェント、権限モード、実行環境を選べます。今回は実装前に内容を確認したいためPlanを使い、元の作業フォルダーを直接変更しないようにGit Worktreeを選びます。
配布ファイル
配布用ZIPファイルには、次の2つを入れています。
air_codex_fastapi_docker_r001/
├─ starter/ Codexへ渡す前の状態
├─ completed/ APIとテストを追加した完成版
├─ task_prompts/ Codexへ渡すタスク文
└─ docs/ Qiita記事
Airで試すときはstarterを使用します。completedは、Codexの変更結果を確認するときの比較用です。
1. スターター版を確認する
スターター版には、FastAPIの起動に必要な最低限のファイルだけを置いています。
starter/
├─ app/
│ ├─ services/
│ │ └─ rainfall.py
│ └─ main.py
├─ task_prompts/
│ └─ codex_add_api_and_tests.md
├─ AGENTS.md
├─ compose.yaml
├─ Dockerfile
├─ requirements.txt
└─ README.md
この段階で用意されているAPIは、ヘルスチェックだけです。
@app.get("/health", tags=["system"])
def health_check() -> dict[str, str]:
"""コンテナとAPIプロセスが稼働していることを返す。"""
return {"status": "ok"}
降雨集計の関数はありますが、空のリストと負の雨量を安全に扱えません。
def summarize_rainfall(rainfall_mm: list[float]) -> dict[str, float]:
total_mm = sum(rainfall_mm)
return {
"total_mm": total_mm,
"max_mm": max(rainfall_mm),
"average_mm": total_mm / len(rainfall_mm),
}
空のリストを渡すとmax()または割り算で例外になります。負の雨量もそのまま計算されます。今回は、この部分もAPI追加と一緒にCodexへ直してもらいます。
2. 先にDocker Composeで起動する
PowerShellでstarterへ移動します。
cd C:\work\air_codex_fastapi_docker_r001\starter
Docker Composeを起動します。
docker compose up --build
ブラウザーで次のURLを開きます。
http://localhost:8000/docs
この段階では、Swagger UIにGET /healthだけが表示されます。
ヘルスチェックはPowerShellからも確認できます。
Invoke-RestMethod http://localhost:8000/health
結果は次のとおりです。
status
------
ok
確認後は停止します。
docker compose down
3. Gitリポジトリを準備する
Git Worktreeを使うため、Airで開く前にGitリポジトリを作成し、初期状態をコミットします。
スターター版には準備用スクリプトを入れています。
.\prepare_repository.ps1
手作業で行う場合は次のコマンドです。
git init
git add .
git commit -m "Create FastAPI starter project"
最後に状態を確認します。
git status
次の表示になれば準備完了です。
nothing to commit, working tree clean
未コミットの変更が残っていると、Codexが変更した部分との区別がつきにくくなります。最初の状態を必ずコミットしてからAirで開きます。
4. Airでスターター版を開く
JetBrains Airを起動し、starterフォルダーを開きます。
C:\work\air_codex_fastapi_docker_r001\starter
今回の設定は次のとおりです。
| 設定 | 選択内容 |
|---|---|
| Agent | OpenAI Codex |
| Permission Mode | Plan |
| Run Environment | Git Worktree |
Planを選ぶと、Codexはいきなりファイルを変更せず、最初に実装計画を作ります。Git Worktreeでは、タスク専用のブランチと作業フォルダーが作られるため、現在の作業コピーは変更されません。
Airの公式ドキュメントでも、Planモードは実装前の計画確認、Git Worktreeは元の作業コピーから変更を分離する用途として説明されています。
5. AGENTS.mdで作業ルールを固定する
スターター版にはAGENTS.mdを置いています。
# 開発ルール
- Python 3.13で動作するコードにする。
- ソースコードはUTF-8で保存する。
- 公開関数と公開クラスには日本語のdocstringを付ける。
- コメントは判断理由や注意点が必要な箇所に記述する。
- APIのURLとレスポンス項目は依頼内容を優先する。
- 実装後はpytestを実行する。
- Docker Composeによるテストも実行する。
- 今回の目的に関係しないリファクタリングは行わない。
- 不要になったコードは残さず削除する。
タスク本文だけでも指示できますが、毎回守ってほしいルールはAGENTS.mdへ分けた方が管理しやすくなります。
6. Codexへ渡すタスク
AirのChatへ、次のタスクを貼り付けます。
このFastAPIプロジェクトに、降雨集計APIと自動テストを追加してください。
現在の状態:
- GET /health は実装済みです。
- app/services/rainfall.py に summarize_rainfall() があります。
- 降雨集計用のAPIとテストはまだありません。
- Docker ComposeではAPIサービスだけを起動できます。
追加するAPI:
- メソッド: POST
- URL: /api/v1/rainfall/summary
リクエスト例:
{
"rainfall_mm": [0.0, 2.5, 10.0, 5.5]
}
レスポンス例:
{
"total_mm": 18.0,
"max_mm": 10.0,
"average_mm": 4.5
}
完了条件:
1. POST /api/v1/rainfall/summary を追加する。
2. 入出力にはPydanticモデルを使う。
3. rainfall_mm が空の場合はHTTP 422を返す。
4. 負の雨量を含む場合はHTTP 422を返す。
5. summarize_rainfall([]) は理由の分かるValueError系例外を送出する。
6. summarize_rainfall() に負の雨量を渡した場合も理由の分かる例外を送出する。
7. 正常な入力では既存の辞書キーを変えずに集計結果を返す。
8. pytestでサービス単体テストとAPI結合テストを追加する。
9. GET /health、正常な降雨集計、空リスト、負の雨量をテストする。
10. Dockerfileにテスト用ステージを追加する。
11. Composeにテスト用サービスを追加する。
12. 次のコマンドでテストが成功するようにする。
docker compose --profile test run --rm --build test
13. READMEへAPIの呼び出し方とテスト方法を追記する。
変更時の条件:
- Python 3.13を使用する。
- GET /health のURLとレスポンスは変更しない。
- summarize_rainfall の関数名と引数名を変更しない。
- total_mm、max_mm、average_mm のキー名を変更しない。
- APIのバージョンは /api/v1 とする。
- 公開関数と公開クラスには日本語のdocstringを付ける。
- コメントは判断理由が必要な箇所にだけ追加する。
- 今回の目的に関係しないリファクタリングは行わない。
- 実装後にローカルpytestとDocker Composeのテストを実行し、結果を報告する。
まずPlanモードで、変更対象ファイル、実装手順、確認方法を整理してください。
Planを承認する前にファイルを変更しないでください。
同じ内容をtask_prompts/codex_add_api_and_tests.mdにも保存しています。
Airでは、入力欄で@を使うとファイルやフォルダーをコンテキストへ追加できます。今回は次の項目を添付しておくと調査範囲がぶれにくくなります。
@app
@compose.yaml
@Dockerfile
@README.md
@AGENTS.md
7. Planで確認する内容
タスクを送ると、Codexが変更対象と実装手順を整理します。
想定されるPlanは次のような内容です。
1. 既存のFastAPI構成と降雨集計関数を確認する
2. リクエスト・レスポンス用のPydanticモデルを作る
3. 降雨集計用のAPIRouterを追加する
4. main.pyへルーターを登録する
5. サービス関数へ入力チェックを追加する
6. pytestでサービス単体テストを作る
7. TestClientを使ったAPIテストを作る
8. Dockerfileへテストステージを追加する
9. compose.yamlへテストサービスを追加する
10. READMEを更新する
11. ローカルとDocker Composeでテストする
Planでは、次の点を確認します。
-
GET /healthを変更しない計画になっているか - APIのURLが
/api/v1/rainfall/summaryになっているか - 入力チェックをAPIだけでなくサービス側にも追加するか
- 単体テストとAPIテストの両方が含まれているか
- Dockerfileの実行用イメージにテスト依存を混ぜない構成か
- 関係のないファイルを変更しようとしていないか
問題がなければ実装へ進めます。
計画に不要な変更が含まれていた場合は、この段階で直します。例えば、データベースやログ機能まで追加しようとしていたら、次のように返します。
今回は降雨集計APIとテスト追加だけを対象にしてください。
データベース、認証、ログ基盤の追加は行わないでください。
8. Codexが追加する構成
完成後の構成は次の形になります。
completed/
├─ app/
│ ├─ api/
│ │ └─ routes/
│ │ └─ rainfall.py
│ ├─ schemas/
│ │ └─ rainfall.py
│ ├─ services/
│ │ └─ rainfall.py
│ └─ main.py
├─ tests/
│ ├─ test_api.py
│ └─ test_rainfall_service.py
├─ AGENTS.md
├─ compose.yaml
├─ Dockerfile
├─ requirements.txt
├─ requirements-dev.txt
└─ README.md
API、入出力モデル、業務ロジックを分けています。小さなサンプルでは1ファイルにもできますが、次の機能追加を考えると、最初から役割を分けた方が追いやすくなります。
9. 入力チェックをPydanticへ追加する
APIの入力モデルでは、リストが1件以上であることと、各雨量が0以上であることを定義します。
from typing import Annotated
from pydantic import BaseModel, Field
NonNegativeRainfall = Annotated[
float,
Field(ge=0.0, description="0以上の時間雨量(mm)"),
]
class RainfallSummaryRequest(BaseModel):
"""降雨集計APIのリクエスト。"""
rainfall_mm: Annotated[
list[NonNegativeRainfall],
Field(min_length=1, description="集計対象の時間雨量(mm)"),
]
この定義により、空のリストや負の値を送った場合、FastAPIがHTTP 422を返します。
入力チェックをルーター内のif文だけで書くより、Swagger UIにも条件が反映されるため、APIの仕様が分かりやすくなります。
10. サービス側にも入力チェックを残す
Pydanticで検証していても、summarize_rainfall()がAPI以外から呼ばれる可能性があります。
そのため、サービス関数側でも空リストと負の値を確認します。
class RainfallValidationError(ValueError):
"""降雨量の入力値が集計条件を満たさない場合に送出する例外。"""
def summarize_rainfall(rainfall_mm: list[float]) -> dict[str, float]:
"""雨量の合計値、最大値、平均値を計算する。"""
if not rainfall_mm:
raise RainfallValidationError(
"rainfall_mmには1件以上の雨量を指定してください。"
)
if any(value < 0.0 for value in rainfall_mm):
raise RainfallValidationError(
"rainfall_mmには0以上の雨量を指定してください。"
)
total_mm = sum(rainfall_mm)
return {
"total_mm": total_mm,
"max_mm": max(rainfall_mm),
"average_mm": total_mm / len(rainfall_mm),
}
APIからの入力はPydantic、業務ロジック単体ではサービス関数が守る形です。
11. APIルーターを追加する
エンドポイントはAPIRouterへ分けます。
from fastapi import APIRouter
from app.schemas.rainfall import (
RainfallSummaryRequest,
RainfallSummaryResponse,
)
from app.services.rainfall import summarize_rainfall
router = APIRouter(prefix="/api/v1/rainfall", tags=["rainfall"])
@router.post("/summary", response_model=RainfallSummaryResponse)
def create_rainfall_summary(
request: RainfallSummaryRequest,
) -> RainfallSummaryResponse:
"""受け取った時間雨量から合計値、最大値、平均値を返す。"""
summary = summarize_rainfall(request.rainfall_mm)
return RainfallSummaryResponse(**summary)
main.pyではルーターを登録します。
from app.api.routes.rainfall import router as rainfall_router
app.include_router(rainfall_router)
ヘルスチェックはそのまま残します。
12. pytestでサービスとAPIを分けて確認する
テストは2種類に分けます。
test_rainfall_service.py
└─ 集計関数だけを確認する
test_api.py
└─ HTTPリクエストとレスポンスを確認する
サービス単体テストでは、正常値、空リスト、負の雨量を確認します。
def test_summarize_rainfall_returns_expected_values() -> None:
result = summarize_rainfall([0.0, 2.5, 10.0, 5.5])
assert result == {
"total_mm": 18.0,
"max_mm": 10.0,
"average_mm": 4.5,
}
APIテストではFastAPIのTestClientを使います。
def test_create_rainfall_summary() -> None:
response = client.post(
"/api/v1/rainfall/summary",
json={"rainfall_mm": [0.0, 2.5, 10.0, 5.5]},
)
assert response.status_code == 200
assert response.json() == {
"total_mm": 18.0,
"max_mm": 10.0,
"average_mm": 4.5,
}
空リストと負の雨量については、HTTP 422が返ることを確認します。
13. Dockerfileへテスト用ステージを追加する
完成版のDockerfileは、実行用とテスト用を分けています。
FROM python:3.13-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /workspace
COPY requirements.txt ./
RUN python -m pip install --upgrade pip \
&& python -m pip install -r requirements.txt
FROM base AS runtime
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
FROM base AS test
COPY requirements-dev.txt ./
RUN python -m pip install -r requirements-dev.txt
COPY app ./app
COPY tests ./tests
CMD ["python", "-m", "pytest", "-q"]
実行用ステージにはpytestを入れず、テスト用ステージだけに開発用依存関係を追加しています。
14. Composeへテストサービスを追加する
compose.yamlには、通常起動するapiと、必要なときだけ使うtestを定義します。
services:
api:
build:
context: .
target: runtime
ports:
- "8000:8000"
test:
profiles:
- test
build:
context: .
target: test
テストサービスはtestプロファイルへ入れています。通常のdocker compose upではAPIだけが起動します。
15. Codexにテストまで実行させる
実装後、Codexには次の2種類のテストを実行させます。
ローカルpytestです。
python -m pip install -r requirements-dev.txt
python -m pytest -q
Docker Composeによるテストです。
docker compose --profile test run --rm --build test
配布している完成版をローカルpytestで確認した結果は次のとおりです。
....... [100%]
7 passed
Codexの最終回答では、単に「テストしました」だけではなく、実行コマンド、成功件数、未確認事項を報告させます。
例えば次の形です。
実行したコマンド:
- python -m pytest -q
- docker compose --profile test run --rm --build test
結果:
- サービス単体テスト: 成功
- APIテスト: 成功
- 合計7件: 成功
- Dockerイメージのビルド: 成功
コマンドの実行結果が表示されていない場合は、追加で確認します。
実行したテストコマンドと終了コードを示してください。
未実行の確認項目があれば、その理由も書いてください。
16. Changes画面で差分を確認する
Codexの作業が終わったら、AirのChanges画面を開きます。
確認する点は次のとおりです。
API
- URLが
/api/v1/rainfall/summaryになっているか - レスポンスの3項目が変わっていないか
-
GET /healthが残っているか
入力チェック
- 空リストがHTTP 422になるか
- 負の雨量がHTTP 422になるか
- サービス関数を直接呼んだ場合も例外になるか
テスト
- 正常系だけでなく異常系もあるか
- HTTPステータスだけでなくレスポンス内容も確認しているか
- 実際のAPIルーターを通るテストになっているか
Docker
- 実行用イメージへpytestを入れていないか
-
docker compose upでテストサービスまで起動しないか - テストコマンドがREADMEに書かれているか
不要な変更があれば、該当行にコメントを付けてCodexへ返します。
17. 変更をローカルへ反映する
差分に問題がなければApply Locallyを実行します。
Git Worktree側の変更が、現在の作業フォルダーへ未コミットの変更としてコピーされます。
PowerShellで状態を確認します。
git status
想定される変更は次のとおりです。
modified: Dockerfile
modified: README.md
modified: app/main.py
modified: app/services/rainfall.py
modified: compose.yaml
modified: requirements-dev.txt
new file: app/api/routes/rainfall.py
new file: app/schemas/rainfall.py
new file: tests/test_api.py
new file: tests/test_rainfall_service.py
ローカルへ反映した後、もう一度テストします。
docker compose --profile test run --rm --build test
問題がなければコミットします。
git add .
git commit -m "Add rainfall summary API and tests"
18. Swagger UIから動作確認する
APIを起動します。
docker compose up --build
ブラウザーでSwagger UIを開きます。
http://localhost:8000/docs
POST /api/v1/rainfall/summaryを開き、次のJSONを入力します。
{
"rainfall_mm": [0.0, 2.5, 10.0, 5.5]
}
レスポンスが次の内容になれば正常です。
{
"total_mm": 18.0,
"max_mm": 10.0,
"average_mm": 4.5
}
空リストも確認します。
{
"rainfall_mm": []
}
この場合はHTTP 422になります。
負の雨量も同様です。
{
"rainfall_mm": [1.0, -0.1, 2.0]
}
こちらもHTTP 422になれば入力チェックが動いています。
19. PowerShellからAPIを呼び出す
PowerShellでは次のように実行できます。
$body = @{
rainfall_mm = @(0.0, 2.5, 10.0, 5.5)
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:8000/api/v1/rainfall/summary" `
-ContentType "application/json" `
-Body $body
結果は次の形で表示されます。
total_mm max_mm average_mm
-------- ------ ----------
18.0 10.0 4.5
まとめ
今回は、FastAPIとDocker Composeで作った小さなスターター版をAirで開き、CodexへAPI追加からテストまでまとめて依頼する流れを整理しました。
作業の順番は次のとおりです。
作業手順のまとめ
- スターター版を起動
↓ - Gitへ初期コミット
↓ - AirでCodex・Plan・Git Worktreeを選択
↓ - API仕様と完了条件を渡す
↓ - Planを確認
↓ - CodexがAPIとテストを追加
↓ - ローカルpytestとDockerテストを実行
↓ - Changes画面で差分を確認
↓ - Apply Locally
↓ - 手元でもう一度テスト
Codexへ依頼するときは、実装内容だけでなく、変更してはいけない部分とテストコマンドまで書いておくことが大切です。
今回のサンプルでは、GET /health、関数名、レスポンス項目を固定しました。このように既存仕様を明記しておくと、必要以上に変更されるのを抑えられます。
小さなAPIで流れを確認できたら、次はDuckDBへ計算結果を保存する構成や、OpenLayersからAPIを呼び出す構成へ広げられます。
参考資料
- Quickstart with Air - JetBrains Air Documentation
- Plan mode - JetBrains Air Documentation
- Task run environments - JetBrains Air Documentation
- Task context - JetBrains Air Documentation
- Accept changes - JetBrains Air Documentation
- Supported agents - JetBrains Air Documentation
- ChatGPTプランでCodexを使う - OpenAI Help Center
