Toolmingo
Guides10 min read

FastAPI Cheat Sheet: The Complete Quick Reference

A complete FastAPI cheat sheet — routing, path/query params, request body, validation, dependency injection, authentication, middleware, background tasks, and testing.

The FastAPI patterns you need every day — routing, validation, dependency injection, auth, and testing — with copy-ready code in every section.

Quick reference

Task Code
Install pip install fastapi uvicorn[standard]
Run dev server uvicorn main:app --reload
Run on port uvicorn main:app --reload --port 8080
Auto docs (Swagger) http://localhost:8000/docs
Auto docs (Redoc) http://localhost:8000/redoc
OpenAPI JSON http://localhost:8000/openapi.json
Path parameter @app.get("/items/{item_id}")
Query parameter def read(skip: int = 0, limit: int = 10)
Request body async def create(item: Item)
Status code @app.post("/items", status_code=201)
Response model @app.get("/items", response_model=list[Item])
Background task background_tasks.add_task(fn, arg)

Project setup

pip install fastapi uvicorn[standard] pydantic-settings

Minimal app (main.py):

from fastapi import FastAPI

app = FastAPI(title="My API", version="1.0.0")

@app.get("/")
async def root():
    return {"message": "Hello World"}
uvicorn main:app --reload

Recommended project layout:

app/
├── main.py          # FastAPI instance, startup/shutdown
├── routers/
│   ├── users.py
│   └── items.py
├── models/          # SQLAlchemy models
├── schemas/         # Pydantic schemas
├── dependencies.py  # Shared dependencies
├── database.py      # DB session
└── config.py        # Settings

Routing

from fastapi import APIRouter

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

@router.get("/")
async def list_items():
    return []

@router.get("/{item_id}")
async def get_item(item_id: int):
    return {"id": item_id}

@router.post("/", status_code=201)
async def create_item(item: ItemCreate):
    return item

@router.put("/{item_id}")
async def update_item(item_id: int, item: ItemUpdate):
    return item

@router.delete("/{item_id}", status_code=204)
async def delete_item(item_id: int):
    pass

Register in main.py:

from app.routers import items, users

app.include_router(items.router)
app.include_router(users.router, prefix="/api/v1")

Path and query parameters

from fastapi import Path, Query
from enum import Enum

class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"

# Path parameter with validation
@app.get("/items/{item_id}")
async def get_item(
    item_id: int = Path(ge=1, le=1000, description="Item ID"),
):
    return {"id": item_id}

# Query parameters with defaults
@app.get("/items")
async def list_items(
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=10, ge=1, le=100),
    q: str | None = Query(default=None, max_length=50),
    active: bool = True,
):
    return {"skip": skip, "limit": limit, "q": q, "active": active}

# Enum path parameter
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    return {"model": model_name}

Request body with Pydantic

from pydantic import BaseModel, Field, EmailStr
from datetime import datetime

class ItemCreate(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    price: float = Field(gt=0)
    description: str | None = None
    tags: list[str] = []

class ItemResponse(ItemCreate):
    id: int
    created_at: datetime

    model_config = {"from_attributes": True}  # Pydantic v2 (ORM mode)

@app.post("/items", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate):
    # item is fully validated
    return {**item.model_dump(), "id": 1, "created_at": datetime.now()}

Nested models

class Address(BaseModel):
    street: str
    city: str
    country: str = "ME"

class User(BaseModel):
    name: str
    email: EmailStr          # pip install pydantic[email]
    address: Address
    tags: list[str] = []
    metadata: dict[str, str] = {}

Body + path + query together

@app.put("/items/{item_id}")
async def update_item(
    item_id: int,           # path param
    item: ItemCreate,       # request body
    q: str | None = None,  # query param
):
    return {"id": item_id, "q": q, **item.model_dump()}

Response models and status codes

from fastapi import HTTPException
from fastapi.responses import JSONResponse, Response

# Restrict response fields
@app.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int):
    user = db.get(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

# Multiple response codes (for docs)
@app.post(
    "/items",
    response_model=ItemResponse,
    status_code=201,
    responses={
        400: {"description": "Invalid input"},
        409: {"description": "Item already exists"},
    },
)
async def create_item(item: ItemCreate): ...

# Custom headers
@app.get("/items/{item_id}")
async def get_item(item_id: int):
    return JSONResponse(
        content={"id": item_id},
        headers={"X-Request-ID": "abc123"},
    )

# Empty 204 response
@app.delete("/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
    return Response(status_code=204)

Dependency injection

from fastapi import Depends
from typing import Annotated

# Simple dependency
def get_db():
    db = SessionLocal()
    try:
        yield db          # yield → FastAPI calls cleanup after request
    finally:
        db.close()

DB = Annotated[Session, Depends(get_db)]

# Use in route
@app.get("/users")
async def list_users(db: DB):
    return db.query(User).all()

# Dependency with parameters
def pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

Pagination = Annotated[dict, Depends(pagination)]

@app.get("/items")
async def list_items(page: Pagination):
    return page

# Nested dependencies
def verify_token(token: str = Header()):
    if token != "secret":
        raise HTTPException(status_code=401)
    return token

def get_current_user(token: str = Depends(verify_token), db: DB = Depends(get_db)):
    return db.query(User).filter_by(token=token).first()

CurrentUser = Annotated[User, Depends(get_current_user)]

# Router-level dependency (applies to all routes in router)
router = APIRouter(dependencies=[Depends(verify_token)])

Authentication

JWT Bearer auth

from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
import jwt  # pip install PyJWT

SECRET_KEY = "your-secret-key"  # use env var in production
ALGORITHM = "HS256"

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

def create_access_token(data: dict, expires_minutes: int = 30):
    payload = {**data, "exp": datetime.utcnow() + timedelta(minutes=expires_minutes)}
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def get_current_user(token: str = Depends(oauth2_scheme), db: DB = Depends(get_db)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: int = payload.get("sub")
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Invalid token")
    user = db.get(User, user_id)
    if not user:
        raise HTTPException(status_code=401, detail="User not found")
    return user

@app.post("/auth/token")
async def login(form: OAuth2PasswordRequestForm = Depends(), db: DB = Depends(get_db)):
    user = authenticate_user(db, form.username, form.password)
    if not user:
        raise HTTPException(status_code=401, detail="Bad credentials")
    token = create_access_token({"sub": str(user.id)})
    return {"access_token": token, "token_type": "bearer"}

@app.get("/me")
async def me(current_user: User = Depends(get_current_user)):
    return current_user

API key auth

from fastapi.security import APIKeyHeader

api_key_header = APIKeyHeader(name="X-API-Key")

async def verify_api_key(api_key: str = Depends(api_key_header)):
    if api_key != settings.API_KEY:
        raise HTTPException(status_code=403, detail="Invalid API key")
    return api_key

Request validation and error handling

from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

# Custom validation error handler
@app.exception_handler(RequestValidationError)
async def validation_error_handler(request, exc):
    return JSONResponse(
        status_code=422,
        content={"detail": exc.errors(), "body": exc.body},
    )

# Custom HTTP exception handler
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    return JSONResponse(
        status_code=exc.status_code,
        content={"error": exc.detail},
    )

# Custom app exception
class ItemNotFoundError(Exception):
    def __init__(self, item_id: int):
        self.item_id = item_id

@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request, exc):
    return JSONResponse(status_code=404, content={"error": f"Item {exc.item_id} not found"})

Headers, cookies, and form data

from fastapi import Header, Cookie, Form, File, UploadFile

# Request headers
@app.get("/items")
async def list_items(
    user_agent: str | None = Header(default=None),
    x_request_id: str | None = Header(default=None),  # X-Request-Id → x_request_id
):
    return {"user_agent": user_agent}

# Cookies
@app.get("/profile")
async def get_profile(session_id: str | None = Cookie(default=None)):
    return {"session": session_id}

# Form data (application/x-www-form-urlencoded)
@app.post("/login")
async def login(username: str = Form(), password: str = Form()):
    return {"username": username}

# File upload (multipart/form-data)
@app.post("/upload")
async def upload_file(file: UploadFile):
    content = await file.read()
    return {"filename": file.filename, "size": len(content), "type": file.content_type}

# Multiple files
@app.post("/upload-many")
async def upload_many(files: list[UploadFile]):
    return [{"filename": f.filename} for f in files]

Database with SQLAlchemy

# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

DATABASE_URL = "postgresql+psycopg2://user:password@localhost/dbname"

engine = create_engine(DATABASE_URL, pool_size=10, max_overflow=20)
SessionLocal = sessionmaker(bind=engine)

class Base(DeclarativeBase):
    pass

# models.py
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    email: Mapped[str] = mapped_column(unique=True, index=True)
    name: Mapped[str]
    is_active: Mapped[bool] = mapped_column(default=True)
    items: Mapped[list["Item"]] = relationship(back_populates="owner")

# Async SQLAlchemy (recommended for async FastAPI)
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

async_engine = create_async_engine("postgresql+asyncpg://user:password@localhost/dbname")
AsyncSessionLocal = async_sessionmaker(async_engine, expire_on_commit=False)

async def get_async_db():
    async with AsyncSessionLocal() as session:
        yield session

Middleware

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

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://example.com", "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Gzip compression
app.add_middleware(GZipMiddleware, minimum_size=1000)

# Custom middleware (request timing)
@app.middleware("http")
async def add_process_time(request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    duration = time.perf_counter() - start
    response.headers["X-Process-Time"] = f"{duration:.4f}s"
    return response

Background tasks

from fastapi import BackgroundTasks

def send_email(email: str, message: str):
    # runs after response is sent
    print(f"Sending email to {email}: {message}")

@app.post("/items")
async def create_item(item: ItemCreate, background_tasks: BackgroundTasks):
    # create item in DB...
    background_tasks.add_task(send_email, "user@example.com", f"Item {item.name} created")
    return item

Startup and shutdown events

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # startup
    await database.connect()
    print("Connected to database")
    yield
    # shutdown
    await database.disconnect()
    print("Database disconnected")

app = FastAPI(lifespan=lifespan)

Settings with pydantic-settings

# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    app_name: str = "My API"
    debug: bool = False
    database_url: str
    secret_key: str
    allowed_origins: list[str] = ["http://localhost:3000"]

    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

settings = Settings()  # reads from .env automatically

# .env
# DATABASE_URL=postgresql+asyncpg://user:pass@localhost/mydb
# SECRET_KEY=super-secret-key

Testing with pytest

# pip install httpx pytest pytest-asyncio
from fastapi.testclient import TestClient
import pytest

from app.main import app

client = TestClient(app)

def test_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}

def test_create_item():
    response = client.post(
        "/items",
        json={"name": "Widget", "price": 9.99},
    )
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Widget"

def test_validation_error():
    response = client.post("/items", json={"name": "", "price": -1})
    assert response.status_code == 422

# Override dependency in tests
from app.dependencies import get_db

def override_get_db():
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

# Async tests
@pytest.mark.asyncio
async def test_async_endpoint():
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
        response = await ac.get("/")
    assert response.status_code == 200

Common patterns

Pagination

from dataclasses import dataclass

@dataclass
class Page:
    items: list
    total: int
    skip: int
    limit: int

    @property
    def has_more(self) -> bool:
        return self.skip + self.limit < self.total

@app.get("/items", response_model=PageResponse)
async def list_items(
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=20, ge=1, le=100),
    db: DB = Depends(get_db),
):
    total = db.query(Item).count()
    items = db.query(Item).offset(skip).limit(limit).all()
    return Page(items=items, total=total, skip=skip, limit=limit)

Versioned API

from fastapi import APIRouter

v1 = APIRouter(prefix="/api/v1")
v2 = APIRouter(prefix="/api/v2")

v1.include_router(items_v1.router, prefix="/items")
v2.include_router(items_v2.router, prefix="/items")

app.include_router(v1)
app.include_router(v2)

Rate limiting

# pip install slowapi
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/items")
@limiter.limit("30/minute")
async def list_items(request: Request):  # request must be in signature
    return []

Common mistakes

Mistake Problem Fix
def instead of async def Blocks event loop for I/O Use async def for all async I/O
Mutable default in Pydantic Shared state across requests Use default_factory=list or Field(default_factory=list)
Missing await on async calls Gets coroutine object, not result Always await async functions
Not using response_model Leaks internal fields Always set response_model on endpoints
Secrets in code Security risk Use pydantic-settings + .env
No CORS middleware Frontend blocked Add CORSMiddleware before routes
Not closing DB session Connection pool exhaustion Use yield + finally in dependency
200 for created resources Wrong semantics Use status_code=201 for POST

FastAPI vs Django vs Flask

Feature FastAPI Django Flask
Performance Excellent (async) Good Good
Auto docs Built-in (Swagger + Redoc) Third-party Third-party
Validation Pydantic (built-in) Forms/DRF serializers Manual
ORM BYO (SQLAlchemy recommended) Built-in BYO
Admin panel Third-party Built-in Third-party
Learning curve Low–medium Medium–high Low
Async support Native Partial (Django 4.1+) Partial
Best for APIs, microservices Full-stack, admin-heavy Simple APIs, flexibility

FAQ

Q: Should I use async def or def for route handlers? Use async def when your handler calls async functions (database, HTTP client, etc.). Use plain def only for CPU-bound work — FastAPI will run it in a thread pool. Mixing sync blocking calls inside async def will block the event loop.

Q: How do I handle CORS? Add CORSMiddleware before any routes. In development, set allow_origins=["*"]. In production, list specific origins. For credentialed requests (cookies, auth headers), set allow_credentials=True and list explicit origins — "*" won't work with credentials.

Q: What's the difference between response_model and the return type annotation? response_model controls what gets serialized and sent to the client — it filters and validates the output. The return type annotation is for IDE/type checker support only and doesn't affect the response. Always set response_model explicitly for public endpoints to avoid leaking fields.

Q: How do I connect FastAPI with a real database? Use SQLAlchemy with async session for best performance: create_async_engine + async_sessionmaker. Expose the session via Depends(get_async_db). For migrations, use Alembic (alembic init, alembic revision --autogenerate, alembic upgrade head).

Q: How do I deploy FastAPI to production? Run with uvicorn behind a reverse proxy (nginx or Caddy). Use gunicorn -k uvicorn.workers.UvicornWorker for multiple workers. On containers, set --workers $(nproc). For serverless: Vercel (via ASGI adapter), AWS Lambda (Mangum adapter), or Google Cloud Run (Docker container).

Q: How do I add background jobs (not just background tasks)? For simple fire-and-forget: BackgroundTasks. For reliable, persistent jobs: use Celery + Redis/RabbitMQ, or ARQ (async Redis Queue), or Dramatiq. Background tasks added via BackgroundTasks are lost if the process crashes.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools