DS DevShelfHub Projects · AI tools
Cheatsheets / FastAPI
Cheatsheet · Dev tooling

FastAPI Cheatsheet: Routing, Pydantic, Auth and Testing

By DevShelfHub

Routing, request models, dependencies, async, auth, background tasks, WebSockets, testing — the modern Python API framework.

118 items 8 min Async Pydantic OpenAPI

Start hereQuick start · 6 you’ll reach for daily

Route@app.get("/items/{id}")
Body modeldef create(item: Item)
DependencyAnnotated[T, Depends(fn)]
Run devfastapi dev app.py
Docs/docs · /redoc
TestTestClient(app)

Target versions · paceVersions

Targets: fastapi ≥ 0.115 pydantic ≥ 2.7 starlette ≥ 0.40 uvicorn ≥ 0.30 python ≥ 3.10

Pydantic v2 is required — .dict() is now .model_dump(), orm_mode is from_attributes=True. @app.on_event("startup") is deprecated — use the lifespan context manager. Annotated[T, Depends(…)] is the modern dependency form. This sheet pins to behavior current as of May 2026.

Install · runSetup

bash
# Install
pip install "fastapi[standard]"
# ↑ bundles uvicorn, httpx, jinja2, python-multipart, email-validator

# Run (dev — auto-reload, docs at /docs)
fastapi dev app.py

# Run (prod)
fastapi run app.py --workers 4

# Or directly with uvicorn
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4

# Alembic migrations (if using SQLAlchemy)
pip install alembic sqlalchemy "psycopg[binary]"

Where things liveCommon imports

from fastapi import FastAPI, APIRouter, Depends, HTTPException, statusCore app + helpers.
from fastapi import Path, Query, Body, Header, Cookie, Form, File, UploadFileParameter sources.
from fastapi import Request, Response, BackgroundTasks, WebSocketLower-level objects.
from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse, FileResponse, RedirectResponseResponse types.
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm, HTTPBearerAuth helpers.
from fastapi.middleware.cors import CORSMiddlewareCORS.
from fastapi.testclient import TestClientSync test client.
from pydantic import BaseModel, Field, EmailStr, field_validator, ConfigDictPydantic v2.
from typing import AnnotatedRequired for the modern dependency syntax.

12-line shapeMinimal app

python
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(title="Items API", version="1.0")

class Item(BaseModel):
    name: str = Field(min_length=1, max_length=80)
    price: float = Field(gt=0)
    in_stock: bool = True

@app.get("/")
async def root() -> dict:
    return {"status": "ok"}

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

@app.post("/items", status_code=201)
async def create_item(item: Item) -> Item:
    return item

# fastapi dev app.py  →  http://127.0.0.1:8000/docs

Paths · methods · routersRouting

@app.get / .post / .put / .patch / .deleteHTTP verb decorators. Methods accept identical kwargs.
@app.api_route("/x", methods=["GET","HEAD"])Multi-method registration.
/items/{id:int}Path converter (rare — usually annotate the param).
@app.get("/", response_model=Item)Strict response shape; auto-validates & documents.
@app.get("/", status_code=201)Override default 200.
@app.get("/", tags=["items"])Group in /docs.
@app.get("/", summary="…", description="…")OpenAPI metadata.
@app.get("/", deprecated=True)Marks route as deprecated in docs.
@app.get("/", include_in_schema=False)Hide from /docs.

APIRouter

router = APIRouter(prefix="/items", tags=["items"])Sub-app with shared prefix + tags.
app.include_router(router)Mount it.
app.include_router(r, prefix="/v1", dependencies=[Depends(auth)])Group-level prefix + auth.

Path · query · bodyParameters

async def f(id: int)Path param — type-coerced & validated.
q: str | None = NoneOptional query param.
q: Annotated[str, Query(min_length=3, max_length=50)]Validated query.
id: Annotated[int, Path(ge=1)]Validated path.
item: ItemJSON body via Pydantic model.
extra: Annotated[str, Body(embed=True)]Force a non-model field into the JSON body.
x_token: Annotated[str, Header()]Header. _ is mapped to -.
session: Annotated[str, Cookie()]Cookie.
name: Annotated[str, Form()]multipart/form-data field.
file: Annotated[UploadFile, File()]File upload. Streams to disk-backed temp.

Validation & serializationPydantic v2 models

class Item(BaseModel): …Inherits Pydantic v2 model.
Field(min_length=1, max_length=80, examples=["tea"])Constraints + OpenAPI examples.
price: float = Field(gt=0, le=1_000_000)Numeric range.
email: EmailStrValidated email string (needs email-validator).
tags: list[str] = []Typed list, defaulted.
@field_validator("name") @classmethod def x(cls, v): …Custom field validator.
model_config = ConfigDict(from_attributes=True)v2 Read from ORM objects (was orm_mode).
Item.model_validate(data)Validate a dict/object.
item.model_dump()v2 Was .dict().
item.model_dump_json()Was .json().

DI · auth · paginationDependencies

python
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()

# Plain dependency
async def common_params(q: str = "", skip: int = 0, limit: int = 10):
    return {"q": q, "skip": skip, "limit": limit}

CommonDeps = Annotated[dict, Depends(common_params)]

@app.get("/items")
async def list_items(params: CommonDeps):
    return params

# Auth via dependency
async def require_token(x_token: Annotated[str, Header()]):
    if x_token != "secret":
        raise HTTPException(401, "bad token")
    return {"user": "alice"}

@app.get("/me")
async def me(user: Annotated[dict, Depends(require_token)]):
    return user

# Class-based dependency
class Pager:
    def __init__(self, page: int = 1, size: int = 20):
        self.offset = (page - 1) * size
        self.limit = size

@app.get("/posts")
async def posts(p: Annotated[Pager, Depends()]):
    return {"offset": p.offset, "limit": p.limit}
Annotated[T, Depends(fn)]Preferred Modern syntax. Reusable as a type alias.
x: T = Depends(fn)Legacy Pre-Annotated syntax. Still works.
dependencies=[Depends(auth)]Route/router-level dep that runs but isn’t injected.
yield in a dependencySetup before yield, teardown after — like a context manager.
app.dependency_overrides[fn] = fakeSwap a dep in tests.
Depends(use_cache=False)Disable per-request caching for repeated deps.

async def · lifespanAsync & lifespan

python
from contextlib import asynccontextmanager
from fastapi import FastAPI
import httpx

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    app.state.http = httpx.AsyncClient(timeout=10)
    yield
    # Shutdown
    await app.state.http.aclose()

app = FastAPI(lifespan=lifespan)

@app.get("/proxy")
async def proxy():
    r = await app.state.http.get("https://httpbin.org/get")
    return r.json()
async def endpoint(…)Runs on the event loop. Use for I/O-bound work.
def endpoint(…)Runs on a threadpool. Use for blocking libs.
@asynccontextmanager + lifespan=…Preferred Startup/shutdown hook.
@app.on_event("startup" | "shutdown")Legacy Use lifespan instead.
await asyncio.gather(a(), b())Run I/O in parallel inside a handler.
from starlette.concurrency import run_in_threadpoolOffload a blocking call from an async handler.

JSON · stream · fileResponses

return {"k": "v"}Auto-JSON. Pydantic models also work.
response_model=ItemFilters response shape; hides extra fields.
response_model_exclude_none=TrueDrop null fields from the JSON.
return JSONResponse(content=…, status_code=…)Manual response, e.g. for custom headers.
return HTMLResponse("<h1>…</h1>")Raw HTML.
return FileResponse("path.pdf", filename="x.pdf")Streams a file from disk.
return StreamingResponse(generator(), media_type="text/event-stream")SSE / chunked output (LLM tokens, exports).
return RedirectResponse("/new", status_code=307)307 preserves method; 302 may downgrade POST → GET.
raise HTTPException(404, "not found")Bail with a status + body.

OAuth2 / JWTAuth

python
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt, JWTError
from passlib.context import CryptContext

SECRET, ALG = "change-me", "HS256"
pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2 = OAuth2PasswordBearer(tokenUrl="token")

app = FastAPI()
USERS = {"alice": {"hashed": pwd.hash("wonderland")}}

def make_token(sub: str) -> str:
    payload = {"sub": sub,
               "exp": datetime.now(timezone.utc) + timedelta(minutes=30)}
    return jwt.encode(payload, SECRET, algorithm=ALG)

@app.post("/token")
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
    u = USERS.get(form.username)
    if not u or not pwd.verify(form.password, u["hashed"]):
        raise HTTPException(401, "bad credentials")
    return {"access_token": make_token(form.username), "token_type": "bearer"}

async def current_user(token: Annotated[str, Depends(oauth2)]) -> str:
    try:
        return jwt.decode(token, SECRET, algorithms=[ALG])["sub"]
    except JWTError:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid token")

@app.get("/me")
async def me(user: Annotated[str, Depends(current_user)]):
    return {"user": user}
OAuth2PasswordBearer(tokenUrl="token")Reads Authorization: Bearer …; wires /docs auth button.
OAuth2PasswordRequestFormParses the form fields the OAuth2 password flow expects.
HTTPBearer()Simpler bearer scheme. No password-grant docs UI.
pwd.hash(plain) / pwd.verify(p, h)bcrypt via passlib.
jwt.encode / jwt.decodepython-jose. Set exp; verify on decode.

After-response · socketsBackground, WebSockets, Files

background: BackgroundTasks; background.add_task(fn, *args)Run after the response is sent. Same process.
For real queuesUse Celery, RQ, or arq — BackgroundTasks won’t survive a restart.
@app.websocket("/ws") async def ws(s: WebSocket): …WS endpoint.
await ws.accept() / ws.receive_text() / ws.send_json(…)Lifecycle & I/O.
await ws.close(code=1000)Close cleanly.
UploadFile.read() / .write() / .seek()SpooledTemporaryFile behind the scenes.
app.mount("/static", StaticFiles(directory="static"))Serve a directory.

Cross-cuttingMiddleware & CORS

app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"])Enable CORS. Restrict in prod.
app.add_middleware(GZipMiddleware, minimum_size=1000)Gzip large responses.
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["api.example.com"])Host header allowlist.
@app.middleware("http") async def m(request, call_next): …Inline middleware. await call_next(request) to continue.
@app.exception_handler(MyError)Global handler for a specific exception class.

TestClient · AsyncClientTesting

python
# tests/test_app.py
import pytest
from httpx import AsyncClient, ASGITransport
from fastapi.testclient import TestClient
from app import app

# 1 · Sync — quick smoke
def test_root():
    client = TestClient(app)
    r = client.get("/")
    assert r.status_code == 200
    assert r.json() == {"status": "ok"}

# 2 · Async — real concurrency
@pytest.mark.asyncio
async def test_item():
    async with AsyncClient(
        transport=ASGITransport(app=app), base_url="http://t"
    ) as c:
        r = await c.post("/items", json={"name": "tea", "price": 3.5})
        assert r.status_code == 201
        assert r.json()["name"] == "tea"

# 3 · Override a dependency
from app import require_token
def fake_token():
    return {"user": "tester"}
app.dependency_overrides[require_token] = fake_token
TestClient(app)Sync wrapper. Runs the app in a thread. Best for quick checks.
AsyncClient(transport=ASGITransport(app=app))Real async transport. Use for awaiting handlers concurrently.
app.dependency_overrides[dep] = fakePer-test DI override. Reset between tests.
pytest-asyncio + @pytest.mark.asyncioRun async tests.
client.cookies / client.headersPersistent across requests for session-style tests.

Async SQLAlchemy in ~30 linesEnd-to-end · CRUD with SQLAlchemy

A real-shape app: async engine, session-per-request dependency, lifespan-managed schema, typed request/response models.

python
from contextlib import asynccontextmanager
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

DB_URL = "sqlite+aiosqlite:///./app.db"
engine = create_async_engine(DB_URL)
Session = async_sessionmaker(engine, expire_on_commit=False)

class Base(DeclarativeBase): pass
class Note(Base):
    __tablename__ = "notes"
    id: Mapped[int] = mapped_column(primary_key=True)
    text: Mapped[str] = mapped_column(index=True)

class NoteIn(BaseModel):
    text: str = Field(min_length=1, max_length=200)

class NoteOut(NoteIn):
    id: int

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

app = FastAPI(lifespan=lifespan)

async def db() -> AsyncSession:
    async with Session() as s:
        yield s

DB = Annotated[AsyncSession, Depends(db)]

@app.post("/notes", response_model=NoteOut, status_code=201)
async def create(payload: NoteIn, s: DB):
    n = Note(text=payload.text)
    s.add(n); await s.commit(); await s.refresh(n)
    return n

@app.get("/notes/{nid}", response_model=NoteOut)
async def get(nid: int, s: DB):
    n = await s.get(Note, nid)
    if not n: raise HTTPException(404, "not found")
    return n

Best practiceGood to know

Separate request models from response models. Define ItemIn for what you accept and ItemOut for what you return. Pin response_model on every route — it filters extra fields and keeps the OpenAPI doc honest.
Use Annotated[T, Depends(…)], not default values. It survives reordering arguments and lets you alias dependencies as types: DB = Annotated[AsyncSession, Depends(db)].
async def only if the handler is actually async. A plain def handler runs on a threadpool — that’s correct for blocking libs (psycopg2, requests). An async def that calls blocking code stalls the entire event loop.

Common trapsWatch out for

BackgroundTasks isn’t a job queue. It runs in the same process, after the response. A crash or restart loses the work. For anything important use Celery, RQ, or arq.
Don’t open a DB session per call inside a handler. Use a yield-based dependency that opens once, commits/rolls back, and closes. Calling Session() directly in every handler leaks connections under load.
CORS preflights silently fail on missing methods. allow_origins=["*"] with allow_credentials=True is invalid — the browser drops the response. Either set explicit origins or drop credentials.

Go deeperSee also

FastAPI FAQ

What is FastAPI?

FastAPI is a modern Python web framework for building APIs with automatic OpenAPI documentation. It is built on Starlette and Pydantic, supports async/await natively, and generates interactive Swagger UI docs at /docs and ReDoc at /redoc without any extra configuration. It is one of the fastest Python frameworks available.

How does FastAPI use Pydantic?

FastAPI uses Pydantic models for request body validation, response serialisation, and settings management. Declare a class inheriting from pydantic.BaseModel, annotate fields with types, and FastAPI automatically validates incoming JSON against the schema, returning a 422 Unprocessable Entity response with error details on validation failure.

How does dependency injection work in FastAPI?

Declare a function with typed parameters and pass it to the Depends() constructor in a route signature. FastAPI resolves the dependency automatically at request time, injecting the return value into the route handler. Dependencies can be nested, can yield (for teardown), and can be scoped to a request, a router, or the entire application.

How does FastAPI handle async endpoints?

Define route handlers with async def to run them on the event loop without blocking. Use await for I/O-bound operations like database queries or HTTP calls to external services. For CPU-bound work, use run_in_executor to offload to a thread pool. Mixing sync and async is allowed; FastAPI runs sync handlers in a thread pool automatically.

How do I test a FastAPI app?

Use httpx.AsyncClient or the synchronous TestClient from fastapi.testclient to make requests against your app in tests. Instantiate TestClient(app) and call .get(), .post() etc. directly. Override dependencies with app.dependency_overrides to inject mocks. FastAPI works with pytest out of the box and integrates with pytest-asyncio for async tests.