第8章「1MBの崩壊をテックリードが救った夜」― FastAPI 15,000行 → 9,949行のリファクタリング実録
この記事は、個人開発サービス CheckMe(checkme.run) の開発物語です。
状況:1ファイル15,000行・1MB超のバケモノ
$ wc -l app/main.py
15352 app/main.py
$ du -h app/main.py
1.1M app/main.py
FastAPIで書かれた単一Pythonファイル。中身は:
- AIプロンプト 54個(ハードコード)
- ツールロジック 60本分
- デッキデータ 106個
- 全設定値
すべてが main.py 一箇所に「詰め込まれていた」。
テックリードの診断
「これ、誰も読めないし直せません」
エラーが出たとき、どのプロンプトが原因か特定するのに5分かかる。新機能を足すたびにどこかが壊れるリスクが上がる。このまま続けると自重で崩壊する。
リファクタリングの方針
「データはコードに書かない。ロジックだけ残す。繰り返しは1回だけ書く。」
3日間の突貫工事で実施した4つ:
① AIプロンプト 54個をSQLiteに移行
# Before(ハードコード)
_SYSTEM = '''あなたは古文の専門家です。...(500文字)...'''
# After(DBから取得)
system = _P['prompt_kobun_system'] # _P はサービス起動時にDBからロード
SQLiteの site_config テーブルに移行することで:
- deploy不要でプロンプト更新できる
- プロンプトの変更履歴が残る
- n8n から自動更新できる
② データを config/*.json に分離
app/config/
├── tool_meta.json (60ツール分のメタ情報)
├── pokemon_types.json (ポケモンタイプデータ)
├── kobun_themes.json (古文テーマ一覧)
├── chat_themes.json (AI会話テーマ)
└── ...(全14ファイル)
# Before
_MY_THEMES = ["春", "夏", "秋", "冬"]
# After
_MY_THEMES = _load_config('my_themes.json', [])
③ _ai_call() 共通ラッパーへの集約
Before:各エンドポイントで重複していた処理
# 毎回書いていたボイラープレート(60箇所以上)
cached = await asyncio.to_thread(_cache_get, "xxx_cache", key)
if cached is not None:
if not isinstance(cached, dict):
await asyncio.to_thread(_cache_delete, "xxx_cache", key)
else:
cached["cached"] = True
return JSONResponse(cached)
_log_ai_call("xxx", "/xxx/xxx")
try:
msg = await asyncio.to_thread(
_anthropic.messages.create,
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": text}]
)
data = _extract_json(msg.content[0].text)
await asyncio.to_thread(_cache_set, "xxx_cache", key, data)
return JSONResponse(data)
except Exception as e:
return _handle_ai_error("xxx", e)
After:_ai_call() 1行に集約
return await _ai_call(
tool="kobun", path="/words/kobun",
table="kobun_cache", key=_cache_key(text),
prompt=text, system_key="prompt_kobun_system",
)
④ レスポンスヘルパーの統一
# Before
return JSONResponse({"error": "入力が不正です。"}, status_code=400)
return templates.TemplateResponse(request, "words/kobun.html", {"key": val})
# After
return _bad_request()
return _t(request, "words/kobun.html", {"key": val})
結果
Before: 15,352行 / 1.1MB
After: 9,949行 / 450KB
削減: 5,403行(35%削減)
所要時間: 3日間
この章のエンジニア向けポイント
-
main.pyがどんどん肥大化するのはFastAPI個人開発あるある - 「動いてるから触らない」は技術的負債が膨らむだけ
- データとロジックを分離するだけで可読性は劇的に上がる
- 共通ラッパーへの集約は「同じ処理を3回書いたら関数化」を徹底すれば防げる
- SQLiteをプロンプトストアとして使うのは個人開発で費用対効果が高い
これで8章完結。次章以降では、このシステムが月間10万PVを目指して動き続ける話を書いていく予定です。