agentleFS
LWaetzigLWaetzig/claude-code-skills/skills/fastapi-patterns/SKILL.md

fastapi-patterns

Skill0 starsChanged 7 months agoMITView on GitHub

Best practices and patterns for building FastAPI applications. Use when creating new endpoints, structuring a FastAPI project, designing Pydantic models, handling errors, setting up authentication, or reviewing existing FastAPI code for quality.

  • Reads credentials

19 other projects use a version of this, 8 of them edited.

Adopted by

Most recently taken up by

Used with

Projects with it also use redis-patterns, typescript-patterns and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • nek1987Skill

    redis-patterns

    Upstash Redis patterns for caching and rate limiting.

    Also in 6 projects that use this

  • cohen-lielSkill

    typescript-patterns

    TypeScript best practices and patterns. Use when writing TypeScript code, defining types, working with generics, or converting JavaScript to TypeScript.

    Also in 3 projects that use this

  • LangChainSkill

    docker-patterns

    Best practices for Docker containerization and multi-stage builds

    Also in 7 projects that use this

  • mstuder-slalomSkill

    golang-patterns

    Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications.

    Also in 5 projects that use this

  • mstuder-slalomSkill

    django-security

    Django security best practices, authentication, authorization, CSRF protection, SQL injection prevention, XSS prevention, and secure deployment configurations.

    Also in 4 projects that use this

  • xenitV1Skill

    python-patterns

    Python development principles and decision-making. Framework selection, async patterns, type hints, project structure. Teaches thinking, not copying.

    Also in 6 projects that use this

  • +1 more from mstuder-slalom

The file

We split this skill into the rules it gives an agent

2 rules. 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 15 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use background tasks for fire-and-forget work (emails, webhooks).

    line 295
  2. Very specific:

    Pydantic v2 uses model_config: not inner Config class — don't mix patterns

    line 354

“Common” is measured: kept by at least 6 in 10 of the 15 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

FastAPI Patterns Skill

Build FastAPI applications that are well-structured, type-safe, and easy to maintain.


Project Structure

For non-trivial APIs, use this layout:

app/
├── main.py              # App factory, lifespan, global middleware
├── api/
│   ├── deps.py          # Shared dependencies (get_db, get_current_user)
│   └── v1/
│       ├── router.py    # Aggregates all v1 routers
│       ├── users.py     # /users endpoints
│       └── items.py     # /items endpoints
├── models/
│   ├── user.py          # SQLAlchemy / SQLModel ORM models
│   └── item.py
├── schemas/
│   ├── user.py          # Pydantic request/response schemas
│   └── item.py
├── services/
│   ├── user_service.py  # Business logic
│   └── item_service.py
├── core/
│   ├── config.py        # Settings via pydantic-settings
│   ├── security.py      # Password hashing, JWT helpers
│   └── database.py      # Engine, SessionLocal, Base
└── tests/

App Factory and Lifespan

# main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.api.v1.router import api_router
from app.core.database import engine, Base

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    # Shutdown
    await engine.dispose()

def create_app() -> FastAPI:
    app = FastAPI(
        title="My API",
        version="1.0.0",
        lifespan=lifespan,
    )
    app.include_router(api_router, prefix="/api/v1")
    return app

app = create_app()

Pydantic Schemas

Separate request schemas, response schemas, and DB models. Never return ORM models directly.

# schemas/user.py
from pydantic import BaseModel, EmailStr, ConfigDict
from datetime import datetime

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

class UserUpdate(BaseModel):
    name: str | None = None
    email: EmailStr | None = None

class UserResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # enables ORM mode

    id: int
    email: EmailStr
    name: str
    created_at: datetime

    # Never include: password, password_hash

Use model_config = ConfigDict(from_attributes=True) on response schemas

This lets FastAPI serialize ORM objects directly: UserResponse.model_validate(db_user).


Dependency Injection

# api/deps.py
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_async_session
from app.core.security import decode_token
from app.models.user import User

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token")

async def get_db() -> AsyncSession:
    async with get_async_session() as session:
        yield session

async def get_current_user(
    token: Annotated[str, Depends(oauth2_scheme)],
    db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
    payload = decode_token(token)
    if not payload:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    user = await db.get(User, payload["sub"])
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    return user

# Typed aliases — use these in route signatures
CurrentUser = Annotated[User, Depends(get_current_user)]
DbSession = Annotated[AsyncSession, Depends(get_db)]

Router and Route Patterns

# api/v1/users.py
from typing import Annotated
from fastapi import APIRouter, HTTPException, status
from app.api.deps import CurrentUser, DbSession
from app.schemas.user import UserCreate, UserResponse, UserUpdate
from app.services.user_service import UserService

router = APIRouter(prefix="/users", tags=["users"])

@router.get("/me", response_model=UserResponse)
async def get_current_user_profile(current_user: CurrentUser):
    return current_user

@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: DbSession):
    user = await UserService(db).get_by_id(user_id)
    if not user:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    return user

@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, db: DbSession):
    return await UserService(db).create(payload)

@router.patch("/{user_id}", response_model=UserResponse)
async def update_user(
    user_id: int,
    payload: UserUpdate,
    current_user: CurrentUser,
    db: DbSession,
):
    if current_user.id != user_id:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
    return await UserService(db).update(user_id, payload)

Error Handling

Use HTTPException for expected errors

raise HTTPException(
    status_code=status.HTTP_404_NOT_FOUND,
    detail="User not found",
)

Custom exception handlers for domain errors

# main.py
from fastapi import Request
from fastapi.responses import JSONResponse

class ResourceNotFoundError(Exception):
    def __init__(self, resource: str, id: int):
        self.resource = resource
        self.id = id

@app.exception_handler(ResourceNotFoundError)
async def not_found_handler(request: Request, exc: ResourceNotFoundError):
    return JSONResponse(
        status_code=404,
        content={"detail": f"{exc.resource} with id {exc.id} not found"},
    )

Validation error format

FastAPI automatically returns 422 with field-level detail for Pydantic validation errors. Don't re-raise or wrap these.


Settings

# core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

    database_url: str
    secret_key: str
    access_token_expire_minutes: int = 30
    debug: bool = False

settings = Settings()  # reads from env / .env file

Authentication (JWT)

# core/security.py
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"])

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)

def create_access_token(subject: int | str) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
    return jwt.encode({"sub": str(subject), "exp": expire}, settings.secret_key, algorithm="HS256")

def decode_token(token: str) -> dict | None:
    try:
        return jwt.decode(token, settings.secret_key, algorithms=["HS256"])
    except JWTError:
        return None

Background Tasks

from fastapi import BackgroundTasks

@router.post("/orders/", status_code=201)
async def create_order(
    payload: OrderCreate,
    background_tasks: BackgroundTasks,
    db: DbSession,
):
    order = await OrderService(db).create(payload)
    background_tasks.add_task(send_order_confirmation_email, order.id)
    return order

Use background tasks for fire-and-forget work (emails, webhooks). For anything that must complete reliably, use a task queue (Celery, ARQ, or similar).


Middleware

from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
import time

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
app.add_middleware(GZipMiddleware, minimum_size=1000)

@app.middleware("http")
async def add_request_timing(request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    response.headers["X-Process-Time"] = str(time.perf_counter() - start)
    return response

OpenAPI Customization

@router.post(
    "/items/",
    response_model=ItemResponse,
    status_code=201,
    summary="Create a new item",
    response_description="The created item",
    responses={
        409: {"description": "Item with this name already exists"},
    },
)
async def create_item(payload: ItemCreate, db: DbSession):
    ...

Tag routers to group endpoints in the docs:

router = APIRouter(prefix="/items", tags=["items"])

Common Gotchas

  • Never use async def with blocking I/O — use run_in_executor or switch to a sync route if the library isn't async-native
  • Dependency functions that yield must be async generators for async DB sessions — async def get_db() with yield
  • response_model strips fields not in the schema — use it as security, not just docs
  • Pydantic v2 uses model_config not inner Config class — don't mix patterns
  • Avoid returning raw dicts — always use response_model so the schema is enforced and documented

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.