| name | fastapi-templates |
| description | 非同期パターン、依存性注入、包括的なエラー処理を備えた本番環境対応のFastAPIプロジェクトを作成。新しいFastAPIアプリケーションの構築またはバックエンドAPIプロジェクトのセットアップ時に使用。 Use when this capability is needed. |
| metadata | {"author":"amurata"} |
English | 日本語
FastAPIプロジェクトテンプレート
非同期パターン、依存性注入、ミドルウェア、高性能APIを構築するためのベストプラクティスを備えた本番環境対応のFastAPIプロジェクト構造。
このスキルを使用する場面
- ゼロから新しいFastAPIプロジェクトを開始
- Pythonで非同期REST APIを実装
- 高性能Webサービスとマイクロサービスの構築
- PostgreSQL、MongoDBを使用した非同期アプリケーションの作成
- 適切な構造とテストを備えたAPIプロジェクトのセットアップ
コアコンセプト
1. プロジェクト構造
推奨レイアウト:
app/
├── api/ # APIルート
│ ├── v1/
│ │ ├── endpoints/
│ │ │ ├── users.py
│ │ │ ├── auth.py
│ │ │ └── items.py
│ │ └── router.py
│ └── dependencies.py # 共有依存関係
├── core/ # コア設定
│ ├── config.py
│ ├── security.py
│ └── database.py
├── models/ # データベースモデル
│ ├── user.py
│ └── item.py
├── schemas/ # Pydanticスキーマ
│ ├── user.py
│ └── item.py
├── services/ # ビジネスロジック
│ ├── user_service.py
│ └── auth_service.py
├── repositories/ # データアクセス
│ ├── user_repository.py
│ └── item_repository.py
└── main.py # アプリケーションエントリー
2. 依存性注入
Dependsを使用したFastAPIの組み込みDIシステム:
- データベースセッション管理
- 認証/認可
- 共有ビジネスロジック
- 設定注入
3. 非同期パターン
適切なasync/await使用:
- 非同期ルートハンドラー
- 非同期データベース操作
- 非同期バックグラウンドタスク
- 非同期ミドルウェア
実装パターン
パターン 1: 完全なFastAPIアプリケーション
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
"""アプリケーションライフスパンイベント。"""
await database.connect()
yield
await database.disconnect()
app = FastAPI(
title="API Template",
version="1.0.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
from app.api.v1.router import api_router
app.include_router(api_router, prefix="/api/v1")
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
"""アプリケーション設定。"""
DATABASE_URL: str
SECRET_KEY: str
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
API_V1_STR: str = "/api/v1"
class Config:
env_file =
() -> Settings:
Settings()
sqlalchemy.ext.asyncio create_async_engine, AsyncSession
sqlalchemy.ext.declarative declarative_base
sqlalchemy.orm sessionmaker
app.core.config get_settings
settings = get_settings()
engine = create_async_engine(
settings.DATABASE_URL,
echo=,
future=
)
AsyncSessionLocal = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=
)
Base = declarative_base()
() -> AsyncSession:
AsyncSessionLocal() session:
:
session
session.commit()
Exception:
session.rollback()
:
session.close()
パターン 2: CRUDリポジトリパターン
from typing import Generic, TypeVar, Type, Optional, List
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel
ModelType = TypeVar("ModelType")
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class BaseRepository(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
"""CRUD操作の基本リポジトリ。"""
def __init__(self, model: Type[ModelType]):
self.model = model
async def get(self, db: AsyncSession, id: int) -> Optional[ModelType]:
"""IDで取得。"""
result = await db.execute(
select(self.model).where(self.model.id == id)
)
return result.scalars().first()
async def get_multi(
self,
db: AsyncSession,
skip: int = 0,
limit: int = 100
) -> [ModelType]:
result = db.execute(
select(.model).offset(skip).limit(limit)
)
result.scalars().()
() -> ModelType:
db_obj = .model(**obj_in.())
db.add(db_obj)
db.flush()
db.refresh(db_obj)
db_obj
() -> ModelType:
update_data = obj_in.(exclude_unset=)
field, value update_data.items():
(db_obj, field, value)
db.flush()
db.refresh(db_obj)
db_obj
() -> :
obj = .get(db, )
obj:
db.delete(obj)
app.repositories.base_repository BaseRepository
app.models.user User
app.schemas.user UserCreate, UserUpdate
(BaseRepository[User, UserCreate, UserUpdate]):
() -> [User]:
result = db.execute(
select(User).where(User.email == email)
)
result.scalars().first()
() -> :
user = .get(db, user_id)
user.is_active user
user_repository = UserRepository(User)
パターン 3: サービス層
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.user_repository import user_repository
from app.schemas.user import UserCreate, UserUpdate, User
from app.core.security import get_password_hash, verify_password
class UserService:
"""ユーザーのビジネスロジック。"""
def __init__(self):
self.repository = user_repository
async def create_user(
self,
db: AsyncSession,
user_in: UserCreate
) -> User:
"""ハッシュ化されたパスワードで新しいユーザーを作成。"""
existing = await self.repository.get_by_email(db, user_in.email)
if existing:
raise ValueError("Email already registered")
user_in_dict = user_in.dict()
user_in_dict["hashed_password"] = get_password_hash(user_in_dict.pop("password"))
user = await self.repository.create(db, UserCreate(**user_in_dict))
return user
async def authenticate(
self,
db: AsyncSession,
email: str,
password:
) -> [User]:
user = .repository.get_by_email(db, email)
user:
verify_password(password, user.hashed_password):
user
() -> [User]:
user = .repository.get(db, user_id)
user:
user_in.password:
user_in_dict = user_in.(exclude_unset=)
user_in_dict[] = get_password_hash(
user_in_dict.pop()
)
user_in = UserUpdate(**user_in_dict)
.repository.update(db, user, user_in)
user_service = UserService()
パターン 4: 依存関係を持つAPIエンドポイント
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List
from app.core.database import get_db
from app.schemas.user import User, UserCreate, UserUpdate
from app.services.user_service import user_service
from app.api.dependencies import get_current_user
router = APIRouter()
@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
async def create_user(
user_in: UserCreate,
db: AsyncSession = Depends(get_db)
):
"""新しいユーザーを作成。"""
try:
user = await user_service.create_user(db, user_in)
return user
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/me", response_model=User)
async def read_current_user(
current_user: User = Depends(get_current_user)
):
"""現在のユーザーを取得。"""
return current_user
@router.get("/{user_id}", response_model=User)
async ():
user = user_service.repository.get(db, user_id)
user:
HTTPException(status_code=, detail=)
user
():
current_user. != user_id:
HTTPException(status_code=, detail=)
user = user_service.update_user(db, user_id, user_in)
user:
HTTPException(status_code=, detail=)
user
():
current_user. != user_id:
HTTPException(status_code=, detail=)
deleted = user_service.repository.delete(db, user_id)
deleted:
HTTPException(status_code=, detail=)
パターン 5: 認証と認可
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import get_settings
settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""JWTアクセストークンを作成。"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""パスワードをハッシュと照合。"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""パスワードをハッシュ化。"""
return pwd_context.hash(password)
from fastapi Depends, HTTPException, status
fastapi.security OAuth2PasswordBearer
jose JWTError, jwt
sqlalchemy.ext.asyncio AsyncSession
app.core.database get_db
app.core.security ALGORITHM
app.core.config get_settings
app.repositories.user_repository user_repository
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=)
():
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=,
headers={: },
)
:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
user_id: = payload.get()
user_id :
credentials_exception
JWTError:
credentials_exception
user = user_repository.get(db, user_id)
user :
credentials_exception
user
テスト
import pytest
import asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.core.database import get_db, Base
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture
async def db_session():
engine = create_async_engine(TEST_DATABASE_URL, echo=True)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with AsyncSessionLocal() as session:
yield session
@pytest.fixture
async def client(db_session):
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(app=app, base_url=) client:
client
pytest
():
response = client.post(
,
json={
: ,
: ,
:
}
)
response.status_code ==
data = response.json()
data[] ==
data
リソース
- references/fastapi-architecture.md: 詳細なアーキテクチャガイド
- references/async-best-practices.md: Async/awaitパターン
- references/testing-strategies.md: 包括的テストガイド
- assets/project-template/: 完全なFastAPIプロジェクト
- assets/docker-compose.yml: 開発環境セットアップ
ベストプラクティス
- 完全非同期: データベース、外部APIに非同期を使用
- 依存性注入: FastAPIのDIシステムを活用
- リポジトリパターン: データアクセスをビジネスロジックから分離
- サービス層: ビジネスロジックをルートの外に保つ
- Pydanticスキーマ: リクエスト/レスポンスの強力な型付け
- エラー処理: 一貫性のあるエラーレスポンス
- テスト: すべての層を独立してテスト
よくある落とし穴
- 非同期内でのブロッキングコード: 同期データベースドライバーの使用
- サービス層なし: ルートハンドラー内のビジネスロジック
- Type hintsの欠落: FastAPIの利点を失う
- セッション無視: データベースセッションの適切な管理なし
- テストなし: 統合テストのスキップ
- 密結合: ルート内での直接データベースアクセス
Source: amurata/cc-tools — distributed by TomeVault.