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).
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).
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 appSee 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.
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 -> 422See 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.
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 strippedSee 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.
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.
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).
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.
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.jsonSee 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 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
| 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 |
| 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()] |
| 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 |
| 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 /docsTyped 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}declareditem_id: intcoerces/items/42to the int42; a bad type like/items/abcis rejected with a422before your handler runs, no manual parsing required. - Prefer
Annotated[...]for params. UseAnnotated[int, Path(ge=1)]andAnnotated[str | None, Query()]; the legacy default-value spelling (q: str = Query(...)) still works but is no longer the taught style. response_modelshapes the way out. Returning extra fields is fine;response_model=ItemOutfilters the response down to the declared fields, so asecretnever leaks.defvsasync defboth work. Writeasync deffor awaitable I/O so other requests proceed while it waits; write a plaindeffor blocking work and FastAPI offloads it to a thread pool so it still does not block the event loop.- Use
lifespan, noton_event. Manage startup and shutdown withFastAPI(lifespan=...); the@app.on_event("startup")and"shutdown"decorators are deprecated. - FastAPI requires Pydantic v2. Use
model_dump(),model_dump_json(),model_validate(), andConfigDict, not the v1.dict(),.json(),parse_obj, or innerclass Config. - Docs come for free. OpenAPI is emitted as 3.1.0 at
/openapi.json, with Swagger UI at/docsand ReDoc at/redoc, no configuration needed.
References
FastAPI documentation (current)
- Documentation home and the Tutorial - User Guide
- Learn (concepts, async, CLI), the API Reference, and the Release notes
- First steps, Bigger applications, Path params, Query params
- Request body, Response model, Dependencies, Handling errors
- Concurrency and async, Lifespan events, Background tasks
- Features, Configure Swagger UI, Run a server manually, FastAPI CLI
Project and related standards