FastAPI Cheatsheet

A visual guide to FastAPI covering route decorators, typed path and query params, Pydantic request and response models, dependency injection, status codes and errors, async handlers, automatic OpenAPI docs, and running with uvicorn.

python
fastapi
cheatsheet
Author

James Balamuta

Published

July 2, 2026

FastAPI is the modern Python framework for producing HTTP APIs from type hints. Where the requests sheet is about consuming HTTP (a client fires a verb at someone else’s server and reads a Response), this sheet is the mirror image: you now write the endpoint on the other end of that arrow. The recurring mental model is one picture: an incoming Request envelope (method plus path) flows along a gray arrow into an @app-decorated route, FastAPI validates the raw bytes into typed Python (path and query params, a Pydantic body model), the handler runs, and the return value leaves as a Response tagged with a status-code chip colored by class (green 2xx, amber 3xx, red 4xx and 5xx), the same chips the requests sheet reads, now generated by your own code. The conventional imports are from fastapi import FastAPI, APIRouter, Path, Query, Body, Depends, HTTPException, status, BackgroundTasks, Request and from pydantic import BaseModel, Field, and everything here is FastAPI on Pydantic v2 (legacy and deprecated spellings are flagged per section).

Complete FastAPI cheatsheet (light mode): eight panels covering route decorators, typed path and query params, Pydantic request and response models, dependency injection, status codes and errors, async handlers and background work, automatic OpenAPI docs, and running with uvicorn.

Complete FastAPI cheatsheet (dark mode): eight panels covering route decorators, typed path and query params, Pydantic request and response models, dependency injection, status codes and errors, async handlers and background work, automatic OpenAPI docs, and running with uvicorn.

Download the full cheatsheet

All eight panels in a single, printable SVG.

Light SVG Dark SVG

Routes

A FastAPI app is an app = FastAPI() object decorated with one path operation per endpoint: @app.get("/items"), @app.post(...), @app.put(...), @app.patch(...), @app.delete(...), each binding an HTTP method and path to the function right below it. As an app grows, group related endpoints in an APIRouter (with a shared prefix and tags) and pull them in with app.include_router(router).

FastAPI routes panel: create the app, GET endpoint, POST endpoint, PUT/PATCH/DELETE on a path, group routes in an APIRouter and include it.

One decorator per HTTP method maps a path to a function.

FastAPI routes panel: create the app, GET endpoint, POST endpoint, PUT/PATCH/DELETE on a path, group routes in an APIRouter and include it.

One decorator per HTTP method maps a path to a function.
from fastapi import FastAPI, APIRouter

app = FastAPI(title="My API", version="1.0")   # the application object

@app.get("/items")            # bind GET /items
def read_items(): ...

@app.post("/items")           # bind POST /items (typically -> 201)
def make_item(): ...

@app.put("/items/{id}")       # also @app.patch(...), @app.delete(...)
def replace_item(id: int): ...

router = APIRouter(prefix="/v1", tags=["items"])   # group related routes
app.include_router(router)                         # mount them on the app

See First steps and Bigger applications for routers.

Params

FastAPI reads your function’s type hints and turns them into validation: a {item_id} in the path declared as item_id: int is parsed and coerced to an integer, and a function argument that is not in the path becomes a query parameter. Wrap the hint in Annotated[int, Path(ge=1)] or Annotated[str | None, Query(max_length=50)] to add constraints, and any value that fails (or a bad type like abc for an int) is rejected automatically with a 422 before your code runs.

FastAPI params panel: typed path parameter, constrain a path param with Path(ge=1), optional query param, bounded query param, automatic type coercion, bad type rejected with 422.

Type hints become validation; declare path and query with Annotated.

FastAPI params panel: typed path parameter, constrain a path param with Path(ge=1), optional query param, bounded query param, automatic type coercion, bad type rejected with 422.

Type hints become validation; declare path and query with Annotated.
from fastapi import FastAPI, Path, Query
from typing import Annotated

app = FastAPI()

@app.get("/items/{item_id}")
def get_item(
    item_id: Annotated[int, Path(ge=1)],                   # path, must be >= 1
    q: Annotated[str | None, Query(max_length=50)] = None, # optional query
    limit: Annotated[int, Query(ge=1, le=100)] = 10,       # bounded query
):
    return {"item_id": item_id, "q": q, "limit": limit}

# GET /items/42?limit=5 -> 200 ;  /items/0 -> 422 ;  /items/abc -> 422

See Path params and numeric validations and Query params and validations.

Models

Declare a pydantic.BaseModel and use it as a parameter type to accept and validate a JSON request body: FastAPI parses the bytes, checks every field (Field(gt=0), required vs defaulted), and hands your function a typed object, or returns a 422 listing exactly which field failed. Set response_model=ItemOut on the decorator to shape and filter what goes back out (stripping secrets, enforcing the output schema), and both models feed the automatic docs.

FastAPI models panel: define a request model, accept it as the body, reject an invalid body with 422, shape the response with response_model, extra body field controls, validated docs for free.

A BaseModel validates the JSON body in and shapes the JSON out.

FastAPI models panel: define a request model, accept it as the body, reject an invalid body with 422, shape the response with response_model, extra body field controls, validated docs for free.

A BaseModel validates the JSON body in and shapes the JSON out.
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class ItemIn(BaseModel):
    name: str
    price: float = Field(gt=0)     # validated: price must be > 0
    tags: list[str] = []

class ItemOut(BaseModel):
    id: int
    name: str
    price: float

@app.post("/items", response_model=ItemOut)   # filter the output shape
def make(item: ItemIn):                        # parse + validate the body
    return {"id": 1, **item.model_dump(), "secret": "hidden"}  # secret stripped

See Request body and Response model for output shaping.

Depends

Dependency injection lets a route declare what it needs (a database session, the current user, shared query params) as Annotated[T, Depends(provider)], and FastAPI calls the provider and injects the result for you, resolving nested sub-dependencies once per request. A provider that uses yield gives you setup-and-teardown around the handler (open then close a connection), and listing dependencies=[Depends(guard)] on the decorator runs a check (like auth) without passing a value into the function.

FastAPI depends panel: define a dependency, inject it into a route, reuse across many routes, setup and teardown with yield, route-level guard, layered sub-dependencies.

Declare what a route needs; FastAPI builds and injects it.

FastAPI depends panel: define a dependency, inject it into a route, reuse across many routes, setup and teardown with yield, route-level guard, layered sub-dependencies.

Declare what a route needs; FastAPI builds and injects it.
from fastapi import FastAPI, Depends, HTTPException, Query
from typing import Annotated

app = FastAPI()

def common(q: str | None = None, page: int = 1):   # a reusable provider
    return {"q": q, "page": page}

def get_db():
    db = connect()
    try:
        yield db          # injected into the handler
    finally:
        db.close()        # teardown, always runs

def verify_token(x_token: Annotated[str | None, Query()] = None):
    if x_token != "secret":
        raise HTTPException(status_code=401, detail="bad token")

@app.get("/search")
def search(params: Annotated[dict, Depends(common)],
           db: Annotated[dict, Depends(get_db)]):
    return {"params": params}

@app.get("/admin", dependencies=[Depends(verify_token)])  # guard, no value passed
def admin():
    return {"ok": True}

See Dependencies.

Errors

Set the happy-path code with status_code= on the decorator (use the status.HTTP_* constants for readability), and signal a failure by raising HTTPException(status_code=..., detail=...), which FastAPI turns into a JSON {"detail": ...} response with that status and any headers you attach. Validation failures (bad body or params) become a structured 422 automatically, and @app.exception_handler(...) lets you convert your own exception types into shaped responses.

FastAPI errors panel: set the success status, named status constants, raise a client error, add headers to the error, automatic validation errors, custom exception handler.

Set the success code; raise HTTPException for the rest.

FastAPI errors panel: set the success status, named status constants, raise a client error, add headers to the error, automatic validation errors, custom exception handler.

Set the success code; raise HTTPException for the rest.
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post("/items", status_code=status.HTTP_201_CREATED)  # set success code
def make_item():
    return {"ok": True}

@app.get("/items/{item_id}")
def read_item(item_id: int):
    if item_id == 0:
        raise HTTPException(                  # -> {"detail": "not found"}
            status_code=status.HTTP_404_NOT_FOUND,
            detail="not found",
            headers={"WWW-Authenticate": "Bearer"},   # attach headers too
        )
    return {"item_id": item_id}

@app.exception_handler(ValueError)            # shape your own exceptions
def handle(request, exc):
    return JSONResponse(status_code=400, content={"detail": str(exc)})

See Handling errors.

Async

Write a handler as async def when it awaits async I/O (a database driver, an HTTP client) so the server can serve other requests while it waits; write a plain def for blocking work and FastAPI runs it in a thread pool so it still does not block the event loop. Queue work to run after the response with BackgroundTasks, and manage app-wide startup and shutdown with a lifespan async context manager passed to FastAPI(lifespan=...) (the old @app.on_event decorators are deprecated).

FastAPI async panel: async handler, plain sync handler in a threadpool, await a dependency, run work after responding with BackgroundTasks, startup and shutdown with lifespan, avoid the deprecated on_event.

async def for awaitable I/O; plain def runs in a threadpool.

FastAPI async panel: async handler, plain sync handler in a threadpool, await a dependency, run work after responding with BackgroundTasks, startup and shutdown with lifespan, avoid the deprecated on_event.

async def for awaitable I/O; plain def runs in a threadpool.
from contextlib import asynccontextmanager
from fastapi import FastAPI, BackgroundTasks

@asynccontextmanager
async def lifespan(app: FastAPI):
    print("startup: open pools, load models")   # runs on enter
    yield
    print("shutdown: close pools")               # runs on exit

app = FastAPI(lifespan=lifespan)

@app.get("/")
async def read():
    return await fetch()        # awaitable I/O, non-blocking

@app.post("/log", status_code=202)
def make_log(bg: BackgroundTasks):
    bg.add_task(send_email, "to@example.com")   # runs after the response
    return {"queued": True}

See Concurrency and async, Lifespan events, and Background tasks.

Docs

Because your routes, params, and Pydantic models are fully typed, FastAPI generates an OpenAPI 3.1 schema at /openapi.json with no extra work, and serves two interactive doc UIs out of the box: Swagger UI at /docs (with “try it out”) and ReDoc at /redoc. Enrich the docs with tags, summary, and per-field Field(description=..., examples=...), or relocate and disable the doc routes via docs_url= and redoc_url= on the app.

FastAPI docs panel: interactive Swagger UI at /docs, alternative ReDoc at /redoc, the raw schema at /openapi.json, describe routes with tags and summary, document model fields, customize or disable the docs.

Your types generate an OpenAPI schema and live docs, free.

FastAPI docs panel: interactive Swagger UI at /docs, alternative ReDoc at /redoc, the raw schema at /openapi.json, describe routes with tags and summary, document model fields, customize or disable the docs.

Your types generate an OpenAPI schema and live docs, free.
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(docs_url="/api-docs", redoc_url=None)   # relocate Swagger, drop ReDoc

class Item(BaseModel):
    name: str = Field(examples=["Ada"], description="The item name")

@app.get("/items", tags=["items"], summary="List items")  # group + title in docs
def list_items(): ...

# Swagger UI  -> http://127.0.0.1:8000/api-docs
# OpenAPI 3.1 -> http://127.0.0.1:8000/openapi.json

See Features and Configure Swagger UI.

Run

FastAPI ships a fastapi CLI: fastapi dev main.py serves with auto-reload for development, and fastapi run main.py serves for production, both built on the uvicorn ASGI server. You can also call uvicorn directly (uvicorn main:app --reload, or uvicorn.run("main:app", ...) from a __main__ guard) and scale it with --workers; whichever you use, the interactive docs are live at /docs the moment the server starts.

FastAPI run panel: develop with auto-reload, run for production, explicit uvicorn invocation, run from Python with a __main__ guard, scale with workers, where the docs live.

fastapi dev to develop, fastapi run (uvicorn) to serve.

FastAPI run panel: develop with auto-reload, run for production, explicit uvicorn invocation, run from Python with a __main__ guard, scale with workers, where the docs live.

fastapi dev to develop, fastapi run (uvicorn) to serve.
fastapi dev main.py            # development: auto-reload, http://127.0.0.1:8000
fastapi run main.py            # production: no reload, binds 0.0.0.0:8000
uvicorn main:app --reload --port 8000   # direct uvicorn invocation
uvicorn main:app --workers 4            # scale with worker processes
# Or run from inside the module
import uvicorn

if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)

See Run a server manually and the FastAPI CLI.

Quick Reference

Key FastAPI calls.
Command What it does Area
app = FastAPI() Create the application object Routes
@app.get("/path") Bind GET (also post/put/patch/delete) Routes
APIRouter(prefix=..., tags=...) Group routes, then include_router Routes
item_id: int (in path) Typed, validated path parameter Params
Annotated[int, Path(ge=1)] Constrain a path parameter Params
Annotated[str \| None, Query()] = None Optional, validated query parameter Params
item: ItemIn (a BaseModel) Parse and validate the JSON body Models
response_model=ItemOut Shape and filter the response Models
Annotated[T, Depends(provider)] Inject a dependency Depends
dependencies=[Depends(guard)] Run a check with no injected value Depends
status_code=status.HTTP_201_CREATED Set the success status Errors
raise HTTPException(404, detail=...) Return a client or server error Errors
async def handler(): await ... Async, non-blocking handler Async
BackgroundTasks.add_task(fn, ...) Run work after responding Async
FastAPI(lifespan=lifespan) App startup and shutdown Async
/docs, /redoc, /openapi.json Auto docs and schema Docs
fastapi dev main.py Serve with auto-reload Run
fastapi run main.py Serve for production Run
uvicorn main:app --reload Direct uvicorn invocation Run
Where each parameter is read from.
Declared as Comes from Example
In the path {...} URL path @app.get("/items/{id}"), id: int
Plain function arg Query string q: str = None -> ?q=...
A BaseModel arg JSON request body item: ItemIn
Annotated[..., Body()] A single body field q: Annotated[str, Body()]
Annotated[..., Depends(...)] A dependency provider db: Annotated[Session, Depends(get_db)]
Annotated[..., Header()] A request header x_token: Annotated[str, Header()]
HTTP status code classes.
Class Range Meaning Color cue
2xx 200 to 299 Success green
3xx 300 to 399 Redirect amber
4xx 400 to 499 Client error (their request) red
5xx 500 to 599 Server error (your handler) red
422 (a 4xx) Validation failed (FastAPI built it) red
Modern FastAPI and Pydantic v2 spellings.
Use this (current) Avoid (deprecated or legacy)
q: Annotated[str, Query()] q: str = Query(...) (legacy default style)
FastAPI(lifespan=lifespan) @app.on_event("startup") / "shutdown"
model.model_dump() model.dict() (Pydantic v1)
model.model_dump_json() model.json() (Pydantic v1)
model_config = ConfigDict(...) inner class Config: (Pydantic v1)
fastapi run main.py (none; this is the new CLI, uvicorn still valid)

Appendix: Sample Code

A complete minimal app (the Request to Response mental model)

from fastapi import FastAPI

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

@app.get("/")
def read_root():
    return {"message": "hello"}

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

# fastapi dev main.py   ->  http://127.0.0.1:8000  and docs at /docs

Typed params with validation

from fastapi import FastAPI, Path, Query
from typing import Annotated

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(
    user_id: Annotated[int, Path(ge=1)],                   # path, must be >= 1
    q: Annotated[str | None, Query(max_length=50)] = None, # optional query
    limit: Annotated[int, Query(ge=1, le=100)] = 10,       # bounded query
):
    return {"user_id": user_id, "q": q, "limit": limit}

# GET /users/0          -> 422 (fails ge=1)
# GET /users/3?limit=999 -> 422 (fails le=100)
# GET /users/3?limit=5  -> 200 {"user_id": 3, "q": null, "limit": 5}

Pydantic request and response models

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class ItemIn(BaseModel):
    name: str
    price: float = Field(gt=0)
    tags: list[str] = []

class ItemOut(BaseModel):
    id: int
    name: str
    price: float

@app.post("/items", response_model=ItemOut, status_code=201)
def create_item(item: ItemIn):
    # return value may carry extra fields; response_model filters them out
    return {"id": 1, "name": item.name, "price": item.price, "secret": "hidden"}

# POST {"name": "x", "price": 9.5} -> 201 {"id": 1, "name": "x", "price": 9.5}
# POST {"name": "x", "price": -1}  -> 422  (price must be > 0; "secret" never leaks)

Dependencies, including setup and teardown with yield

from fastapi import FastAPI, Depends, HTTPException, Query
from typing import Annotated

app = FastAPI()

def common_params(q: str | None = None, page: int = 1):
    return {"q": q, "page": page}

CommonDep = Annotated[dict, Depends(common_params)]

def get_db():
    db = {"conn": "open"}      # setup
    try:
        yield db               # injected into the handler
    finally:
        db["conn"] = "closed"  # teardown, always runs

def verify_token(x_token: Annotated[str | None, Query()] = None):
    if x_token != "secret":
        raise HTTPException(status_code=401, detail="bad token")

@app.get("/search")
def search(params: CommonDep, db: Annotated[dict, Depends(get_db)]):
    return {"params": params, "db": db}

@app.get("/admin", dependencies=[Depends(verify_token)])  # guard, no value passed
def admin():
    return {"ok": True}

Errors, async, background tasks, and lifespan

from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, BackgroundTasks, status
import asyncio

@asynccontextmanager
async def lifespan(app: FastAPI):
    print("startup: open pools, load models")
    yield
    print("shutdown: close pools")

app = FastAPI(lifespan=lifespan)

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    await asyncio.sleep(0)                 # await real async I/O here
    if item_id == 0:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="not found",
            headers={"X-Error": "missing"},
        )
    return {"item_id": item_id}

def write_log(message: str):
    ...  # runs after the response is sent

@app.post("/log", status_code=202)
def make_log(bg: BackgroundTasks):
    bg.add_task(write_log, "logged")
    return {"queued": True}

Running the server

# Development: auto-reload, opens on http://127.0.0.1:8000, docs at /docs
fastapi dev main.py

# Production: no reload, binds 0.0.0.0:8000
fastapi run main.py

# Direct uvicorn (equivalent), with reload and a chosen port
uvicorn main:app --reload --port 8000

# Scale with multiple worker processes
uvicorn main:app --workers 4
# Or run from inside the module
import uvicorn

if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)

Behavior notes

  • Type hints are the validation. A {item_id} declared item_id: int coerces /items/42 to the int 42; a bad type like /items/abc is rejected with a 422 before your handler runs, no manual parsing required.
  • Prefer Annotated[...] for params. Use Annotated[int, Path(ge=1)] and Annotated[str | None, Query()]; the legacy default-value spelling (q: str = Query(...)) still works but is no longer the taught style.
  • response_model shapes the way out. Returning extra fields is fine; response_model=ItemOut filters the response down to the declared fields, so a secret never leaks.
  • def vs async def both work. Write async def for awaitable I/O so other requests proceed while it waits; write a plain def for blocking work and FastAPI offloads it to a thread pool so it still does not block the event loop.
  • Use lifespan, not on_event. Manage startup and shutdown with FastAPI(lifespan=...); the @app.on_event("startup") and "shutdown" decorators are deprecated.
  • FastAPI requires Pydantic v2. Use model_dump(), model_dump_json(), model_validate(), and ConfigDict, not the v1 .dict(), .json(), parse_obj, or inner class Config.
  • Docs come for free. OpenAPI is emitted as 3.1.0 at /openapi.json, with Swagger UI at /docs and ReDoc at /redoc, no configuration needed.

References

FastAPI documentation (current)

Project and related standards