building-python-web-apps について
Building Python Web Apps は、Python で小〜中規模の SaaS アプリを作る個人開発者や小規模チーム向けのスキルです。再利用しやすい FastAPI の構成、アプリ設計、認証、課金、バックグラウンド処理、デプロイの指針をまとめています。
主な機能
- 01FastAPI を中心としたアプリ設計
- 02ルート・サービス・モデルの階層化
- 03データベース・設定・認証のパターン
- 04Stripe 課金と Webhook の指針
- 05Docker・Terraform によるデプロイ設計
活用シーン
- 01小規模 SaaS アプリの立ち上げ
- 02アプリ構成と標準スタックの選定
- 03課金と Webhook 処理の実装
- 04デプロイとバックグラウンド処理の計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wdm0006/ python-skills --skill building-python-web-appsインストール後、Claude Code で/building-python-web-appsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AUTH.mdBACKGROUND_JOBS.mdDEPLOYMENT.mdFRONTEND.mdPAYMENTS.mdSTACK.md詳細
Python Web アプリの構築
小規模から中規模の SaaS アプリを、個人または少人数のチームでリリースするための、独自の方針に基づく必要な機能をすべて備えた設計図です。定番でよく理解されている要素を、毎回同じ方法で組み合わせることを重視し、新しいアプリで「認証+課金+デプロイ」を数週間ではなく数時間で実現できます。
デフォルトのスタック
| 項目 | デフォルト |
|---|---|
| フレームワーク | FastAPI(非同期)、uvicorn[standard] |
| パッケージング | uv + pyproject.toml + uv.lock、hatchling、src/ レイアウト |
| データベース | Postgres 16 + asyncpg、SQLAlchemy 2.0 非同期、Alembic |
| 設定 | pydantic-settings BaseSettings、環境変数プレフィックス、@lru_cache |
| 決済 | Stripe ≥15、署名付き Webhook を 1 つ、冪等性テーブル |
| 認証 | 依存関係によるガード + bcrypt。トークン方式はアプリごとに決定(AUTH.md を参照) |
| フロントエンド | Jinja2 + Tailwind (CDN) + Alpine.js、または 分離された SPA |
| バックグラウンド処理 | cron-as-worker(同じイメージでコマンドを変更)— Celery は使いません |
| 可観測性 | Sentry + PostHog。どちらもキーを設定するまで何もしません |
| デプロイ | Docker → Terraform 経由でマネージドプラットフォーム(Render 形式) |
| Lint/テスト | ruff + pytest + pytest-asyncio、Makefile のエントリーポイント |
変更する理由がなければ、各項目をそのまま使ってください。このスキルの残りの部分では「方法」を説明し、参照ファイルにはコピーして使えるテンプレートを用意しています。
パッケージ構成
レイヤー構造のパッケージを 1 つ用意し、ルーターをドメインごとに分割します。ビジネスロジックをルートに置くことはありません。
src/myapp/
main.py # app + middleware + include_router() in a loop
config.py # pydantic-settings Settings + get_settings()
db.py # lazy async engine/sessionmaker + get_session() dep
models.py # SQLAlchemy 2.0 DeclarativeBase, Mapped[]
schemas.py # Pydantic request/response models
security.py # hashing, tokens, auth dependencies
observability.py # Sentry/PostHog init (guarded)
api/ # one APIRouter module per domain (+ deps.py, webhooks.py)
services/ # business logic; external SDKs isolated here
migrations/versions/ # Alembic
tests/ # pytest, mirrors api/ + services/
terraform/ # render.tf, stripe.tf, posthog.tf, variables.tf
Dockerfile docker-compose.yml pyproject.toml uv.lock alembic.ini Makefile
ルート → サービス → モデル。 ルートは解析と検証を行い、サービスを呼び出します。サービスはロジックを保持し、外部 SDK の呼び出し(Stripe、メール、LLM)をすべて担うため、モック可能です。モデルは永続化のみを担当します。
アプリの配線
モジュールレベルの app(ファクトリーでも構いませんが、必須ではありません)、ループ内で登録するルーター、ミドルウェアとしての横断的関心事、そして DB に到達可能であることを確認するヘルスチェックを用意します。完全なテンプレートは STACK.md を参照してください。
# main.py (shape)
app = FastAPI(docs_url=None if settings.is_prod else "/docs")
app.add_middleware(CORSMiddleware, allow_origins=settings.cors_origins, ...)
# request-id + security-headers middleware, slowapi rate limiter
for router in (auth.router, billing.router, webhooks.router, ...):
app.include_router(router)
@app.get("/health")
async def health(db: SessionDep):
await db.execute(text("SELECT 1")) # 503 if the DB is down
return {"status": "ok"}
依存関係は Annotated 型エイリアスとして共有すると、シグネチャがすっきりします。
# api/deps.py
SessionDep = Annotated[AsyncSession, Depends(get_session)]
UserDep = Annotated[User, Depends(require_user)]
データベース
遅延初期化される非同期エンジンのシングルトン、pool_pre_ping=True、expire_on_commit=False、get_session() 非同期ジェネレーター依存関係、そしてプラットフォームが発行する postgres:// URL を postgresql+asyncpg:// に変換する URL 正規化処理を用意します。本番環境では Postgres、高速なローカル/CI テストでは SQLite(aiosqlite)を使います。 マイグレーションには Alembic を使い、デプロイの事前デプロイ手順で適用します。STACK.md を参照してください。
設定とシークレット
環境変数プレフィックスを持つ Settings(BaseSettings) を 1 つ用意し、@lru_cache get_settings() を使います。また、本番環境でデフォルトのシークレットまたはシークレットが未設定の場合に起動を拒否するバリデーターを設定します。.env.example はコミットし、.env は絶対にコミットしないでください。計算済みの @property ヘルパー(is_prod、cors_origins、billing_enabled)を使うと、ルートをすっきり保てます。
決済
Stripe では、署名検証を行う Webhook エンドポイントを必ず 1 つだけ設け、永続化されたイベントテーブルで冪等性を担保し、未知のイベントを受け取っても 500 エラーを返さないイベントタイプ別のディスパッチを実装します。SDK の呼び出しはすべて services/stripe/ モジュールに分離します。完全な実装パターン(チェックアウト、サブスクリプション、Webhook ハンドラー、Terraform でプロビジョニングする署名シークレット)については、PAYMENTS.md を参照してください。
実際に選択する項目
上記はすべて固定です。アプリごとに実際に選択するのは、次の項目です。
- フロントエンド — サーバーサイドでレンダリングする Jinja + Tailwind/Alpine(JS のビルドが不要で、迅速にリリースできます) または 独自のオリジンで動作する、分離型の React/Vite SPA(より豊かな UI を実現できますが、構成要素が増えます)。 → FRONTEND.md
- 認証方式 — cookie-JWT(ブラウザーアプリ)、bearer-JWT/API-key(プログラムからの利用)、 magic-link(パスワードレス)、または OAuth(委任型)。bcrypt と依存関係のガードは 共通で、異なるのはトークンのみです。→ AUTH.md
- バックグラウンド処理 — リクエスト内でインライン実行(最もシンプル)、cron-as-worker(定期処理やキュー処理の デフォルト)、または Redis ベースのジョブキュー(ロングポーリングジョブ)。 これら3つでは対応できなくなった場合に限り、Celery を選択してください。→ BACKGROUND_JOBS.md
- LLM 機能 — 構造化された Pydantic 出力を備えた
pydantic-ai。起動時にキーが必要で、 モデルは環境変数で設定できます。補足は FRONTEND.md にあります。
デプロイ
単一の Docker イメージ(astral-sh/uv ベース、レイヤーキャッシュに対応した2段階の uv sync、非 root ユーザー、uvicorn --proxy-headers)。同じイメージを、異なるコマンドでウェブ、ワーカー、マイグレーションに使用します。マネージドプラットフォームを対象とし、Terraform(アプリ + Postgres + Stripe + PostHog をコードで管理)でプロビジョニングします。フォールバックとして、プラットフォームの Blueprint ファイルをドキュメントに記載します。マイグレーションはデプロイ前のステップで実行します。シークレットは Terraform で生成するか、同期対象外として指定し、別途設定します。→ DEPLOYMENT.md
レビューチェックリスト
Structure:
- [ ] Logic in services/, not in route handlers
- [ ] External SDKs (Stripe/email/LLM) isolated behind a service module
- [ ] /health (or /readyz) actually pings the database
- [ ] Job status: failure writes its own terminal value, "in progress" is written
before the work is handed off, and the current value is read by monotonic id
Data:
- [ ] Async engine is a lazy singleton with pool_pre_ping
- [ ] postgres:// URLs normalized to postgresql+asyncpg://
- [ ] Every schema change has an Alembic migration
Config:
- [ ] Production refuses to boot on a default/missing secret
- [ ] CORS not "*" and HTTPS enforced in production
- [ ] .env.example committed; .env gitignored
Billing & security:
- [ ] Exactly one Stripe webhook, signature-verified, idempotent
- [ ] New passwords centrally limited to 72 UTF-8 bytes before bcrypt hashing
- [ ] Password verification remains unbounded for compatibility with existing hashes
- [ ] Passwords bcrypt-hashed; auth via reusable dependencies
- [ ] CSRF enforced globally for cookie auth (not per-form opt-in); no
state-changing GET requests — mutations use POST/DELETE
- [ ] Client IP for rate-limit keys/allowlists/audit logs taken from a fixed
position in the X-Forwarded-For chain (not the spoofable left-most value)
- [ ] Authentication rate limits use a required shared backend; backend outages
fail closed with 503, while healthy exhausted buckets return 429 + actual TTL
- [ ] Private OpenAPI schema served only by a custom guarded route (`openapi_url=None`)
- [ ] Sentry + PostHog init guarded (no-op when unkeyed, never break a request)
Ship:
- [ ] One Docker image; web/worker/migrate are just different commands
- [ ] Migrations run pre-deploy, not at app startup in prod
- [ ] Infra is in Terraform; secrets are not committed
参考ファイル
- STACK.md — pyproject、
main.py、db.py、config.py、Makefile のテンプレート - PAYMENTS.md — Stripe のチェックアウトと冪等性を備えた webhook のパターン
- AUTH.md — 4つのトークン方式と共通の依存関係ガード
- BACKGROUND_JOBS.md — インライン実行、cron-worker、Redis キューの比較
- FRONTEND.md — Jinja/Tailwind/Alpine と分離型 SPA の比較(+ LLM に関する補足)
- DEPLOYMENT.md — Dockerfile、Terraform、マイグレーション、シークレット