FastAPI is a modern, high-performance Python web framework for building APIs based on standard Python type hints. It's built on top of Starlette for web handling and Pydantic for data validation, providing automatic API documentation, async support, and excellent developer experience. FastAPI excels at creating production-ready RESTful APIs quickly while maintaining type safety and performance. One key distinction: FastAPI's automatic validation and documentation generation from type hints eliminates boilerplate code that other frameworks require manually, making it both fast to develop with and fast to execute.
19 tables, 155 concepts. Select a concept node to jump to its table row.
Table 1: Core Application Setup
Everything starts with the FastAPI() instance and the path-operation decorators that hang routes off it. These are the pieces you write in your very first file — creating the app, defining an endpoint, choosing sync or async, and wiring up startup logic with a lifespan handler. The automatic /docs and Pydantic integration listed here are also why FastAPI feels productive from line one.
| Concept | Example | Description | |
|---|---|---|---|
app = FastAPI()app = FastAPI(title="My API", version="1.0.0") | • Creates the main application instance • accepts optional metadata like title, description, version for OpenAPI docs. | ||
| • Defines an HTTP endpoint with method and path • decorator tells FastAPI to handle requests to this route • path operations are matched in declaration order, so a fixed path like /users/me must be declared before a dynamic one like /users/{user_id} or the dynamic route shadows it. | ||
def read_root(): return {"msg": "Hello"} | • Function executed when endpoint is hit • return value is automatically serialized to JSON. | ||
fastapi dev main.pyfastapi run main.py | • Recommended way to run FastAPI (included in fastapi[standard])• dev for development (auto-reload, binds localhost only); run for production (no reload, binds all interfaces). | ||
uvicorn main:app --reload (dev)uvicorn main:app --host 0.0.0.0 --port 80 (manual/production) | • Installs and runs the ASGI server directly, useful when you want a different ASGI server or flags the fastapi CLI does not expose• fastapi dev/fastapi run remain the recommended default, including for Docker deployments. | ||
async def read_items(): return await db.fetch() | • Uses async def for non-blocking I/O operations, running directly on the event loop• a plain def function instead runs in an external threadpool, which is why blocking calls belong there, not inside async def. | ||
from contextlib import asynccontextmanagerasync def lifespan(app): # startup yield # shutdownapp = FastAPI(lifespan=lifespan) | • Manages startup and shutdown logic in a single function split by yield• replaces the deprecated on_event decorators; setting lifespan disables on_event entirely, it is one or the other, never both. | ||
/docs for Swagger UI/redoc for ReDoc | • FastAPI auto-generates interactive API docs at these endpoints from your existing type hints and decorators • requires zero configuration, no separate spec file to maintain. | ||
class Item(BaseModel): name: str price: float | • Pydantic models provide automatic validation and serialization on the request body itself • FastAPI rejects a non-matching body before your function runs, this is separate from response_model, which only shapes the outgoing response. |
Table 2: HTTP Methods and Route Definitions
Each HTTP method maps to a decorator and a conventional meaning: GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes. Following these conventions keeps your API predictable and lets caches, browsers, and other tools behave correctly. The lesser used HEAD and OPTIONS round out the set for existence checks and CORS handling.
| Method | Example | Description | |
|---|---|---|---|
def read_item(id: int): | • Retrieves data • no request body, typically uses path or query parameters. | ||
def create_item(item: Item): | • Creates new resource • accepts request body defined by Pydantic model. | ||
def update_item(id: int, item: Item): | • Replaces entire resource • expects complete object in request body. | ||
def partial_update(id: int, item: PartialItem): | • Updates partial resource • only specified fields are modified. | ||
def delete_item(id: int): | • Removes resource • typically returns status confirmation. | ||
def items_head(): | • Returns headers only, no body • useful for checking resource existence or metadata. | ||
def items_options(): | • Returns allowed HTTP methods for endpoint • often handled automatically for CORS. |
Table 3: Request Parameters
Data reaches your endpoint from several places — the URL path, the query string, the JSON body, headers, cookies, form fields, and uploaded files — and FastAPI pulls each one in just by declaring it as a typed function argument. The standout feature is that the type annotation alone drives parsing and validation, so a path parameter declared as int is automatically rejected if someone passes text. Knowing which source maps to which declaration is the core skill this table builds.
| Type | Example | Description | |
|---|---|---|---|
from typing import Annotatedq: Annotated[str | None, Query(max_length=50)] = None | • Recommended modern syntax (since FastAPI 0.95.0) for declaring parameters with validation • keeps type annotation and metadata together. | ||
def read(item_id: int): | • Extracts value from URL path • type validated automatically based on annotation. | ||
def read_items(skip: int = 0, limit: int = 10): | • Reads from URL query string ( ?skip=0&limit=10)• optional with defaults. | ||
def create(item: Item): | • Accepts JSON payload • Pydantic model validates structure and types automatically. | ||
def create(item: Item, user: User): | • Accepts multiple JSON objects in request body • FastAPI expects nested structure. | ||
from fastapi import Headerdef read(user_agent: str = Header()): | • Reads HTTP headers • automatically converts your snake_case parameter name to hyphen-case to match the header. | ||
from fastapi import Cookiedef read(session_id: str = Cookie()): | • Extracts cookie values • type-validated like other parameters. | ||
from fastapi import Formdef login(username: str = Form()): | • Accepts application/x-www-form-urlencoded• requires python-multipart installed. | ||
from fastapi import UploadFiledef upload(file: UploadFile): | • Handles file uploads • UploadFile provides async methods and metadata like filename. | ||
def upload(files: list[UploadFile]): | • Accepts multiple files in single request • validated as list. |
Table 4: Parameter Validation
Beyond checking types, you often need real constraints too: a minimum length, a numeric range, a regex pattern, a required versus optional rule. The modern Annotated syntax shown in the first rows is the recommended way to attach these rules while keeping the type and its metadata together. Push validation this far up the stack, and your endpoint body can trust the data is already clean.
| Technique | Example | Description | |
|---|---|---|---|
from typing import Annotatedq: Annotated[str | None, Query(min_length=3)] = None | • Recommended way to use Query, Path, Header, Cookie • type annotation and validation rules stay together. | ||
from fastapi import Queryq: str = Query(min_length=3) | Applies constraints like min/max length, regex patterns to query parameters. | ||
from fastapi import Pathid: int = Path(ge=1, le=1000) | Validates path parameters with numeric constraints like ge (greater/equal), le (less/equal). | ||
q: Annotated[str, Query(min_length=1)] | • Modern approach: no default = required when using Annotated • older approach: q: str = Query(...) with ellipsis. | ||
q: str | None = None | • Python 3.10+ union syntax for optional parameters • older versions use Optional[str]. | ||
tags: list[str] = Query([]) | • Accepts multiple values for same parameter ( ?tags=a&tags=b)• validated as list. | ||
item_query: str = Query(alias="item-query") | Maps hyphenated URL param to snake_case Python variable. | ||
old_param: str = Query(deprecated=True) | • Marks parameter as deprecated in OpenAPI docs • still functional but discouraged. | ||
code: str = Query(pattern="^[A-Z]{3}$") | Validates string parameter against regex pattern. | ||
price: float = Query(gt=0, le=9999.99) | Applies gt (greater than), lt (less than), ge, le constraints to numbers. |
Table 5: Pydantic Models and Validation
Pydantic models are the heart of how FastAPI validates and serializes structured data, a BaseModel subclass defines the shape of a request or response, and FastAPI does the rest. This table covers the tools for going beyond simple fields: per-field constraints, custom field and model validators for logic that types can't express, computed fields, and model inheritance for reuse. Note this is Pydantic v2, so model_config and field_validator replace the older patterns you may have seen.
| Concept | Example | Description | |
|---|---|---|---|
class Item(BaseModel): name: str price: float | • Base class for all Pydantic models • provides automatic validation and serialization. | ||
from pydantic import Fieldprice: float = Field(gt=0) | • Adds validation rules directly to model fields • similar to Query/Path but for models. | ||
name: str = Field(description="Item name") | • Adds documentation to field • appears in OpenAPI schema. | ||
price: float = Field(examples=[9.99, 19.99]) | Provides example values for API documentation and testing. | ||
def check_email(cls, v): | • Custom validation logic for specific field • runs after type validation. | ||
def check_passwords(self): | • Validates entire model after individual fields • useful for cross-field validation. | ||
from pydantic import ConfigDictmodel_config = ConfigDict(str_strip_whitespace=True) | • Configures model behavior using Pydantic v2's ConfigDict• replaces the old inner class Config. | ||
description: str | None = None | • Field can be omitted or null • uses Python union type or Optional. | ||
def total(self) -> float: | • Adds calculated field to model • included in serialization but not validation. | ||
class ExtendedItem(Item): category: str | • Inherits fields from base model • enables model reuse and extension. |
Table 6: Response Configuration
Controlling what goes back to the client is just as important as parsing what comes in. The response_model and return-type annotation filter and validate your output, handy for stripping a password field before it ever leaves the server, while the various response classes let you return HTML, files, redirects, or streamed data instead of plain JSON. Status codes, custom headers, and cookies round out the toolkit for shaping a precise response.
| Technique | Example | Description | |
|---|---|---|---|
| • Specifies return type for validation and docs • filters response to match model. | ||
async def create_item(item: Item) -> Item: | • Modern preferred approach for response type declaration • FastAPI uses the return annotation for validation, filtering, and docs. | ||
response_model=User, response_model_exclude={"password"} | • Removes specified fields from the serialized response only • the field is still fetched and held in memory first, so pair it with a separate output model for real secrets. | ||
| • Sets HTTP status code for the response • FastAPI's default is 200 for every method, 201 for POST is only a convention you set explicitly. | ||
from fastapi.responses import JSONResponsereturn JSONResponse(content=data) | Explicit JSON response with custom headers or status code. | ||
from fastapi.responses import HTMLResponsereturn HTMLResponse(content=html) | • Returns HTML content • sets Content-Type to text/html. | ||
from fastapi.responses import RedirectResponsereturn RedirectResponse(url="/new") | • HTTP redirect to different URL • status_code defaults to 307. | ||
from fastapi.responses import FileResponsereturn FileResponse(path="file.pdf") | • Serves static files • handles content-type and download headers automatically. | ||
from fastapi.responses import StreamingResponsereturn StreamingResponse(generator) | • Streams large data in chunks • memory-efficient for big responses. | ||
from fastapi.responses import ORJSONResponsereturn ORJSONResponse(content=data) | • Uses the third party orjson library for JSON serialization • a faster alternative response class, not a difference in what data it returns. | ||
from fastapi import Responseresponse.headers["X-Custom"] = "value" | Sets custom HTTP headers in response. | ||
response.set_cookie(key="session", value="abc") | • Adds cookie to response • supports httponly, secure, samesite options. |
Table 7: Dependency Injection
Dependency injection is one of FastAPI's most loved features — Depends() lets you factor out reusable logic like database sessions, authentication, and pagination, then declare it as a parameter wherever you need it. Dependencies can nest, run as yield generators that clean up after the response, apply at the route, router, or app level, and be swapped out entirely for testing. Lean on this pattern and your endpoints stay thin while shared concerns live in one place.
| Pattern | Example | Description | |
|---|---|---|---|
from typing import Annotateddef route(db: Annotated[DB, Depends(get_db)]): | • Function providing reusable logic • recommended Annotated syntax (since 0.95.0) keeps type and dependency together. | ||
DBDep = Annotated[Session, Depends(get_db)]def endpoint(db: DBDep): | • Defines reusable type alias for common dependency • reduces repetition across many routes. | ||
async def get_session(): async with Session() as s: yield s | • Async generator dependency • sync ( def) and async (async def) dependencies can be freely mixed in the same app, FastAPI runs each correctly. | ||
class Pagination: def __init__(self, skip=0): | • Class instances as dependencies • __init__ parameters become sub-dependencies, validated the same way as path operation parameters, and give the editor type completion a dict cannot. | ||
def get_user(token = Depends(get_token)): | • Dependency can depend on other dependencies • creates dependency chain; a sub-dependency shared by several dependants in one request still runs only once per request. | ||
app = FastAPI(dependencies=[Depends(verify)]) | • Applied to all routes in application • useful for auth, logging; combines with router level and route level dependencies rather than overriding them. | ||
router = APIRouter(dependencies=[Depends(check)]) | • Applied to all routes in router • scoped to specific module; when a route also has its own dependencies, FastAPI resolves the router's first, then the path decorator's, then normal parameter dependencies. | ||
def get_db(): db = Database() yield db db.close() | • Provides resource cleanup after the response is already sent, not before • if the code after yield catches an exception without re-raising it, FastAPI never learns the exception happened. | ||
app.dependency_overrides[get_db] = mock_db | • Replaces dependency for testing • a plain dict swap, no route code changes • stays active until you manually reset it with app.dependency_overrides = {}. | ||
| • Dependency executed but result not used in function • runs unconditionally for its side effects only, such as raising an error on an invalid header. |
Table 8: Error Handling
When something goes wrong, you want a clean, consistent error response rather than a raw stack trace. Raising HTTPException is the everyday move for returning a 404 or 403, while custom exceptions paired with exception handlers let you centralize how whole categories of errors turn into responses. You can even override FastAPI's default validation-error format when you need errors shaped a particular way.
| Approach | Example | Description | |
|---|---|---|---|
from fastapi import HTTPExceptionraise HTTPException(status_code=404, detail="Not found") | • Built-in exception for HTTP errors • caught by a default handler and formatted as JSON automatically, no setup needed. | ||
class CustomError(Exception): pass | • Define a custom exception class by subclassing plain Exception• does nothing on its own until paired with a handler. | ||
def handler(request, exc): | • Catches a specific exception type globally, and any subclass of it too • returns a custom response. | ||
from fastapi.exceptions import RequestValidationError | • Customizes Pydantic's 422 validation error response format • needs its own registration, overriding HTTPException's handler alone does not affect it. | ||
raise HTTPException(status_code=403, detail="Forbidden") | • Sets the HTTP status code directly when raising the exception • common codes: 400, 401, 403, 404, 500. | ||
raise HTTPException(status_code=401, headers={"WWW-Authenticate": "Bearer"}) | • Adds custom headers to an error response • useful for auth challenges, carries real protocol meaning, not just decoration. | ||
from starlette.exceptions import HTTPException as StarletteHTTPException | • Replaces FastAPI's default error formatting • registers on Starlette's HTTPException so it also catches errors Starlette raises internally, not just FastAPI's own subclass. |
Topic edits
- Description: kept-as-is (already specific and learning-focused, correctly frames the progression from HTTPException to custom handlers to overriding validation formatting)
- Rows added: none
- Rows removed: none
- Rows reordered: no
- Column 1 link updates: none
- Row description rewrites: HTTPException (clarified it is caught by a default handler automatically, no setup needed, to head off confusion with unhandled exceptions), Custom exception (clarified it does nothing on its own until paired with a handler), Exception handler (clarified it also catches subclasses of the registered type), Validation error handler (clarified overriding the HTTPException handler alone does not affect it), Custom headers in exception (clarified the header carries real protocol meaning, not just decoration), Override default handlers (clarified it must register on Starlette's HTTPException specifically to also catch Starlette's own internally raised errors)
- Row example rewrites: none (all examples already match the official docs' canonical syntax)
- Duplicates merged: none
- Deprecated demotions: none
Table 9: Authentication and Security
FastAPI ships with security utilities that plug straight into its dependency system and its OpenAPI docs. The rows below walk through the common building blocks: OAuth2 password flows, API keys sent via headers, query parameters, or cookies, JWT creation and verification with PyJWT, and password hashing with pwdlib, and how they combine into a get_current_user dependency you reuse across protected routes. Security scopes at the end add fine-grained, permission-based access on top.
| Mechanism | Example | Description | |
|---|---|---|---|
from fastapi.security import OAuth2PasswordBeareroauth2 = OAuth2PasswordBearer(tokenUrl="token") | • Standard OAuth2 flow for password-based authentication • extracts the bearer token string from the header only, your own dependency must still verify it. | ||
from fastapi.security import OAuth2PasswordRequestFormdef login(form: OAuth2PasswordRequestForm = Depends()): | • Form data model for username/password login • must be sent as form fields, not JSON, the field names are fixed by the OAuth2 spec. | ||
from fastapi.security import APIKeyHeaderapi_key = APIKeyHeader(name="X-API-Key") | • Extracts API key from a custom header • only reads the value, your code must still check that it is a valid key. | ||
from fastapi.security import APIKeyQueryapi_key = APIKeyQuery(name="api_key") | • Reads API key from a query parameter • less secure than a header, query strings often end up in server logs and browser history. | ||
from fastapi.security import APIKeyCookieapi_key = APIKeyCookie(name="session") | • Extracts API key from a cookie • cookies are attached to requests automatically by the browser, which is what makes cookie-based auth CSRF-prone without extra defenses. | ||
import jwttoken = jwt.encode(data, SECRET, algorithm="HS256") | • Encodes a JWT token with a payload and a secret key • FastAPI's current docs use the actively maintained PyJWT library ( import jwt), not the older python-jose. | ||
import jwtfrom jwt.exceptions import InvalidTokenErrorpayload = jwt.decode(token, SECRET, algorithms=["HS256"]) | • Decodes and verifies a JWT's signature • raises InvalidTokenError if invalid, the payload itself is only base64-encoded, never encrypted. | ||
from pwdlib import PasswordHashpassword_hash = PasswordHash.recommended() | • Hashes passwords for storage, one-way, never store plain passwords • FastAPI's current docs recommend pwdlib with Argon2, bcrypt remains supported for compatibility with older hashes. | ||
async def get_current_user(token = Depends(oauth2)): # decode token, look up user, check active | • Dependency that decodes the token and looks up the user • also checks the account is still active, a valid signature alone is not enough. | ||
| • Permission-based access control using OAuth2 scopes • your own dependency must compare the token's scopes against the required ones and raise the error itself. |
Table 10: Middleware
Middleware wraps every request and response, making it the right place for cross-cutting concerns that shouldn't live in individual endpoints. CORS is the one nearly everyone needs first, while the others handle compression, HTTPS enforcement, trusted-host checks, and sessions — plus the option to write your own function- or class-based middleware for custom logic like timing or request logging.
| Type | Example | Description | |
|---|---|---|---|
from fastapi.middleware.cors import CORSMiddlewareapp.add_middleware(CORSMiddleware, allow_origins=["https://example.com"], allow_credentials=True) | • Enables Cross-Origin Resource Sharing • configures which domains can access API. | ||
async def add_header(request, call_next): | • Function-based middleware • runs before and after every request. | ||
class TimingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): | • Class-based middleware • supports configuration via __init__ for more complex logic. | ||
from starlette.middleware.trustedhost import TrustedHostMiddlewareapp.add_middleware(TrustedHostMiddleware, allowed_hosts=["*.example.com"]) | • Validates Host header • prevents HTTP Host header attacks. | ||
from starlette.middleware.httpsredirect import HTTPSRedirectMiddlewareapp.add_middleware(HTTPSRedirectMiddleware) | • Redirects HTTP to HTTPS • enforces secure connections. | ||
from fastapi.middleware.gzip import GZipMiddlewareapp.add_middleware(GZipMiddleware, minimum_size=1000) | • Compresses responses over specified size • reduces bandwidth. | ||
from starlette.middleware.sessions import SessionMiddlewareapp.add_middleware(SessionMiddleware, secret_key="key") | Adds session support via signed cookies. |
Table 11: Background Tasks
Some work, like sending a confirmation email or writing a log line, shouldn't make the user wait for the response. FastAPI's built-in BackgroundTasks handles these lightweight cases right inside the same process, with no extra infrastructure required. For heavier or long-running jobs that need their own workers, this table also covers Celery and ARQ as the next step up.
| Approach | Example | Description | |
|---|---|---|---|
from fastapi import BackgroundTasksdef task(email: str): send_email(email) | • Simple background function executed after response • no external queue needed. | ||
def endpoint(bg: BackgroundTasks): bg.add_task(send_email, to="user@example.com") | • Schedules function to run after response sent • tasks run in same process. | ||
bg.add_task(task1)bg.add_task(task2) | • Multiple tasks run sequentially in order added • an exception in one stops the rest from running | ||
def get_query(background_tasks: BackgroundTasks, q: str = None): background_tasks.add_task(write_log, message) | • A dependency can receive BackgroundTasks and call .add_task() itself• its data is resolved during the request, before the task runs, not by Depends() on the task function itself. | ||
from celery import Celerycelery = Celery("tasks", broker="redis://localhost")def heavy_task(): | • Distributed task queue for heavy/long-running jobs • runs in separate workers outside request cycle. | ||
from arq import create_poolredis = await create_pool() | • Redis-based async task queue built on asyncio • alternative to Celery for async-first applications. |
Table 12: Database Integration
Most FastAPI apps talk to a database through SQLAlchemy, and the choice between async and sync sessions sets the tone for everything else. These rows cover defining ORM models, running queries the SQLAlchemy 2.0 way, injecting a session as a dependency so it closes cleanly after each request, and managing schema changes with Alembic. Connection pooling at the end is the detail that keeps a busy API from exhausting the database.
| Technique | Example | Description | |
|---|---|---|---|
from sqlalchemy.ext.asyncio import AsyncSessionasync def get_db(): yield session | • Async database session for non-blocking DB operations • requires an asyncio-native driver like asyncpg or aiosqlite. | ||
from sqlalchemy.orm import Sessiondef get_db(): yield db | • Synchronous session • simpler but blocks during queries. | ||
from sqlalchemy.orm import Mapped, mapped_columnclass User(Base): id: Mapped[int] = mapped_column(primary_key=True) | • ORM model defining table structure via typed columns • the modern Mapped/mapped_column() style replaces the older Column(...) form. | ||
result = await session.execute(select(User))users = result.scalars().all() | • Executes async query • uses SQLAlchemy 2.0 style. | ||
def endpoint(db: Session = Depends(get_db)): | • Injects database session into route • session closed automatically after request via the dependency's yield/finally. | ||
alembic init alembicalembic revision --autogeneratealembic upgrade head | • Database schema migrations • tracks and applies schema changes over time; always review autogenerated migrations before applying, since they can miss renames. | ||
engine = create_async_engine(url, pool_size=20, max_overflow=10) | • Reuses database connections instead of opening a new one per query • critical for production performance. |
Table 13: WebSockets and Real-Time Communication
When you need a live, two-way channel, like chat, notifications, or a streaming dashboard, a plain request/response cycle won't do. FastAPI's WebSocket support gives you the full lifecycle: accept the connection, receive and send text or JSON in a loop, and handle the disconnect cleanly. The final SSE row is the simpler choice when updates only need to flow one way, from server to client.
| Feature | Example | Description | |
|---|---|---|---|
from fastapi import WebSocketasync def websocket_endpoint(websocket: WebSocket): | • Defines WebSocket route • full-duplex communication channel. | ||
await websocket.accept() | • Establishes WebSocket connection • must be called before send/receive. | ||
data = await websocket.receive_text()data = await websocket.receive_json() | • Reads message from client • supports text, JSON, or bytes. | ||
await websocket.send_text("Hello")await websocket.send_json({"msg": "Hi"}) | Sends message to client. | ||
try: while True: await websocket.receive_text()except WebSocketDisconnect: | • Handles client disconnect • exception raised when connection closes. | ||
from fastapi.responses import StreamingResponsedef event_stream(): yield f"data: {msg}\n\n" | • One-way streaming from server to client • simpler than WebSockets for uni-directional updates. |
Table 14: Testing
FastAPI makes testing genuinely pleasant because TestClient runs your app in-process — no server to spin up. These rows show the standard pytest workflow, the context-manager form that triggers lifespan events, the httpx-based async client for testing async code properly, and the dependency-override trick that swaps a real database for a mock without touching application code.
| Tool | Example | Description | |
|---|---|---|---|
from fastapi.testclient import TestClientclient = TestClient(app)response = client.get("/") | • Synchronous test client • tests FastAPI without running server. | ||
def test_read_main(): response = client.get("/") assert response.status_code == 200 | • Standard pytest patterns work with TestClient • uses fixtures for setup. | ||
with TestClient(app) as client: response = client.get("/") | • Triggers lifespan events during testing • startup/shutdown run automatically. | ||
from httpx import ASGITransport, AsyncClientasync with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: response = await client.get("/") | • Async testing with httpx • lets a test function await other async code, like async database calls. | ||
app.dependency_overrides[get_db] = lambda: mock_db | • Replaces real dependency with mock for testing • no code changes needed. | ||
files = {"file": ("test.txt", b"content", "text/plain")}client.post("/upload", files=files) | • Simulates file upload in tests • tuple format: (filename, content, content-type). | ||
def client(): return TestClient(app) | • Reusable test setup • pytest fixture creates client for all tests. |
Table 15: Advanced Routing
As an app grows past a single file, APIRouter is how you split routes into focused modules with shared prefixes, tags, and dependencies, then mount them onto the main app with include_router(). Sub-application mounting and static file serving are a different tool entirely, since they hand a path over to a genuinely separate ASGI app instead of just organizing routes. The route-priority row captures a subtle gotcha: declare /users/me before /users/{id}, or the parameterized route swallows it silently.
| Concept | Example | Description | |
|---|---|---|---|
from fastapi import APIRouterrouter = APIRouter(prefix="/items", tags=["items"]) | • Modular routing • groups related endpoints with shared prefix and tags. | ||
app.include_router(router) | • Mounts router to main app • enables splitting large apps into modules. | ||
app.mount("/v1", app_v1) | • Mounts independent FastAPI app at path • useful for API versioning. | ||
from fastapi.staticfiles import StaticFilesapp.mount("/static", StaticFiles(directory="static")) | Serves static files from directory. | ||
router = APIRouter(dependencies=[Depends(verify_token)]) | • Applies dependency to all routes in router • cleaner than per-route. | ||
router = APIRouter(tags=["items"]) | • Groups endpoints in OpenAPI docs • creates collapsible sections. | ||
Define more specific routes first: | • FastAPI matches first matching route • specific paths before parameterized ones. |
Table 16: OpenAPI Customization
FastAPI generates an OpenAPI schema and interactive docs for free, but real projects want to shape them. These rows let you set titles and descriptions, group endpoints under documented tags, add example responses, deprecate old routes, change or disable the docs URLs, and tweak Swagger UI behavior. Polishing the generated docs is what turns a working API into one other developers actually enjoy consuming.
| Element | Example | Description | |
|---|---|---|---|
app = FastAPI(title="My API", description="Docs", version="1.0") | Sets API title, description, version in OpenAPI schema and docs. | ||
tags_metadata = [{"name": "items", "description": "Item operations"}]app = FastAPI(openapi_tags=tags_metadata) | Adds descriptions to tag groups in documentation. | ||
def custom_openapi(): # modify schemaapp.openapi = custom_openapi | • Overrides OpenAPI generation • add custom extensions or modify schema • caches the result in app.openapi_schema so it is only built once. | ||
app = FastAPI(docs_url=None, redoc_url=None) | • Hides the Swagger UI and ReDoc pages • the /openapi.json schema stays public by default, set openapi_url=None too if it needs to be gone. | ||
app = FastAPI(docs_url="/api/docs") | Changes Swagger UI path from default /docs. | ||
| • Sets unique operation identifier in OpenAPI • useful for code generation tools. | ||
| • Marks endpoint as deprecated in docs • still functional. | ||
responses={200: {"description": "OK", "content": {"application/json": {"example": {...}}}}} | Adds example responses to OpenAPI docs. | ||
app = FastAPI(separate_input_output_schemas=False) | • FastAPI 0.102.0+ generates separate OpenAPI schemas for input and output with Pydantic v2 • set False to disable. | ||
app = FastAPI(swagger_ui_parameters={"defaultModelsExpandDepth": -1}) | Customizes Swagger UI behavior like theme, syntax highlighting, model expansion. |
Table 17: Settings and Configuration
Pydantic Settings brings the same type-safe validation you use for requests to your application config, reading values from environment variables and .env files and failing fast at startup if something's missing. Injecting settings as a dependency keeps them easy to mock in tests, and caching them with lru_cache means the config is parsed once. This is Pydantic Settings v2, so SettingsConfigDict replaces the old inner Config class.
| Technique | Example | Description | |
|---|---|---|---|
from pydantic_settings import BaseSettingsclass Settings(BaseSettings): api_key: str | • Type-safe configuration from environment variables • validates config at startup. | ||
from pydantic_settings import SettingsConfigDictmodel_config = SettingsConfigDict(env_file=".env") | • Loads environment variables from .env using Pydantic Settings v2 • replaces old inner class Config: env_file = ".env" pattern. | ||
def get_settings(): return Settings()settings: Settings = Depends(get_settings) | • Injects settings into routes • enables easy mocking in tests. | ||
def get_settings(): return Settings() | • Loads settings once and caches • improves performance. | ||
class Settings(BaseSettings): env: str = "dev" database_url: str | • Uses different config per environment • set via ENV variable. |
Table 18: Performance Optimization
FastAPI is already fast, but these levers squeeze out more under real load. Writing I/O-bound handlers as async unlocks concurrency, Redis caching and HTTP cache headers cut repeated work, connection pooling protects the database, and ORJSON speeds up serialization. The deployment rows — Uvicorn workers, Gunicorn, and the Docker pattern — cover putting all of that into production across multiple CPU cores.
| Strategy | Example | Description | |
|---|---|---|---|
async def fetch_data(): return await http_client.get(url) | • Use async def for I/O-bound operations• enables concurrent request handling, but a CPU-heavy async def with no real await still blocks the whole event loop. | ||
FastAPIRedis(app).lifespan().caching() | • Caches responses in Redis to reduce database load for repeated queries • needs an explicit eviction step, or users can see stale data until the TTL expires. | ||
def get_items(response: Response): response.headers["Cache-Control"] = "max-age=3600" | • Sets HTTP caching headers so clients, proxies, and CDNs know how long to reuse a response • purely advisory, FastAPI still runs the route on every request unless an actual cache sits in front of it. | ||
engine = create_engine(url, pool_size=20) | • Reuses database connections instead of opening a new one per request • an undersized pool queues up requests and raises timeout errors once concurrent load exceeds its capacity. | ||
app = FastAPI(default_response_class=ORJSONResponse) | • Uses the Rust based orjson library to serialize responses faster than the standard json module• a response_model can already reach similar speed, and orjson has its own edge cases like turning NaN into null. | ||
uvicorn main:app --workers 4 | • Runs multiple worker processes to use multiple CPU cores • each worker is a separate process with its own memory, so in-memory caches or rate limiters aren't shared across workers. | ||
gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker | • Process manager that starts and supervises Uvicorn worker processes, restarting any that crash • a common production deployment combination. | ||
FROM python:3.14RUN pip install fastapi[standard]CMD ["fastapi", "run", "main.py", "--port", "80"] | • Official Docker image pattern for FastAPI production deployment • on Kubernetes or similar clusters, run a single process per container and let the cluster handle replication instead of adding --workers too. |
Topic edits
- Description: kept-as-is (already specific and learning-focused, names the concrete levers covered by the table)
- Rows added: none
- Rows removed: none
- Rows reordered: no
- Column 1 link updates: Redis caching: https://medium.com/@kirant3ja/redis-hybrid-caching-in-fastapi-af0bf0ec40cb → https://redis.io/docs/latest/integrate/fastapi/ (official Redis docs for the FastAPI integration, replacing a third-party Medium post); Response caching headers: https://mojoauth.com/implement-caching/implement-caching-with-fastapi → https://fastapi.tiangolo.com/advanced/response-headers/ (official FastAPI docs, replacing a low-authority blog whose own example used an invalid FastAPI syntax); Uvicorn workers: https://www.uvicorn.org/deployment/ → https://fastapi.tiangolo.com/deployment/server-workers/ (the old uvicorn.org domain no longer resolves; linking FastAPI's own official Uvicorn-workers doc instead); Gunicorn + Uvicorn: https://www.uvicorn.org/deployment/ → https://uvicorn.dev/deployment/ (uvicorn.org is dead, the project's docs now live at uvicorn.dev)
- Row description rewrites: Async operations (added the CPU-bound blocking caveat), Redis caching (added the eviction/staleness caveat), Response caching headers (clarified the header is advisory, not enforced server-side, and fixed the accompanying example), Connection pooling (added the undersized-pool production-timeout consequence), ORJSONResponse (added the response_model-parity and NaN-handling nuance), Uvicorn workers (added the per-process memory isolation caveat), Docker deployment (added the one-process-per-container guidance for clusters)
- Row example rewrites: Response caching headers (the prior example,
@app.get("/", response_headers={...}), is not valid FastAPI syntax; there is noresponse_headersparameter on a path operation decorator, replaced with the documentedResponseparameter pattern); Redis caching (replaced the third-partyfastapi_cachesnippet with the officialfastapi-redis-sdkpattern matching the new Column 1 link) - Duplicates merged: none
- Deprecated demotions: none
Table 19: Advanced Patterns
These are the extras you reach for once the basics are in place and the app is heading toward production. Rate limiting protects you from abuse, API versioning lets the interface evolve without breaking clients, and a GraphQL endpoint or class-based views fit specialized needs. Request-ID tracking and alternative serialization like MessagePack round out a toolkit for APIs that need to scale and be observable.
| Pattern | Example | Description | |
|---|---|---|---|
from strawberry.fastapi import GraphQLRouterapp.include_router(GraphQLRouter(schema), prefix="/graphql") | • Adds GraphQL endpoint using Strawberry • enables flexible query language. | ||
from slowapi import Limiterdef endpoint(): | • Throttles requests per IP/user • prevents API abuse. | ||
app.mount("/v1", v1_app)app.mount("/v2", v2_app) | • Versions API via URL path • most common versioning strategy. | ||
version = request.headers.get("API-Version", "v1") | • Reads version from custom header • cleaner URLs but less discoverable. | ||
from fastapi_utils.cbv import cbvclass ItemAPI: | • Groups related endpoints in class • reduces boilerplate for CRUD operations. | ||
from msgpack_asgi import MessagePackMiddlewareapp.add_middleware(MessagePackMiddleware) | • Uses binary format for smaller payloads • faster than JSON for large data. | ||
async def add_request_id(request, call_next): | • Adds unique ID to each request • enables distributed tracing. |