# Fastapi Patterns

> When to activate: FastAPI, Pydantic v2, dependency injection, async endpoints, middleware, background tasks, OpenAPI

- Skill: `mattakushi432/fastapi-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/fastapi-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/fastapi-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/fastapi-patterns

---


# FastAPI Patterns

## Application Structure
```
app/
├── main.py              # FastAPI() instance, startup/shutdown events
├── api/
│   ├── deps.py          # Shared dependencies (db, auth, settings)
│   └── v1/
│       ├── router.py    # APIRouter for v1
│       └── users.py     # Endpoint handlers
├── models/
│   ├── user.py          # SQLAlchemy models
│   └── base.py          # Base, metadata
├── schemas/
│   ├── user.py          # Pydantic request/response schemas
│   └── common.py        # Shared schemas (pagination, errors)
├── services/
│   └── user_service.py  # Business logic (not in endpoints)
└── core/
    ├── config.py        # Settings (pydantic-settings)
    └── security.py      # Auth helpers
```

## Dependency Injection
```python
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

# Good: generator dependency with cleanup
async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_maker() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise

# Type-annotated dependency
CurrentUser = Annotated[User, Depends(get_current_user)]

@router.get("/me", response_model=UserSchema)
async def get_me(user: CurrentUser) -> User:
    return user
```

## Pydantic v2 Schemas
```python
from pydantic import BaseModel, EmailStr, field_validator, model_validator, ConfigDict

class UserCreate(BaseModel):
    email: EmailStr
    password: str
    name: str

    @field_validator("password")
    @classmethod
    def validate_password(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("Password must be at least 8 characters")
        return v

class UserResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # replaces orm_mode
    
    id: int
    email: EmailStr
    name: str
    created_at: datetime
```

## Response Models and Error Handling
```python
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse

# Always specify response_model
@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(body: UserCreate, db: AsyncSession = Depends(get_db)) -> User:
    if await user_service.email_exists(db, body.email):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={"code": "email_taken", "message": "Email already registered"},
        )
    return await user_service.create(db, body)

# Global exception handler
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
    return JSONResponse(status_code=400, content={"error": str(exc)})
```

## Background Tasks
```python
from fastapi import BackgroundTasks

@router.post("/reports")
async def generate_report(
    params: ReportParams,
    background_tasks: BackgroundTasks,
    db: AsyncSession = Depends(get_db),
) -> dict:
    report_id = uuid4()
    # Don't fire-and-forget without error capture
    background_tasks.add_task(report_service.generate, db, report_id, params)
    return {"report_id": str(report_id), "status": "processing"}
```

## Anti-Patterns
- Using `Session` (sync) inside `async def` endpoints
- Missing `response_model` on endpoints
- Business logic inside endpoint handlers (use service layer)
- `async def` without any actual `await` (just use `def`)
- Mutable defaults in Pydantic fields: `tags: list = []` → `tags: list = Field(default_factory=list)`

