Install any skill in seconds. Free to start, no credit card required.
Get Started Free →FastAPI patterns for async APIs, dependency injection, Pydantic request and response models, OpenAPI docs, tests, security, and production readiness.
.claude/skills/affaan-m-fastapi-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 73% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 168% | 0% |
| case-09 | ✓→✗ | ▼ Worse | 62% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 68% | 0% |
本番指向のFastAPIサービスのためのパターン。
FastAPIアプリを明示的な依存関係とサービスコードの上の薄いHTTPレイヤーとして扱います:
main.py はアプリ構築、ミドルウェア、例外ハンドラー、ルーター登録を担当する。schemas/ はPydanticのリクエストとレスポンスモデルを担当する。dependencies.py はデータベース、認証、ページネーション、リクエストスコープの依存関係を担当する。services/ または crud/ はビジネスと永続化操作を担当する。tests/ は本番リソースを開かずに依存関係をオーバーライドする。小さなルーターと明示的なresponse_model宣言を優先します。レスポンススキーマには生のORMオブジェクト、シークレット、フレームワークのグローバル変数を含めないでください。
textapp/ |-- main.py |-- config.py |-- dependencies.py |-- exceptions.py |-- api/ | `-- routes/ | |-- users.py | `-- health.py |-- core/ | |-- security.py | `-- middleware.py |-- db/ | |-- session.py | `-- crud.py |-- models/ |-- schemas/ `-- tests/
テストとワーカーが制御された設定でアプリをビルドできるように、ファクトリーを使用します。
pythonfrom contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api.routes import health, users from app.config import settings from app.db.session import close_db, init_db from app.exceptions import register_exception_handlers @asynccontextmanager async def lifespan(app: FastAPI): await init_db() yield await close_db() def create_app() -> FastAPI: app = FastAPI( title=settings.api_title, version=settings.api_version, lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=bool(settings.cors_origins), allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], allow_headers=["Authorization", "Content-Type"], ) register_exception_handlers(app) app.include_router(health.router, prefix="/health", tags=["health"]) app.include_router(users.router, prefix="/api/v1/users", tags=["users"]) return app app = create_app()
allow_credentials=Trueと一緒にallow_origins=["*"]を使用しないでください; ブラウザはその組み合わせを拒否し、Starletteは認証情報付きリクエストに対してそれを禁止します。
リクエスト、更新、レスポンスのモデルを分離します。
pythonfrom datetime import datetime from typing import Annotated from uuid import UUID from pydantic import BaseModel, ConfigDict, EmailStr, Field class UserBase(BaseModel): email: EmailStr full_name: Annotated[str, Field(min_length=1, max_length=100)] class UserCreate(UserBase): password: Annotated[str, Field(min_length=12, max_length=128)] class UserUpdate(BaseModel): email: EmailStr | None = None full_name: Annotated[str | None, Field(min_length=1, max_length=100)] = None class UserResponse(UserBase): model_config = ConfigDict(from_attributes=True) id: UUID created_at: datetime updated_at: datetime
レスポンスモデルにはパスワードハッシュ、アクセストークン、リフレッシュトークン、内部認可状態を含めてはなりません。
リクエストスコープのリソースには依存性注入を使用します。
pythonfrom collections.abc import AsyncIterator from uuid import UUID from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import decode_token from app.db.session import session_factory from app.models.user import User oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") async def get_db() -> AsyncIterator[AsyncSession]: async with session_factory() as session: try: yield session await session.commit() except Exception: await session.rollback() raise async def get_current_user( token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db), ) -> User: payload = decode_token(token) user_id = UUID(payload["sub"]) user = await db.get(User, user_id) if user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") return user
ルートハンドラー内でインラインにセッション、クライアント、または認証情報を作成しないでください。
I/Oを実行する場合はルートハンドラーを非同期にし、その内部で非同期ライブラリを使用します。
pythonfrom fastapi import APIRouter, Depends, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db from app.models.user import User from app.schemas.user import UserResponse router = APIRouter() @router.get("/", response_model=list[UserResponse]) async def list_users( limit: int = Query(default=50, ge=1, le=100), offset: int = Query(default=0, ge=0), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(User).order_by(User.created_at.desc()).limit(limit).offset(offset) ) return result.scalars().all()
非同期ハンドラーからの外部HTTP呼び出しにはhttpx.AsyncClientを使用してください。非同期ルートでrequestsを呼び出さないでください。
ドメイン例外を一元化し、レスポンスの形状を安定させます。
pythonfrom fastapi import FastAPI, Request from fastapi.responses import JSONResponse class ApiError(Exception): def __init__(self, status_code: int, code: str, message: str): self.status_code = status_code self.code = code self.message = message def register_exception_handlers(app: FastAPI) -> None: @app.exception_handler(ApiError) async def api_error_handler(request: Request, exc: ApiError): return JSONResponse( status_code=exc.status_code, content={"error": {"code": exc.code, "message": exc.message}}, )
カスタムOpenAPI呼び出し可能オブジェクトをapp.openapiに割り当ててください; 関数を一度だけ呼び出さないでください。
pythonfrom fastapi import FastAPI from fastapi.openapi.utils import get_openapi def install_openapi(app: FastAPI) -> None: def custom_openapi(): if app.openapi_schema: return app.openapi_schema app.openapi_schema = get_openapi( title="Service API", version="1.0.0", routes=app.routes, ) return app.openapi_schema app.openapi = custom_openapi
ルートハンドラーが決して参照しない内部ヘルパーではなく、Dependsで使用される依存関係をオーバーライドします。
pythonimport pytest from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_db from app.main import create_app @pytest.fixture async def client(test_session: AsyncSession): app = create_app() async def override_get_db(): yield test_session app.dependency_overrides[get_db] = override_get_db async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", ) as test_client: yield test_client app.dependency_overrides.clear()
argon2-cffi、bcrypt、または現在のpasslib互換ハッシャーでパスワードをハッシュする。これらの例はプロジェクト全体のテンプレートではなく、パターンとして使用してください:
create_appでミドルウェアとルーターを一度設定する。UserCreate、UserUpdate、UserResponseはそれぞれ異なる責務を持つ。get_dbを直接オーバーライドする。app.openapi = custom_openapiを割り当てる。fastapi-reviewer/fastapi-reviewpython-patternspython-testingapi-design| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-07 | fail→pass | 12,548 | 10,550 | -16% | 1 | 1 | 0% | 2,800 | 4,853 | +73% | 0 | 0 | — |
case-01 | fail→pass | 12,694 | 11,559 | -9% | 1 | 1 | 0% | 2,871 | 5,264 | +83% | 0 | 0 | — |
case-02 | pass→pass | 12,419 | 7,226 | -42% | 1 | 1 | 0% | 2,385 | 4,002 | +68% | 0 | 0 | — |
case-03 | pass→pass | 8,588 | 8,437 | -2% | 1 | 1 | 0% | 1,867 | 4,299 | +130% | 0 | 0 | — |
case-04 | pass→pass | 11,822 | 10,219 | -14% | 1 | 1 | 0% | 2,344 | 4,955 | +111% | 0 | 0 | — |
case-05 | pass→pass | 7,845 | 5,515 | -30% | 1 | 1 | 0% | 1,567 | 3,680 | +135% | 0 | 0 | — |
case-06 | pass→pass | 13,701 | 8,225 | -40% | 1 | 1 | 0% | 2,994 | 4,271 | +43% | 0 | 0 | — |
case-08 | pass→pass | 7,893 | 4,596 | -42% | 1 | 1 | 0% | 1,625 | 3,488 | +115% | 0 | 0 | — |
case-09 | pass→fail | 13,975 | 11,777 | -16% | 1 | 1 | 0% | 3,134 | 5,078 | +62% | 0 | 0 | — |
case-10 | pass→pass | 9,053 | 9,581 | +6% | 1 | 1 | 0% | 1,706 | 4,465 | +162% | 0 | 0 | — |
case-11 | pass→pass | 8,916 | 6,346 | -29% | 1 | 1 | 0% | 1,719 | 3,812 | +122% | 0 | 0 | — |
case-12 | pass→pass | 6,895 | 3,557 | -48% | 1 | 1 | 0% | 1,377 | 3,344 | +143% | 0 | 0 | — |
case-13 | pass→pass | 10,706 | 9,899 | -8% | 1 | 1 | 0% | 1,969 | 4,260 | +116% | 0 | 0 | — |
case-14 | pass→pass | 7,619 | 6,154 | -19% | 1 | 1 | 0% | 1,573 | 3,878 | +147% | 0 | 0 | — |
case-15 | pass→pass | 18,033 | 15,907 | -12% | 1 | 1 | 0% | 3,787 | 5,895 | +56% | 0 | 0 | — |
case-16 | fail→pass | 7,949 | 8,875 | +12% | 1 | 1 | 0% | 1,692 | 4,530 | +168% | 0 | 0 | — |
case-17 | pass→pass | 12,274 | 15,596 | +27% | 1 | 1 | 0% | 2,611 | 6,101 | +134% | 0 | 0 | — |
case-18 | pass→pass | 9,678 | 5,465 | -44% | 1 | 1 | 0% | 1,951 | 3,692 | +89% | 0 | 0 | — |
case-19 | pass→pass | 10,363 | 8,061 | -22% | 1 | 1 | 0% | 2,082 | 4,251 | +104% | 0 | 0 | — |
case-20 | pass→pass | 10,196 | 12,254 | +20% | 1 | 1 | 0% | 2,287 | 5,329 | +133% | 0 | 0 | — |
case-21 | pass→pass | 12,186 | 12,849 | +5% | 1 | 1 | 0% | 2,586 | 5,351 | +107% | 0 | 0 | — |
case-22 | pass→pass | 10,869 | 10,338 | -5% | 1 | 1 | 0% | 2,289 | 4,578 | +100% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.