fastapi

SKILL.md

FastAPI Framework Guide

Applies to: FastAPI 0.100+, Pydantic v2, SQLAlchemy 2.0 Language: Python 3.10+ Type: Async API Framework

Overview

FastAPI is a modern, high-performance web framework for building APIs with Python based on standard type hints. Built on Starlette (web) and Pydantic (validation).

Use FastAPI when:

  • Building REST APIs or GraphQL backends
  • Need native async/await support
  • Want automatic OpenAPI documentation
  • Need high performance (comparable to Node.js/Go)
  • Want strong type validation with Pydantic

Consider alternatives when:

  • Need full-stack with templates (consider Django)
  • Building simple scripts (consider Flask)
  • Need extensive admin interface (consider Django)

Core Principles

  1. Type-First: Use type hints everywhere; they drive validation and docs
  2. Async by Default: Use async def for I/O-bound endpoints
  3. Dependency Injection: Use Depends() for shared logic and resources
  4. Thin Endpoints: Keep route handlers thin, business logic in services
  5. Schema Validation: Use Pydantic models for all request/response data

Guardrails

Code Style

  • Use async def for endpoints with I/O operations
  • Use def (sync) only for CPU-bound operations without I/O
  • Never use sync database calls in async context
  • Always use type hints on function signatures
  • Run ruff check and mypy before committing

API Design

  • Version your API: /api/v1/...
  • Use plural nouns for resources: /users, /products
  • Use HTTP methods correctly: GET (read), POST (create), PUT/PATCH (update), DELETE
  • Return appropriate status codes (201 for creation, 204 for deletion)
  • Always set response_model on endpoints

Security

  • Validate all inputs with Pydantic (automatic with FastAPI)
  • Use OAuth2PasswordBearer for token auth
  • Hash passwords with bcrypt (never store plaintext)
  • Set CORS origins explicitly (never use * in production)
  • Use Depends() for auth checks on every protected endpoint

Error Handling

  • Use custom exception classes inheriting from HTTPException
  • Register global exception handlers for consistent responses
  • Never expose internal errors to clients
  • Always wrap database errors with context

Project Structure

myproject/
├── pyproject.toml
├── alembic.ini
├── alembic/
│   ├── versions/
│   └── env.py
├── app/
│   ├── __init__.py
│   ├── main.py                 # Application entry point
│   ├── config.py               # Settings (pydantic-settings)
│   ├── database.py             # Async SQLAlchemy engine/session
│   ├── dependencies.py         # Shared dependencies
│   ├── models/                 # SQLAlchemy ORM models
│   │   ├── __init__.py
│   │   ├── base.py             # DeclarativeBase, mixins
│   │   └── user.py
│   ├── schemas/                # Pydantic v2 schemas
│   │   ├── __init__.py
│   │   └── user.py
│   ├── api/                    # API routes
│   │   ├── __init__.py
│   │   ├── deps.py             # API-level dependencies (auth)
│   │   └── v1/
│   │       ├── __init__.py
│   │       ├── router.py       # Aggregates all v1 routers
│   │       └── endpoints/
│   │           ├── users.py
│   │           └── products.py
│   ├── services/               # Business logic layer
│   │   ├── __init__.py
│   │   └── user.py
│   ├── repositories/           # Data access layer
│   │   ├── __init__.py
│   │   └── user.py
│   └── core/                   # Cross-cutting utilities
│       ├── __init__.py
│       ├── security.py         # JWT, password hashing
│       └── exceptions.py       # Custom exception classes
├── tests/
│   ├── conftest.py             # Fixtures (async client, db session)
│   ├── test_users.py
│   └── factories.py
└── docker-compose.yml
  • models/ = SQLAlchemy ORM (database shape)
  • schemas/ = Pydantic (API shape, validation)
  • services/ = Business logic (orchestration, rules)
  • repositories/ = Data access (queries, CRUD)

Application Setup

Main Application

# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.api.v1.router import api_router
from app.config import settings
from app.database import engine
from app.models.base import Base


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


app = FastAPI(
    title=settings.PROJECT_NAME,
    version=settings.VERSION,
    openapi_url=f"{settings.API_V1_STR}/openapi.json",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(api_router, prefix=settings.API_V1_STR)


@app.get("/health")
async def health_check():
    return {"status": "healthy"}

Configuration

# app/config.py
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict


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

    PROJECT_NAME: str = "FastAPI App"
    VERSION: str = "1.0.0"
    DEBUG: bool = False
    API_V1_STR: str = "/api/v1"
    DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost/db"
    SECRET_KEY: str
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    CORS_ORIGINS: list[str] = ["http://localhost:3000"]

@lru_cache
def get_settings() -> Settings:
    return Settings()

settings = get_settings()

Pydantic Schemas (v2)

# app/schemas/user.py
from datetime import datetime
from uuid import UUID

from pydantic import BaseModel, ConfigDict, EmailStr, Field


class UserBase(BaseModel):
    email: EmailStr
    first_name: str | None = None
    last_name: str | None = None

class UserCreate(UserBase):
    password: str = Field(..., min_length=8, max_length=100)

class UserUpdate(BaseModel):
    """Partial update: all fields optional."""
    email: EmailStr | None = None
    first_name: str | None = None
    last_name: str | None = None
    password: str | None = Field(None, min_length=8, max_length=100)

class UserResponse(UserBase):
    model_config = ConfigDict(from_attributes=True)
    id: UUID
    is_active: bool
    created_at: datetime

class PaginatedResponse[T](BaseModel):
    items: list[T]
    total: int
    page: int
    size: int
    pages: int

Key Pydantic v2 patterns:

  • Use ConfigDict(from_attributes=True) instead of class Config: orm_mode = True
  • Use model_dump() / model_validate() instead of .dict() / .from_orm()
  • Use Field(...) for required fields with constraints
  • Use model_dump(exclude_unset=True) for partial updates

Route Definitions

Router Setup

# app/api/v1/router.py
from fastapi import APIRouter
from app.api.v1.endpoints import auth, users, products

api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(products.router, prefix="/products", tags=["products"])

Endpoint Pattern

# app/api/v1/endpoints/users.py
from uuid import UUID
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_current_user
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserResponse, PaginatedResponse
from app.services.user import UserService

router = APIRouter()


@router.get("", response_model=PaginatedResponse[UserResponse])
async def list_users(
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Get paginated list of users."""
    service = UserService(db)
    skip = (page - 1) * size
    users, total = await service.get_multi(skip=skip, limit=size)

    return PaginatedResponse(
        items=[UserResponse.model_validate(u) for u in users],
        total=total,
        page=page,
        size=size,
        pages=(total + size - 1) // size,
    )


@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
    user_id: UUID,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Get user by ID."""
    service = UserService(db)
    user = await service.get(user_id)
    return UserResponse.model_validate(user)

Dependency Injection

# app/database.py — Session dependency
async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()


# app/api/deps.py — Auth dependencies
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer

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


async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
) -> User:
    payload = verify_token(token)
    user = await UserRepository(db).get(payload.sub)
    if not user or not user.is_active:
        raise HTTPException(status_code=403, detail="Inactive or missing user")
    return user


async def get_current_superuser(
    current_user: User = Depends(get_current_user),
) -> User:
    if not current_user.is_superuser:
        raise HTTPException(status_code=403, detail="Not enough permissions")
    return current_user

DI best practices:

  • Use Depends() for database sessions, auth, services
  • Dependencies can depend on other dependencies (chain)
  • Use yield dependencies for setup/teardown (e.g., DB sessions)
  • Keep dependency functions small and focused

Async Patterns

Database (Async SQLAlchemy)

# app/database.py
from sqlalchemy.ext.asyncio import (
    AsyncSession, async_sessionmaker, create_async_engine,
)

engine = create_async_engine(
    settings.DATABASE_URL,
    echo=settings.DEBUG,
    pool_pre_ping=True,
    pool_size=5,
    max_overflow=10,
)

AsyncSessionLocal = async_sessionmaker(
    engine, class_=AsyncSession,
    expire_on_commit=False,
)

Key async rules

  • Use asyncpg driver (not psycopg2) for PostgreSQL
  • Use selectinload() for eager loading relationships (avoids N+1)
  • Use await session.flush() to get IDs without committing
  • Use await session.refresh(obj) after flush to load defaults

Error Handling

# app/core/exceptions.py
from fastapi import HTTPException, status


class AppException(HTTPException):
    def __init__(self, detail: str, status_code: int = 500):
        super().__init__(status_code=status_code, detail=detail)


class NotFoundException(AppException):
    def __init__(self, detail: str = "Not found"):
        super().__init__(detail=detail, status_code=status.HTTP_404_NOT_FOUND)


class ConflictException(AppException):
    def __init__(self, detail: str = "Conflict"):
        super().__init__(detail=detail, status_code=status.HTTP_409_CONFLICT)


class UnauthorizedException(AppException):
    def __init__(self, detail: str = "Unauthorized"):
        super().__init__(detail=detail, status_code=status.HTTP_401_UNAUTHORIZED)


class ForbiddenException(AppException):
    def __init__(self, detail: str = "Forbidden"):
        super().__init__(detail=detail, status_code=status.HTTP_403_FORBIDDEN)

Register a global handler in main.py:

from fastapi.responses import JSONResponse

@app.exception_handler(AppException)
async def app_exception_handler(request, exc):
    return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})

Commands Reference

# Development
uvicorn app.main:app --reload --port 8000

# Production
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

# Database migrations
alembic init alembic
alembic revision --autogenerate -m "Initial migration"
alembic upgrade head
alembic downgrade -1

# Testing
pytest
pytest -v --cov=app --cov-report=html
pytest tests/test_users.py -k "test_register"

# Linting & type checking
ruff check app/
ruff check app/ --fix
mypy app/

Best Practices Summary

Do

  • Use Pydantic for all request/response validation
  • Use dependency injection for services and sessions
  • Keep endpoints thin, logic in services
  • Use async def for I/O operations
  • Handle errors with custom exceptions
  • Use type hints everywhere
  • Write comprehensive async tests with httpx.AsyncClient

Don't

  • Put business logic in endpoints
  • Use sync database calls in async context
  • Expose internal ORM models directly as responses
  • Skip input validation
  • Ignore error handling
  • Use global mutable state

Advanced Topics

For detailed patterns, full code examples, and advanced usage, see:

  • references/patterns.md -- Repository pattern, services, testing, security, middleware, background tasks, WebSocket

External References

Weekly Installs
10
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
Feb 20, 2026
Installed on
opencode10
gemini-cli10
github-copilot10
codex10
amp10
kimi-cli10