This guide is the source-of-truth operational reference for FastAPI — the modern, high-performance Python web framework for building APIs with standard Python type hints. It is written for platform engineers, DevOps practitioners, security reviewers, architects, and developers who deploy, scale, extend, or contribute to FastAPI applications. The companion User Guide covers everyday usage — route definitions, request handling, and interactive docs. This Admin Guide covers what happens underneath: the ASGI middleware stack, OpenAPI schema generation internals, Pydantic integration mechanics, dependency injection resolution, security subsystem architecture, deployment topologies, performance tuning, and the complete configuration surface.
This document covers FastAPI 0.135.3 at commit 9653034 on the master branch of tiangolo/fastapi. Where the implementation deviates from external documentation or blog posts, the source wins; this guide reflects the source.
Table of Contents
- Architecture Overview
- System Requirements
- Configuration Reference
- Deployment and Scaling
- Operations
- Performance
- Security
- Developer Internals
- Integration Points
- Module Reference
- Testing Strategy
- Code Patterns and Conventions
- Troubleshooting
- Quick Reference
1. Architecture Overview
FastAPI is a layered framework built on two foundational libraries: Starlette (the ASGI toolkit providing HTTP/WebSocket protocol handling, routing, middleware, and server integration) and Pydantic v2 (the data validation and serialization layer that powers type-driven request parsing and OpenAPI schema generation). FastAPI's own code is relatively thin — 23 files, ~522K characters in the fastapi/ package — because it delegates transport to Starlette and validation to Pydantic, focusing its value on the declarative API surface that ties them together.
1.1 High-level component diagram
+---------------------------------------------------------------+
| APPLICATION CODE |
| @app.get("/items/{id}") |
| async def read_item(id: int, q: str = Query(...)): ... |
+----------------------------+----------------------------------+
|
v
+---------------------------------------------------------------+
| FastAPI (applications.py) |
| extends Starlette |
| - APIRouter tree (routing.py) |
| - Dependency injection (dependencies/) |
| - Parameter resolution (params.py, param_functions.py) |
| - OpenAPI schema generation (openapi/) |
| - Security utilities (security/) |
+----------------------------+----------------------------------+
|
v
+---------------------------------------------------------------+
| Starlette (ASGI framework) |
| - Routing, Request/Response, WebSocket |
| - Middleware stack |
| - Static files, background tasks |
| - TestClient (httpx-based) |
+----------------------------+----------------------------------+
|
v
+---------------------------------------------------------------+
| Pydantic v2 |
| - Model validation, JSON Schema generation |
| - Field metadata (used by FastAPI params) |
| - BaseSettings for configuration |
+----------------------------+----------------------------------+
|
v
+---------------------------------------------------------------+
| ASGI Server |
| Uvicorn / Hypercorn / Daphne / Gunicorn w/ UvicornWorker |
+---------------------------------------------------------------+
1.2 The middleware stack
When FastAPI.build_middleware_stack() executes (inherited from Starlette, called lazily on first request), it constructs the following chain:
Incoming ASGI request
|
v
ServerErrorMiddleware # Catches unhandled exceptions, returns 500
|
v
User-registered middleware # app.add_middleware(...) in LIFO order
|
v
ExceptionMiddleware # Routes exceptions to registered handlers
|
v
AsyncExitStackMiddleware # FastAPI's own — manages dependency exit stacks
|
v
Router # Starlette route matching + endpoint dispatch
Important
The
AsyncExitStackMiddlewareis FastAPI-specific (defined infastapi/middleware/asyncexitstack.py). It creates anAsyncExitStackonrequest.statethat dependency injection uses to manage generator-based dependencies (those usingyield). Without this middleware, dependencies that yield would never have their cleanup code executed.
1.3 Component inventory
| Component | Source path | Role |
|---|---|---|
| FastAPI app | fastapi/applications.py |
Main application class; extends Starlette; holds routers, OpenAPI config, exception handlers, lifespan. |
| APIRouter | fastapi/routing.py |
Route registration, path operation decoration, sub-router inclusion. |
| APIRoute / APIWebSocketRoute | fastapi/routing.py |
Individual route wrappers; compile endpoint functions into ASGI apps with dependency resolution. |
| Parameter classes | fastapi/params.py |
Path, Query, Header, Cookie, Body, Form, File, Depends, Security — extend Pydantic FieldInfo. |
| Parameter functions | fastapi/param_functions.py |
Function-form wrappers around parameter classes for cleaner syntax. |
| OpenAPI generation | fastapi/openapi/utils.py |
get_openapi() — builds the complete OpenAPI 3.1.0 JSON schema from registered routes. |
| OpenAPI models | fastapi/openapi/models.py |
Full Pydantic model tree for OpenAPI 3.1.0 spec objects. |
| Interactive docs | fastapi/openapi/docs.py |
HTML generators for Swagger UI and ReDoc. |
| Security schemes | fastapi/security/ |
OAuth2, HTTP Basic/Bearer/Digest, API key, OpenID Connect dependency providers. |
| Encoders | fastapi/encoders.py |
jsonable_encoder() — recursive Pydantic model to JSON-safe dict conversion. |
| SSE support | fastapi/sse.py |
EventSourceResponse, ServerSentEvent for server-sent event streaming. |
| Exception types | fastapi/exceptions.py |
HTTPException, WebSocketException, RequestValidationError, ResponseValidationError. |
| Exception handlers | fastapi/exception_handlers.py |
Default handlers wired at app startup. |
| Middleware re-exports | fastapi/middleware/ |
CORS, GZip, HTTPS redirect, trusted host, WSGI — all re-exported from Starlette. |
| Compatibility layer | fastapi/_compat/ |
Pydantic v2 adaptation: ModelField, GenerateJsonSchema, field serialization. |
| Responses | fastapi/responses.py |
UJSONResponse, ORJSONResponse for alternative JSON serializers. |
| Data structures | fastapi/datastructures.py |
UploadFile (extends Starlette), DefaultPlaceholder, Default. |
| Background tasks | fastapi/background.py |
BackgroundTasks re-export from Starlette. |
| CLI delegation | fastapi/cli.py |
Defers to fastapi-cli package; raises RuntimeError if not installed. |
| Re-exports | fastapi/requests.py, fastapi/websockets.py |
Request, WebSocket from Starlette. |
| Utilities | fastapi/utils.py, fastapi/types.py, fastapi/logger.py |
Internal helpers, type aliases, configured logger. |
1.4 Technology stack
| Layer | Library | Role |
|---|---|---|
| ASGI framework | Starlette | HTTP/WebSocket protocol, routing, middleware, test client |
| Data validation | Pydantic v2 | Request/response validation, JSON Schema, settings |
| ASGI server | Uvicorn (recommended) | Production-grade ASGI server with uvloop |
| Form parsing | python-multipart | Required for Form() and File() parameters |
| Async test client | httpx | Powers TestClient and async testing |
| Optional JSON | orjson, ujson | Alternative JSON serializers for ORJSONResponse, UJSONResponse |
| Optional JWT | python-jose, PyJWT | JWT encoding/decoding in security examples |
| Optional hashing | passlib, bcrypt | Password hashing in security examples |
| Optional ORM | SQLModel | SQL database integration (SQLAlchemy + Pydantic) |
| Optional SSE | sse-starlette (or built-in) | Server-sent events (FastAPI now has built-in sse.py) |
Note
FastAPI's core package has minimal hard dependencies:
starlette,pydantic, andtyping-extensions. Everything else —python-multipart,orjson,ujson,python-jose,passlib,sqlmodel,httpx— is optional and only needed when you use the corresponding features. Thepython-multipartdependency is required at runtime only if you declareForm(),File(), orUploadFileparameters.
2. System Requirements
2.1 Runtime requirements
| Requirement | Minimum | Recommended |
|---|---|---|
| Python | 3.8+ | 3.11+ (for performance and typing improvements) |
| Starlette | Compatible version pinned by FastAPI | Latest compatible |
| Pydantic | v2.0+ | v2.7+ (latest v2 line) |
| ASGI server | Any ASGI 3.0 server | Uvicorn 0.25+ with uvloop and httptools |
| OS | Linux, macOS, Windows | Linux (for production deployments) |
2.2 Installation matrix
# Minimal install
pip install fastapi
# With ASGI server (required for running)
pip install "fastapi[standard]"
# Includes: uvicorn, httpx, python-multipart, email-validator, jinja2
# Individual extras
pip install uvicorn[standard] # uvloop + httptools + watchfiles
pip install python-multipart # Form/File support
pip install orjson # ORJSONResponse
pip install ujson # UJSONResponse
pip install python-jose[cryptography] # JWT tokens
pip install passlib[bcrypt] # Password hashing
pip install sqlmodel # Database ORM
pip install httpx # Async HTTP + TestClient
pip install pydantic-settings # BaseSettings for config
2.3 Hardware sizing guidelines
| Deployment size | vCPUs | RAM | Workers | Expected RPS |
|---|---|---|---|---|
| Development | 1 | 512 MB | 1 (auto-reload) | N/A |
| Small (< 100 concurrent) | 2 | 1 GB | 2-4 | 1,000-5,000 |
| Medium (100-1,000 concurrent) | 4 | 4 GB | 4-8 | 5,000-20,000 |
| Large (1,000+ concurrent) | 8+ | 8+ GB | 8-16 | 20,000-100,000+ |
Tip
Worker count formula:
(2 * CPU_CORES) + 1for CPU-bound workloads. For I/O-bound async endpoints (the common case with FastAPI), a single worker with async handlers can saturate a CPU core. Scale workers to match CPU cores, not connections.
3. Configuration Reference
FastAPI applications are configured through the FastAPI() constructor, ASGI server settings, and application-level patterns (environment variables via pydantic-settings). There is no configuration file format built into FastAPI itself.
3.1 Complete FastAPI() constructor parameters
Every parameter accepted by FastAPI.__init__() as found in applications.py:
| Parameter | Type | Default | Description |
|---|---|---|---|
debug |
bool |
False |
Enables Starlette debug mode; shows tracebacks in 500 responses. Never enable in production. |
title |
str |
"FastAPI" |
API title shown in OpenAPI schema and interactive docs. |
summary |
str | None |
None |
Short API summary for OpenAPI info.summary. |
description |
str |
"" |
API description (Markdown supported) for OpenAPI info.description. |
version |
str |
"0.1.0" |
API version string for OpenAPI info.version. |
openapi_url |
str | None |
"/openapi.json" |
Path where the OpenAPI JSON schema is served. Set to None to disable OpenAPI entirely. |
openapi_tags |
list[dict] | None |
None |
Tag metadata for OpenAPI schema grouping. |
servers |
list[dict] | None |
None |
Server objects for the OpenAPI servers array. |
dependencies |
Sequence[Depends] | None |
None |
Global dependencies applied to every route. |
default_response_class |
type[Response] |
JSONResponse |
Default response class for all routes. Change to ORJSONResponse or UJSONResponse for faster JSON serialization. |
redirect_slashes |
bool |
True |
Whether to redirect /path to /path/ (or vice versa) when a route match is found for the alternate form. |
docs_url |
str | None |
"/docs" |
Path for Swagger UI. Set to None to disable. |
redoc_url |
str | None |
"/redoc" |
Path for ReDoc. Set to None to disable. |
swagger_ui_oauth2_redirect_url |
str | None |
"/docs/oauth2-redirect" |
OAuth2 redirect URL for Swagger UI authorization flow. |
swagger_ui_init_oauth |
dict | None |
None |
Initial OAuth2 configuration for Swagger UI (passed to ui.initOAuth()). |
middleware |
Sequence[Middleware] | None |
None |
Middleware to register at construction time (alternative to app.add_middleware()). |
exception_handlers |
dict | None |
None |
Exception-to-handler mapping registered at construction time. |
on_startup |
Sequence[Callable] | None |
None |
Deprecated. Startup event handlers. Use lifespan instead. |
on_shutdown |
Sequence[Callable] | None |
None |
Deprecated. Shutdown event handlers. Use lifespan instead. |
lifespan |
Lifespan | None |
None |
Async context manager for application lifespan (startup/shutdown). Replaces on_startup/on_shutdown. |
terms_of_service |
str | None |
None |
URL to terms of service for OpenAPI info.termsOfService. |
contact |
dict | None |
None |
Contact info for OpenAPI info.contact. |
license_info |
dict | None |
None |
License info for OpenAPI info.license. |
root_path |
str |
"" |
ASGI root path (for reverse proxy path prefixing). Equivalent to Uvicorn's --root-path. |
root_path_in_servers |
bool |
True |
Whether to include root_path as the first entry in OpenAPI servers. |
responses |
dict | None |
None |
Additional global responses for all routes (merged into OpenAPI schema). |
callbacks |
list[BaseRoute] | None |
None |
Global OpenAPI callback routes. |
webhooks |
APIRouter | None |
None |
Webhook definitions for OpenAPI 3.1.0. |
deprecated |
bool | None |
None |
Mark all routes as deprecated in OpenAPI schema. |
include_in_schema |
bool |
True |
Whether to include routes in the OpenAPI schema. |
swagger_ui_parameters |
dict | None |
None |
Custom parameters passed to Swagger UI initialization (e.g., {"persistAuthorization": True}). |
generate_unique_id_function |
Callable |
Default(generate_unique_id) |
Function to generate unique operation IDs for OpenAPI. Default: {method}_{path}__{name}. |
separate_input_output_schemas |
bool |
True |
When True, generates separate schemas for request/response models (input uses mode='validation', output uses mode='serialization'). |
openapi_external_docs |
dict | None |
None |
External docs link for OpenAPI externalDocs. |
strict_content_type |
bool |
True |
When True, strictly validates request Content-Type headers against expected media types. |
Warning
Setting
openapi_urltoNonedisables the OpenAPI schema endpoint entirely, which also disables/docsand/redoc. This is recommended for production APIs that should not expose their schema publicly. The interactive docs are a development tool, not a production feature.
3.2 Route decorator parameters
Parameters accepted by @app.get(), @app.post(), and other HTTP method decorators (defined in APIRouter.api_route()):
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
str |
(required) | URL path (supports {param} path parameters). |
response_model |
type | None |
None |
Pydantic model for response validation/serialization. |
status_code |
int | None |
None |
Default HTTP status code (e.g., 201 for POST). |
tags |
list[str | Enum] |
None |
OpenAPI tags for grouping. |
dependencies |
Sequence[Depends] |
None |
Route-level dependencies (in addition to router/app-level). |
summary |
str |
None |
OpenAPI operation summary. |
description |
str |
None |
OpenAPI operation description. |
response_description |
str |
"Successful Response" |
OpenAPI response description. |
responses |
dict |
None |
Additional response schemas for OpenAPI. |
deprecated |
bool |
None |
Mark route as deprecated in OpenAPI. |
operation_id |
str |
None |
Custom OpenAPI operation ID. |
response_model_include |
set |
None |
Fields to include in response model. |
response_model_exclude |
set |
None |
Fields to exclude from response model. |
response_model_by_alias |
bool |
True |
Use model field aliases in response. |
response_model_exclude_unset |
bool |
False |
Exclude unset fields from response. |
response_model_exclude_defaults |
bool |
False |
Exclude default-valued fields. |
response_model_exclude_none |
bool |
False |
Exclude None-valued fields. |
include_in_schema |
bool |
True |
Whether to include in OpenAPI schema. |
response_class |
type[Response] |
Default(JSONResponse) |
Response class for this route. |
name |
str |
None |
Route name (used in URL generation). |
callbacks |
list[BaseRoute] |
None |
OpenAPI callback routes. |
openapi_extra |
dict |
None |
Extra OpenAPI schema data merged into the operation. |
generate_unique_id_function |
Callable |
Default(generate_unique_id) |
Per-route unique ID generator override. |
3.3 Application settings pattern (pydantic-settings)
FastAPI does not have a built-in configuration file format. The canonical pattern for application configuration uses pydantic-settings:
from functools import lru_cache
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "My API"
admin_email: str = "[email removed]"
database_url: str = "sqlite:///./app.db"
secret_key: str = "change-me"
debug: bool = False
items_per_page: int = 50
allowed_hosts: list[str] = ["*"]
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
@lru_cache
def get_settings() -> Settings:
return Settings()
Tip
The
@lru_cachedecorator onget_settings()ensures the settings object is created once and reused. This is critical for performance — without it,Settings()would read environment variables and parse the.envfile on every request that depends on settings. For testing, useapp.dependency_overrides[get_settings] = lambda: TestSettings().
3.4 ASGI server configuration (Uvicorn)
Key Uvicorn settings that affect FastAPI behavior:
| Setting | CLI flag | Default | Description |
|---|---|---|---|
| Host | --host |
127.0.0.1 |
Bind address. Use 0.0.0.0 for container/production deployments. |
| Port | --port |
8000 |
Bind port. |
| Workers | --workers |
1 |
Number of worker processes (use for multi-core). |
| Root path | --root-path |
"" |
ASGI root path for reverse proxy setups. |
| Reload | --reload |
False |
Auto-reload on code changes. Development only. |
| Loop | --loop |
auto |
Event loop: auto, asyncio, uvloop. uvloop recommended for production. |
| HTTP parser | --http |
auto |
HTTP parser: auto, h11, httptools. httptools is faster. |
| Log level | --log-level |
info |
Logging level: critical, error, warning, info, debug, trace. |
| Access log | --access-log / --no-access-log |
enabled | Disable for maximum throughput. |
| Limit concurrency | --limit-concurrency |
None |
Max concurrent connections before returning 503. |
| Limit max requests | --limit-max-requests |
None |
Worker recycling: restart after N requests (guards against memory leaks). |
| Timeout keep-alive | --timeout-keep-alive |
5 |
Keep-alive timeout in seconds. |
| SSL cert | --ssl-certfile |
None |
TLS certificate file (usually handled by reverse proxy). |
| SSL key | --ssl-keyfile |
None |
TLS key file. |
| Proxy headers | --proxy-headers |
True |
Trust X-Forwarded-* headers from reverse proxy. |
| Forwarded allow IPs | --forwarded-allow-ips |
127.0.0.1 |
IPs trusted for proxy headers. |
4. Deployment and Scaling
4.1 Production deployment topologies
Single-server deployment
Internet
|
+--------v--------+
| Reverse proxy |
| nginx / Caddy |
| (TLS, static) |
+--------+--------+
|
+--------v--------+
| Gunicorn |
| w/ UvicornWorker|
| (N workers) |
+--------+--------+
|
+--------v--------+
| FastAPI app |
| (per worker) |
+-----------------+
# Production command with Gunicorn
gunicorn app.main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--timeout 120 \
--graceful-timeout 30 \
--max-requests 10000 \
--max-requests-jitter 1000 \
--access-logfile - \
--error-logfile -
Container deployment
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app ./app
# Non-root user
RUN adduser --disabled-password --no-create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Caution
Do not use
--reloadin production containers. The file-watcher consumes CPU, and the auto-reload behavior creates unpredictable restart windows. Always use multi-worker mode (--workers) or Gunicorn withUvicornWorkerfor production.
Kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-app
spec:
replicas: 3
selector:
matchLabels:
app: fastapi-app
template:
metadata:
labels:
app: fastapi-app
spec:
containers:
- name: api
image: myregistry/fastapi-app:latest
ports:
- containerPort: 8000
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
4.2 Reverse proxy configuration
Nginx
upstream fastapi_backend {
server 127.0.0.1:8000;
# Add more servers for load balancing
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.crt;
ssl_certificate_key /etc/ssl/private/api.key;
# WebSocket support
location /ws {
proxy_pass http://fastapi_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# SSE support (disable buffering)
location /stream {
proxy_pass http://fastapi_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Standard API
location / {
proxy_pass http://fastapi_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Important
When deploying behind a reverse proxy, you must either pass
--root-path /api/v1to Uvicorn or setroot_path="/api/v1"in theFastAPI()constructor if the proxy strips or rewrites the path prefix. Without this, the OpenAPI schema will generate incorrect URLs for the interactive docs. Theroot_path_in_servers=Truedefault ensures the root path appears in the OpenAPIserversarray.
4.3 Scaling strategies
| Strategy | When to use | How |
|---|---|---|
| Vertical (more workers) | Single-server, CPU-bound endpoints | Increase --workers in Uvicorn/Gunicorn. Formula: 2 * cores + 1. |
| Horizontal (more instances) | Multi-server, stateless API | Deploy multiple containers behind a load balancer. Ensure session state is external (Redis, DB). |
| Async I/O | I/O-bound endpoints (DB, HTTP calls) | Use async def endpoints with async database drivers (asyncpg, aiosqlite). A single worker handles thousands of concurrent I/O-bound requests. |
| Background tasks | Long-running operations | Use BackgroundTasks for simple fire-and-forget, or Celery/ARQ for distributed task queues. |
| Caching | Read-heavy endpoints | Add Redis/Memcached caching at the application or reverse proxy layer. |
| CDN/Edge | Static responses, geographic distribution | Place a CDN (CloudFlare, CloudFront) in front for cacheable responses. |
5. Operations
5.1 Health checks
FastAPI does not ship a built-in health endpoint. The standard pattern:
from fastapi import FastAPI, status
app = FastAPI()
@app.get("/health", status_code=status.HTTP_200_OK)
async def health_check():
return {"status": "healthy"}
@app.get("/health/ready", status_code=status.HTTP_200_OK)
async def readiness_check():
"""Check downstream dependencies."""
# Check database connectivity
try:
await db.execute("SELECT 1")
except Exception:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"status": "unhealthy", "detail": "database unreachable"}
)
return {"status": "ready"}
5.2 Logging
FastAPI uses Python's standard logging module. The framework logger is configured in fastapi/logger.py:
import logging
logger = logging.getLogger("fastapi")
Production logging configuration:
import logging
import sys
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
# Suppress noisy libraries
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("uvicorn.error").setLevel(logging.INFO)
For structured logging (JSON), use python-json-logger or structlog:
import structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.stdlib.LoggerFactory(),
)
5.3 Lifespan management
The lifespan parameter replaces the deprecated on_startup/on_shutdown events:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: runs before accepting requests
db_pool = await create_db_pool()
app.state.db_pool = db_pool
ml_model = load_model()
app.state.ml_model = ml_model
yield # Application runs here
# Shutdown: runs after last request completes
await db_pool.close()
app = FastAPI(lifespan=lifespan)
Note
The
lifespancontext manager receives theFastAPIapp instance as its argument. Resources initialized during startup should be stored onapp.stateand accessed from dependencies or endpoints. Theyieldstatement marks the boundary between startup and shutdown. If startup raises an exception, the application does not start and shutdown code does not run.
5.4 Graceful shutdown
Uvicorn handles SIGTERM and SIGINT by default:
- Stop accepting new connections
- Wait for in-flight requests to complete (up to
--timeout-graceful-shutdown, default 0 = wait forever) - Run lifespan shutdown
- Exit
For Gunicorn with UvicornWorker:
# Graceful restart (reload workers one at a time)
kill -HUP $(cat /var/run/gunicorn.pid)
# Graceful shutdown
kill -TERM $(cat /var/run/gunicorn.pid)
5.5 Monitoring and observability
Prometheus metrics
from prometheus_client import Counter, Histogram, generate_latest
from starlette.middleware.base import BaseHTTPMiddleware
import time
REQUEST_COUNT = Counter(
"http_requests_total",
"Total HTTP requests",
["method", "endpoint", "status"]
)
REQUEST_LATENCY = Histogram(
"http_request_duration_seconds",
"Request latency",
["method", "endpoint"]
)
class PrometheusMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
REQUEST_COUNT.labels(
method=request.method,
endpoint=request.url.path,
status=response.status_code
).inc()
REQUEST_LATENCY.labels(
method=request.method,
endpoint=request.url.path
).observe(duration)
return response
app.add_middleware(PrometheusMiddleware)
@app.get("/metrics")
async def metrics():
return Response(
content=generate_latest(),
media_type="text/plain; version=0.0.4"
)
OpenTelemetry integration
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://collector:4317"))
)
FastAPIInstrumentor.instrument_app(app)
6. Performance
6.1 Async vs sync endpoints
FastAPI handles async def and def endpoints differently:
| Endpoint type | Execution model | Best for |
|---|---|---|
async def |
Runs directly on the event loop | I/O-bound work: database queries, HTTP calls, file I/O (with async drivers) |
def (sync) |
Runs in a thread pool (anyio.to_thread.run_sync) |
CPU-bound work, blocking I/O, legacy sync code |
Warning
Using blocking I/O (e.g.,
requests.get(),time.sleep(), sync database drivers) inside anasync defendpoint blocks the entire event loop, preventing all other requests from being processed. Either usedef(sync) endpoints so FastAPI runs them in a thread pool, or use async-native libraries (httpx,asyncpg,aiofiles).
6.2 JSON serialization performance
| Serializer | Response class | Relative speed | Notes |
|---|---|---|---|
json (stdlib) |
JSONResponse (default) |
1x (baseline) | Universal compatibility |
orjson |
ORJSONResponse |
~3-10x faster | Binary output, handles datetime/numpy natively; pip install orjson |
ujson |
UJSONResponse |
~2-4x faster | pip install ujson |
To use globally:
from fastapi.responses import ORJSONResponse
app = FastAPI(default_response_class=ORJSONResponse)
6.3 Response model optimization
# SLOW: validates and serializes the full model every time
@app.get("/items/", response_model=list[Item])
async def list_items():
return await db.fetch_all_items()
# FASTER: exclude fields you don't need
@app.get("/items/", response_model=list[Item], response_model_exclude={"internal_field"})
async def list_items():
return await db.fetch_all_items()
# FASTEST: return Response directly (no validation overhead)
@app.get("/items/")
async def list_items():
items = await db.fetch_all_items()
return ORJSONResponse(content=[item.model_dump() for item in items])
6.4 Dependency injection caching
Dependencies with the same parameters in the same request are resolved once and cached:
async def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Both parameters resolve to the SAME db session within one request
@app.get("/items/{id}")
async def get_item(id: int, db: Session = Depends(get_db)):
...
To force re-evaluation, use Depends(get_db, use_cache=False).
6.5 OpenAPI schema generation
The OpenAPI schema is generated lazily on first access to openapi_url and then cached on app.openapi_schema. For applications with hundreds of routes, initial schema generation can take noticeable time. The schema is regenerated only if app.openapi_schema is set to None.
To pre-generate and cache at startup:
@asynccontextmanager
async def lifespan(app: FastAPI):
app.openapi() # Force schema generation at startup
yield
6.6 Connection pooling
FastAPI itself does not manage database connections. Common patterns:
# SQLAlchemy async with connection pool
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
engine = create_async_engine(
"postgresql+asyncpg://user:pass@host/db",
pool_size=20, # Concurrent connections
max_overflow=10, # Extra connections under load
pool_pre_ping=True, # Validate connections before use
pool_recycle=3600, # Recycle connections after 1 hour
)
7. Security
7.1 Security scheme architecture
FastAPI's security module (fastapi/security/) provides dependency-injectable security schemes that integrate with OpenAPI. These are not authentication implementations — they are protocol extractors that parse credentials from requests and declare security requirements in the OpenAPI schema.
| Class | Module | What it extracts | OpenAPI scheme |
|---|---|---|---|
OAuth2PasswordBearer |
oauth2.py |
Bearer token from Authorization header |
oauth2 with password flow |
OAuth2AuthorizationCodeBearer |
oauth2.py |
Bearer token from Authorization header |
oauth2 with authorizationCode flow |
OAuth2PasswordRequestForm |
oauth2.py |
Form fields: username, password, scope, grant_type, client_id, client_secret |
N/A (form parser) |
SecurityScopes |
oauth2.py |
Aggregated scopes from the dependency chain | N/A (scope aggregator) |
HTTPBasic |
http.py |
Username/password from Authorization: Basic header |
http with basic scheme |
HTTPBearer |
http.py |
Token from Authorization: Bearer header |
http with bearer scheme |
HTTPDigest |
http.py |
Credentials from Authorization: Digest header |
http with digest scheme |
APIKeyQuery |
api_key.py |
API key from query parameter | apiKey in query |
APIKeyHeader |
api_key.py |
API key from custom header | apiKey in header |
APIKeyCookie |
api_key.py |
API key from cookie | apiKey in cookie |
OpenIdConnect |
open_id_connect_url.py |
Token from Authorization header |
openIdConnect |
All security classes inherit from SecurityBase (security/base.py) which stores the OpenAPI security scheme model.
7.2 Authentication patterns
JWT Bearer Token (the canonical pattern from docs_src)
from datetime import datetime, timedelta
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
SECRET_KEY = "your-secret-key" # Use a proper secret
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class TokenData(BaseModel):
username: str | None = None
scopes: list[str] = []
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(username=username, scopes=token_scopes)
except JWTError:
raise credentials_exception
user = get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user
7.3 CORS configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # NEVER use ["*"] in production
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
expose_headers=["X-Request-ID"],
max_age=600, # Preflight cache in seconds
)
Caution
Setting
allow_origins=["*"]withallow_credentials=Trueis a security misconfiguration. The CORS spec requires that when credentials are allowed, origins must be explicitly listed. Starlette'sCORSMiddlewarewill silently downgradeallow_credentialstoFalsewhenallow_origins=["*"]is used, which may break authenticated cross-origin requests without any error message.
7.4 Request validation security
FastAPI's type-driven validation provides defense-in-depth against injection attacks:
| Attack vector | FastAPI mitigation |
|---|---|
| SQL injection | Path/query parameters are type-validated (e.g., id: int rejects strings); use parameterized queries in the data layer. |
| XSS | JSON responses are Content-Type application/json by default; HTML must be explicitly returned. |
| Path traversal | Path parameters are validated against the path template; no filesystem path interpretation. |
| Request smuggling | Handled by the ASGI server (Uvicorn/Hypercorn), not FastAPI. |
| Oversized payloads | Configure --limit-max-request-size in Uvicorn (default: unlimited). |
| Slowloris | Configure --timeout-keep-alive in Uvicorn and use a reverse proxy with request timeouts. |
7.5 Validation error handling
When request validation fails, FastAPI raises RequestValidationError (which extends Pydantic's ValidationError). The default handler returns a 422 response with detailed error information:
{
"detail": [
{
"type": "missing",
"loc": ["body", "name"],
"msg": "Field required",
"input": {},
"url": "https://errors.pydantic.dev/2/v/missing"
}
]
}
Warning
The default 422 response can leak internal field names and model structure to clients. In production, consider overriding the validation error handler to return sanitized error messages:
from fastapi.exceptions import RequestValidationError @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): return JSONResponse( status_code=422, content={"detail": "Invalid request parameters"}, )
7.6 Trusted host middleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["api.example.com", "*.example.com"],
)
This rejects requests with Host headers that do not match the allowed list, preventing host header injection attacks.
7.7 HTTPS redirect
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)
Forces all HTTP requests to redirect to HTTPS. Only enable when TLS termination is properly configured.
7.8 Secret management
FastAPI does not include secret management. Best practices:
| Environment | Approach |
|---|---|
| Development | .env file with pydantic-settings |
| Containers | Environment variables via orchestrator secrets |
| Kubernetes | Secret resources mounted as env vars |
| AWS | AWS Secrets Manager or SSM Parameter Store |
| GCP | Google Secret Manager |
| Azure | Azure Key Vault |
8. Developer Internals
8.1 Request processing pipeline
When a request arrives at a FastAPI endpoint, the following sequence executes:
1. ASGI server receives HTTP request
2. ServerErrorMiddleware wraps the call
3. User middleware chain executes (request phase)
4. ExceptionMiddleware wraps the call
5. AsyncExitStackMiddleware creates request.state._exit_stack
6. Router matches the path to an APIRoute
7. APIRoute.get_route_handler() returns the endpoint wrapper
8. Dependency resolution begins:
a. Build dependency graph from function signature
b. Resolve dependencies depth-first (respecting cache)
c. For generator dependencies (yield), enter the context
9. Parameter extraction:
a. Path parameters extracted from URL and validated
b. Query parameters extracted and validated
c. Header parameters extracted and validated
d. Cookie parameters extracted and validated
e. Body parsed (JSON, form, multipart) and validated
10. Endpoint function called with resolved parameters
11. Response model validation (if response_model specified)
12. Response serialized (jsonable_encoder → response class)
13. Generator dependencies exit (cleanup after yield)
14. AsyncExitStack closes
15. User middleware chain executes (response phase)
16. Response sent to client
8.2 Dependency injection internals
The dependency injection system is implemented across fastapi/dependencies/ and integrated into fastapi/routing.py. Key mechanics:
Dependency graph resolution
Dependencies are resolved recursively. Each Depends() parameter creates a node in a directed acyclic graph. The resolver:
- Inspects the endpoint function signature using
inspect.signature() - For each parameter with a
Dependsdefault, recursively resolves the dependency's own parameters - Builds a flat execution plan (topological sort)
- Executes dependencies bottom-up, caching results per request
Generator dependencies (yield-based)
async def get_db():
db = SessionLocal()
try:
yield db # <-- dependency value injected here
finally:
db.close() # <-- runs after the response is sent
The AsyncExitStackMiddleware creates an AsyncExitStack per request. Generator dependencies are entered into this stack via contextmanager / asynccontextmanager wrappers. The exit stack unwinds after the response is sent, executing finally blocks.
Important
There is a known edge case in
routing.py(line ~125): if a dependency withyielduses a bareexcept:clause (catchingBaseException), it can interceptGeneratorExitandCancelledError, leading to the errorFastAPIError: "Response not awaited". Always useexcept Exception:in yield dependencies, never bareexcept:.
Dependency scoping
| Scope | Behavior | Example |
|---|---|---|
| Request-scoped (default) | One instance per request, cached within that request | Database sessions |
| Uncached | New instance on every injection point | Depends(dep, use_cache=False) |
| Application-scoped | Share state across requests | Use @lru_cache or store on app.state |
8.3 OpenAPI schema generation internals
The schema generation pipeline (fastapi/openapi/utils.py):
get_openapi()is called with the app's routes and metadata- For each
APIRoute, extract:- Path and HTTP methods
- Request body schema (from Pydantic models in
Bodyparams) - Query/path/header/cookie parameter schemas
- Response model schema
- Security requirements from
Security()dependencies
- Pydantic models are converted to JSON Schema via
model.model_json_schema() - When
separate_input_output_schemas=True(default), input schemas usemode='validation'and output schemas usemode='serialization'— this means models with computed fields, aliases, or default factories may have different input/output representations - Schemas are deduplicated and placed in
components.schemas - The complete OpenAPI 3.1.0 document is returned as a dict
The OpenAPI models in fastapi/openapi/models.py define the full OpenAPI 3.1.0 specification as Pydantic models:
| Model | OpenAPI Object |
|---|---|
OpenAPI |
Root document |
Info |
API metadata |
PathItem |
Path operations |
Operation |
Single operation |
Parameter |
Query/path/header/cookie params |
RequestBody |
Request body |
MediaType |
Content type definitions |
Schema |
JSON Schema (extended) |
SecurityScheme |
Auth scheme definitions |
Components |
Reusable components |
Tag |
Tag metadata |
Server |
Server definitions |
ExternalDocumentation |
External docs link |
Webhook |
Webhook definitions |
8.4 Pydantic v2 compatibility layer
The fastapi/_compat/ package bridges FastAPI's internal needs with Pydantic v2's API:
ModelField— FastAPI's internal representation of a route parameter. Wraps Pydantic'sFieldInfowith additional metadata (field location, alias, required status, validators).GenerateJsonSchema— Custom JSON Schema generator that handles FastAPI-specific schema modifications (e.g.,UploadFilerepresented as{"type": "string", "format": "binary"}).- Validation adapters — Functions that bridge between FastAPI's parameter extraction and Pydantic v2's
TypeAdapterfor validation.
Note
FastAPI dropped Pydantic v1 support. The
PydanticV1NotSupportedErrorinencoders.pyandutils.pyis raised if apydantic.v1model is detected as a response model or passed tojsonable_encoder(). The error message explicitly states that Pydantic v1 compatibility models are no longer supported.
8.5 The jsonable_encoder() function
fastapi/encoders.py provides jsonable_encoder(), which recursively converts Python objects into JSON-serializable types:
Input types handled:
Pydantic BaseModel → .model_dump()
dataclass → dataclasses.asdict()
Enum → .value
PurePath → str()
set/frozenset → list
bytes → decoded string
datetime/date/time/timedelta → .isoformat()
Decimal → float
UUID → str()
dict → recurse on keys and values
list/tuple/sequence → recurse on items
generators → list, then recurse
The function accepts include, exclude, by_alias, exclude_unset, exclude_defaults, and exclude_none parameters that mirror Pydantic's model_dump() options.
8.6 Exception handling chain
FastAPI registers default exception handlers at startup:
| Exception | Handler | Status code | Response format |
|---|---|---|---|
RequestValidationError |
request_validation_exception_handler |
422 | {"detail": [<validation errors>]} |
HTTPException |
http_exception_handler |
(from exception) | {"detail": <message>} |
WebSocketRequestValidationError |
websocket_request_validation_exception_handler |
N/A (WebSocket close) | Validation errors as close reason |
WebSocketException |
Built into Starlette | N/A (WebSocket close) | Close with code and reason |
Custom handlers override defaults:
@app.exception_handler(RequestValidationError)
async def custom_validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={"errors": exc.errors(), "body": exc.body},
)
8.7 Error messages reference
All error messages raised by FastAPI core code (from source analysis):
| # | Exception type | Message | Source | Trigger |
|---|---|---|---|---|
| 1 | FastAPIError |
"Prefix and path cannot be both empty" |
routing.py:1727 |
Including a router with empty prefix and a route with empty path |
| 2 | FastAPIError |
"Response not awaited" |
routing.py:125 |
Dependency with yield uses bare except: that swallows GeneratorExit |
| 3 | FastAPIError |
Invalid args type | utils.py:75 |
Internal: invalid argument types passed to utility functions |
| 4 | PydanticV1NotSupportedError |
pydantic.v1 models no longer supported | encoders.py:323 |
Pydantic v1 model passed to jsonable_encoder() |
| 5 | PydanticV1NotSupportedError |
pydantic.v1 response model not supported | utils.py:67 |
Pydantic v1 model used as response_model |
| 6 | RuntimeError |
(dynamic message) | cli.py:12 |
fastapi-cli package not installed when running python -m fastapi |
| 7 | ValueError |
"SSE 'id' must not contain null characters" |
sse.py:38 |
Server-sent event ID contains \0 |
| 8 | ValueError |
"Cannot set both 'data' and 'raw_data' on the same ServerSentEvent" |
sse.py:138 |
Both data and raw_data set on an SSE event |
| 9 | ValueError |
f'Expected UploadFile, received: {type(...)}' |
datastructures.py:135 |
Invalid type passed where UploadFile expected |
| 10 | ValueError |
errors (from encoder) |
encoders.py:336 |
Value cannot be encoded to JSON |
| 11 | WebSocketRequestValidationError |
solved_result.errors |
routing.py:759 |
WebSocket parameter validation failure |
8.8 Server-Sent Events (SSE) internals
FastAPI 0.135.3 includes built-in SSE support via fastapi/sse.py:
ServerSentEvent— Pydantic model representing a single SSE event with fields:data,raw_data,event,id,retry,comment,sep(separator, defaults to\r\n).EventSourceResponse— Streaming response that sendsServerSentEventobjects over atext/event-streamconnection. Accepts an async generator as its content source.
Validation rules:
idfield must not contain null characters (\0)dataandraw_dataare mutually exclusive on the same eventdatais serialized viajsonable_encoder()then JSON-dumped;raw_datais sent as-is
from fastapi.sse import EventSourceResponse, ServerSentEvent
@app.get("/stream")
async def stream_events():
async def event_generator():
for i in range(100):
yield ServerSentEvent(data={"count": i}, event="update")
await asyncio.sleep(1)
return EventSourceResponse(content=event_generator())
9. Integration Points
9.1 Database integration
SQLModel (recommended pattern from docs_src)
from sqlmodel import SQLModel, Field, Session, create_engine, select
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine("sqlite:///database.db")
@asynccontextmanager
async def lifespan(app: FastAPI):
SQLModel.metadata.create_all(engine)
yield
app = FastAPI(lifespan=lifespan)
def get_session():
with Session(engine) as session:
yield session
@app.post("/heroes/", response_model=Hero)
def create_hero(hero: Hero, session: Session = Depends(get_session)):
session.add(hero)
session.commit()
session.refresh(hero)
return hero
SQLAlchemy async
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
engine = create_async_engine("postgresql+asyncpg://user:pass@host/db")
async_session = async_sessionmaker(engine, expire_on_commit=False)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
yield session
9.2 Background tasks
from fastapi import BackgroundTasks
def send_notification(email: str, message: str):
"""Runs after the response is sent."""
# Send email logic here
pass
@app.post("/items/")
async def create_item(background_tasks: BackgroundTasks):
background_tasks.add_task(send_notification, "[email removed]", "Item created")
return {"status": "created"}
Tip
BackgroundTasksrun in the same process after the response is sent. For tasks that are CPU-intensive, long-running, or need retry logic, use a proper task queue like Celery, ARQ, or Dramatiq.BackgroundTasksare best for fire-and-forget operations like sending emails or writing audit logs.
9.3 WebSocket integration
from fastapi import WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"Client #{client_id}: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
9.4 Middleware integration
Custom middleware
from starlette.middleware.base import BaseHTTPMiddleware
class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid4()))
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
app.add_middleware(RequestIDMiddleware)
Pure ASGI middleware (higher performance)
class TimingMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
start = time.perf_counter()
async def send_wrapper(message):
if message["type"] == "http.response.start":
duration = time.perf_counter() - start
headers = MutableHeaders(scope=message)
headers.append("X-Process-Time", str(duration))
await send(message)
await self.app(scope, receive, send_wrapper)
app.add_middleware(TimingMiddleware)
Note
Pure ASGI middleware (the
__call__approach) is faster thanBaseHTTPMiddlewarebecause it avoids reading the entire request body into memory.BaseHTTPMiddlewarebuffers the request body to construct aRequestobject, which can cause issues with streaming uploads and WebSocket connections.
9.5 Custom route classes
FastAPI allows extending APIRoute to customize request handling at the route level:
from fastapi.routing import APIRoute
class GzipRoute(APIRoute):
def get_route_handler(self):
original_handler = super().get_route_handler()
async def custom_handler(request: Request):
if "gzip" in request.headers.get("Content-Encoding", ""):
body = await request.body()
request._body = gzip.decompress(body)
return await original_handler(request)
return custom_handler
app = FastAPI()
app.router.route_class = GzipRoute
Use cases from docs_src examples:
- Gzip request decompression — decompress gzip-encoded request bodies
- Request timing — log per-route request duration
- Validation error logging — capture and log validation failures before returning 422
9.6 Sub-application mounting
# Main app
app = FastAPI()
# Sub-application with its own OpenAPI schema
api_v2 = FastAPI(title="API v2", version="2.0.0")
@api_v2.get("/items/")
async def read_items_v2():
return [{"id": 1}]
app.mount("/api/v2", api_v2)
Sub-applications get their own OpenAPI schema at their own docs_url. They are completely independent FastAPI instances mounted at a path prefix.
9.7 Dependency overrides for testing
from fastapi.testclient import TestClient
def get_settings():
return Settings()
app = FastAPI()
# In tests:
def get_settings_override():
return Settings(database_url="sqlite:///test.db")
app.dependency_overrides[get_settings] = get_settings_override
client = TestClient(app)
10. Module Reference
10.1 fastapi/applications.py — FastAPI Application
The FastAPI class (extends Starlette) is the central entry point. Key methods:
| Method | Purpose |
|---|---|
__init__(**kwargs) |
Constructs app with all parameters from Section 3.1 |
openapi() |
Generates and caches the OpenAPI schema dict |
setup() |
Registers OpenAPI and docs routes (called by __init__) |
add_api_route() |
Programmatic route registration |
api_route() |
Decorator for route registration |
add_api_websocket_route() |
WebSocket route registration |
websocket() |
WebSocket decorator |
include_router() |
Mount an APIRouter |
get/post/put/delete/patch/options/head/trace() |
HTTP method decorators |
build_middleware_stack() |
Constructs the middleware chain (from Starlette, extended) |
The setup() method is called automatically during __init__. It adds three routes if their respective URLs are not None:
openapi_url→ servesself.openapi()as JSONdocs_url→ serves Swagger UI HTMLredoc_url→ serves ReDoc HTML
The April Fool's joke at line 4618: calling app() with certain internal conditions triggers FastAPIError: "Are you kidding me? Happy April Fool's". This is dead code in normal operation.
10.2 fastapi/routing.py — Router and Route Classes
APIRouter — Route collection that can be included in a FastAPI app or another router:
| Method | Purpose |
|---|---|
api_route(path, **kwargs) |
Route decorator |
include_router(router, prefix, tags, ...) |
Nest routers |
add_api_route(path, endpoint, **kwargs) |
Programmatic route add |
get/post/put/delete/patch/options/head/trace() |
HTTP method shortcuts |
websocket(path, ...) |
WebSocket route decorator |
APIRoute — Wraps a single endpoint:
| Attribute/Method | Purpose |
|---|---|
endpoint |
The original Python function |
dependant |
Compiled dependency tree |
response_model |
Pydantic model for response validation |
get_route_handler() |
Returns the ASGI handler; override for custom request processing |
APIWebSocketRoute — WebSocket equivalent of APIRoute.
Key internal behavior:
APIRoute.__init__()callsget_typed_signature()to inspect the endpoint's type hints- Dependencies are pre-compiled into a
Dependanttree at route registration time, not at request time - Route matching delegates to Starlette's router
10.3 fastapi/params.py — Parameter Classes
All parameter classes extend Pydantic's FieldInfo:
| Class | Location | Pydantic base |
|---|---|---|
Path |
URL path segments | FieldInfo |
Query |
URL query string | FieldInfo |
Header |
HTTP headers | FieldInfo |
Cookie |
HTTP cookies | FieldInfo |
Body |
Request body | FieldInfo |
Form |
Form-encoded body | FieldInfo |
File |
Multipart file upload | FieldInfo |
Depends |
Dependency injection | (standalone) |
Security |
Security dependency | extends Depends |
Each parameter class supports:
default/default_factory— default valuesalias— alternative name in the requesttitle,description— OpenAPI documentationgt,ge,lt,le— numeric constraintsmin_length,max_length,pattern— string constraintsexamples— OpenAPI examplesdeprecated— mark as deprecatedinclude_in_schema— control OpenAPI visibility
10.4 fastapi/openapi/ — OpenAPI Schema Generation
openapi/utils.py — Core functions:
| Function | Purpose |
|---|---|
get_openapi(title, version, routes, ...) |
Build the complete OpenAPI 3.1.0 dict |
get_openapi_path(route, ...) |
Convert a single route to OpenAPI path item |
get_flat_params(dependant) |
Flatten dependency tree into parameter list |
openapi/docs.py — HTML generators:
| Function | Output |
|---|---|
get_swagger_ui_html(openapi_url, title, ...) |
Complete Swagger UI HTML page |
get_redoc_html(openapi_url, title, ...) |
Complete ReDoc HTML page |
get_swagger_ui_oauth2_redirect_html() |
OAuth2 redirect handler HTML |
openapi/models.py — Full OpenAPI 3.1.0 spec as Pydantic models. 295+ model classes representing every object in the OpenAPI specification.
10.5 fastapi/security/ — Security Utilities
See Section 7.1 for the complete class inventory. Additional implementation details:
oauth2.py internals:
OAuth2PasswordBearer.__call__()extracts fromAuthorization: Bearer <token>header- If
auto_error=True(default), missing/invalid credentials raiseHTTPException(401) - If
auto_error=False, returnsNonefor missing credentials (allows optional auth) OAuth2PasswordRequestFormis a regular dependency that parsesapplication/x-www-form-urlencodedbody fields:grant_type,username,password,scope(space-separated string →scopeslist),client_id,client_secretSecurityScopesaggregates allscopesfrom the dependency chain into a singleSecurityScopes.scopeslist andSecurityScopes.scope_strstring
api_key.py internals:
APIKeyQuery(name="api_key")extracts from?api_key=<value>APIKeyHeader(name="X-API-Key")extracts from custom headerAPIKeyCookie(name="session")extracts from cookie- All support
auto_error=True/Falsebehavior
10.6 fastapi/encoders.py — JSON Encoding
jsonable_encoder() signature:
def jsonable_encoder(
obj: Any,
include: IncEx | None = None,
exclude: IncEx | None = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
custom_encoder: dict[Any, Callable] | None = None,
sqlalchemy_safe: bool = True,
) -> Any:
The sqlalchemy_safe=True default excludes keys starting with _sa (SQLAlchemy internal attributes).
10.7 fastapi/datastructures.py — UploadFile
UploadFile extends Starlette's UploadFile with Pydantic v2 validation support:
| Attribute | Type | Description |
|---|---|---|
filename |
str | None |
Original filename from the upload |
size |
int | None |
File size in bytes |
headers |
Headers |
Multipart headers |
content_type |
str | None |
MIME type from Content-Type header |
| Method | Description |
|---|---|
await read(size) |
Read bytes |
await write(data) |
Write bytes |
await seek(offset) |
Seek to position |
await close() |
Close the file |
The __get_pydantic_core_schema__ class method registers a custom Pydantic validator that raises ValueError: f'Expected UploadFile, received: {type(...)}' for non-UploadFile inputs.
10.8 fastapi/_compat/v2.py — Pydantic Compatibility
Key functions in the compatibility layer:
| Function | Purpose |
|---|---|
get_model_fields(model) |
Extract FieldInfo dict from a Pydantic model |
get_schema_from_model_field(field, ...) |
Generate JSON Schema for a ModelField |
serialize_sequence_value(field, value) |
Serialize list/set values according to field schema |
get_missing_field_error(loc) |
Generate a standardized "field required" error |
create_body_model(fields, model_name) |
Dynamically create a Pydantic model from body fields |
10.9 fastapi/sse.py — Server-Sent Events
| Class | Purpose |
|---|---|
ServerSentEvent |
Pydantic model for a single SSE event |
EventSourceResponse |
Streaming response that yields SSE events |
ServerSentEvent fields: data (any JSON-serializable), raw_data (str, sent as-is), event (event type), id (event ID, no nulls), retry (reconnect interval ms), comment (SSE comment line), sep (line separator).
10.10 fastapi/exception_handlers.py — Default Handlers
async def http_exception_handler(request, exc) -> JSONResponse | PlainTextResponse
async def request_validation_exception_handler(request, exc) -> JSONResponse
async def websocket_request_validation_exception_handler(websocket, exc) -> None
The HTTP exception handler returns PlainTextResponse if exc.detail is not a dict/list, otherwise JSONResponse. This allows both HTTPException(detail="Not found") and HTTPException(detail={"error": "not_found", "field": "id"}).
11. Testing Strategy
11.1 TestClient
FastAPI re-exports Starlette's TestClient (powered by httpx):
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items/{id}")
async def read_item(id: int):
return {"id": id}
client = TestClient(app)
def test_read_item():
response = client.get("/items/42")
assert response.status_code == 200
assert response.json() == {"id": 42}
def test_read_item_invalid():
response = client.get("/items/not-an-int")
assert response.status_code == 422
11.2 Async testing
import pytest
from httpx import AsyncClient, ASGITransport
@pytest.mark.anyio
async def test_read_item_async():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/items/42")
assert response.status_code == 200
11.3 Dependency overrides
def test_with_override():
def override_get_db():
return TestDatabase()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
response = client.get("/items/")
assert response.status_code == 200
# Clean up
app.dependency_overrides.clear()
11.4 WebSocket testing
def test_websocket():
client = TestClient(app)
with client.websocket_connect("/ws") as websocket:
websocket.send_text("hello")
data = websocket.receive_text()
assert data == "Message text was: hello"
11.5 Testing file uploads
def test_upload():
client = TestClient(app)
response = client.post(
"/uploadfile/",
files={"file": ("test.txt", b"file content", "text/plain")},
)
assert response.status_code == 200
12. Code Patterns and Conventions
12.1 Project structure conventions
project/
app/
__init__.py
main.py # FastAPI() instance + lifespan
config.py # Settings (pydantic-settings)
dependencies.py # Shared dependencies
models.py # Pydantic models (schemas)
database.py # Database engine + session
routers/
__init__.py
items.py # APIRouter for /items
users.py # APIRouter for /users
middleware/
__init__.py
timing.py
auth.py
services/ # Business logic (not FastAPI-specific)
item_service.py
user_service.py
tests/
conftest.py
test_items.py
test_users.py
alembic/ # Database migrations
requirements.txt
Dockerfile
12.2 Router organization pattern
# app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException, status
router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(verify_token)],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def list_items(): ...
@router.get("/{id}")
async def get_item(id: int): ...
# app/main.py
from app.routers import items, users
app = FastAPI()
app.include_router(items.router)
app.include_router(users.router, prefix="/api/v1")
12.3 Response model patterns
# Separate input/output models
class ItemCreate(BaseModel):
name: str
price: float
class ItemResponse(BaseModel):
id: int
name: str
price: float
model_config = ConfigDict(from_attributes=True)
class ItemList(BaseModel):
items: list[ItemResponse]
total: int
@app.post("/items/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate): ...
12.4 Error handling patterns
# Custom exception class
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
# Register handler
@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
return JSONResponse(
status_code=404,
content={"detail": f"Item {exc.item_id} not found"},
)
12.5 Dependency injection patterns
# Class-based dependencies
class Pagination:
def __init__(self, skip: int = Query(0, ge=0), limit: int = Query(100, le=1000)):
self.skip = skip
self.limit = limit
@app.get("/items/")
async def list_items(pagination: Pagination = Depends()):
return items[pagination.skip : pagination.skip + pagination.limit]
# Dependency chain
async def get_db(): ...
async def get_current_user(db = Depends(get_db)): ...
async def get_current_active_user(user = Depends(get_current_user)): ...
@app.get("/users/me")
async def read_me(user = Depends(get_current_active_user)):
return user
13. Troubleshooting
13.1 Common errors and solutions
| Error | Cause | Solution |
|---|---|---|
422 Unprocessable Entity |
Request body/params fail Pydantic validation | Check the detail array in the response for field-level errors. Ensure request matches the expected schema. |
No module named 'multipart' |
python-multipart not installed |
pip install python-multipart. Required for Form(), File(), UploadFile. |
"Could not import module" in Uvicorn |
Incorrect module path | Verify uvicorn app.main:app matches your file structure. |
| Swagger UI shows wrong URLs behind proxy | root_path not configured |
Set root_path in FastAPI constructor or --root-path in Uvicorn. |
Depends(use_cache=False) ignored |
Dependency is not the same callable object | Ensure the dependency function is the same object (not a lambda wrapper). |
| WebSocket disconnect not caught | Not catching WebSocketDisconnect |
Wrap receive_text/json() in try/except WebSocketDisconnect. |
FastAPIError: "Prefix and path cannot be both empty" |
Router with prefix="" containing a route with path="" |
Provide at least a prefix on the router or a path on the route. |
FastAPIError: "Response not awaited" |
Yield dependency with bare except: |
Change to except Exception: — never catch BaseException in yield dependencies. |
| Lifespan not running | Using on_startup + lifespan together |
Use only lifespan. on_startup/on_shutdown are deprecated and ignored when lifespan is set. |
| Async endpoint blocks event loop | Sync I/O in async def endpoint |
Either make the endpoint def (sync) or use async I/O libraries. |
PydanticV1NotSupportedError |
Using Pydantic v1 models | Migrate to Pydantic v2. FastAPI 0.135.3 does not support pydantic.v1 models. |
13.2 Performance debugging
# Add timing middleware to identify slow endpoints
import time
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
if duration > 1.0: # Log slow requests
logger.warning(
f"Slow request: {request.method} {request.url.path} "
f"took {duration:.2f}s"
)
response.headers["X-Process-Time"] = str(duration)
return response
13.3 Debug mode
app = FastAPI(debug=True)
In debug mode:
ServerErrorMiddlewarereturns full tracebacks in 500 responses (HTML format)- Starlette enables additional debug logging
Caution
Never enable
debug=Truein production. Stack traces expose internal code paths, file locations, variable values, and potentially sensitive configuration to attackers.
14. Quick Reference
14.1 Import cheat sheet
# Core
from fastapi import FastAPI, Request, Response, Depends, HTTPException, status
# Parameters
from fastapi import Path, Query, Header, Cookie, Body, Form, File, UploadFile
# Security
from fastapi.security import (
OAuth2PasswordBearer, OAuth2PasswordRequestForm, OAuth2AuthorizationCodeBearer,
HTTPBasic, HTTPBearer, HTTPBasicCredentials, HTTPAuthorizationCredentials,
APIKeyHeader, APIKeyQuery, APIKeyCookie, SecurityScopes, OpenIdConnect,
)
# Routing
from fastapi import APIRouter
from fastapi.routing import APIRoute, APIWebSocketRoute
# Responses
from fastapi.responses import (
JSONResponse, HTMLResponse, PlainTextResponse, RedirectResponse,
StreamingResponse, FileResponse, ORJSONResponse, UJSONResponse,
)
# Middleware
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
# Exceptions
from fastapi.exceptions import (
HTTPException, WebSocketException, RequestValidationError,
ResponseValidationError,
)
# Encoding
from fastapi.encoders import jsonable_encoder
# Background
from fastapi import BackgroundTasks
# SSE
from fastapi.sse import EventSourceResponse, ServerSentEvent
# Testing
from fastapi.testclient import TestClient
# WebSocket
from fastapi import WebSocket, WebSocketDisconnect
# OpenAPI
from fastapi.openapi.utils import get_openapi
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
# Static files and templates (from Starlette)
from starlette.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates
14.2 HTTP status code reference
from fastapi import status
# Success
status.HTTP_200_OK
status.HTTP_201_CREATED
status.HTTP_204_NO_CONTENT
# Redirection
status.HTTP_301_MOVED_PERMANENTLY
status.HTTP_307_TEMPORARY_REDIRECT
# Client errors
status.HTTP_400_BAD_REQUEST
status.HTTP_401_UNAUTHORIZED
status.HTTP_403_FORBIDDEN
status.HTTP_404_NOT_FOUND
status.HTTP_409_CONFLICT
status.HTTP_422_UNPROCESSABLE_ENTITY
status.HTTP_429_TOO_MANY_REQUESTS
# Server errors
status.HTTP_500_INTERNAL_SERVER_ERROR
status.HTTP_502_BAD_GATEWAY
status.HTTP_503_SERVICE_UNAVAILABLE
14.3 OpenAPI 3.1.0 schema structure
{
"openapi": "3.1.0",
"info": {
"title": "...",
"summary": "...",
"description": "...",
"version": "...",
"termsOfService": "...",
"contact": {},
"license": {}
},
"servers": [{"url": "..."}],
"paths": {
"/items/{id}": {
"get": {
"summary": "...",
"operationId": "...",
"parameters": [],
"responses": {},
"security": []
}
}
},
"components": {
"schemas": {},
"securitySchemes": {}
},
"tags": [],
"externalDocs": {},
"webhooks": {}
}
14.4 Codebase statistics
| Metric | Value |
|---|---|
| Version | 0.135.3 |
| Total files | 2,846 |
| Source characters | ~1.5M |
| Core package files | 23 (fastapi/) |
| Core package size | ~522K chars |
| OpenAPI module | 4 files, ~53K chars |
| Security module | 7 files, ~52K chars |
| Compatibility layer | 3 files, ~26K chars |
| Classes | 295 |
| Functions | 847 |
| docs_src example endpoints | 421 |
| Error messages | 13 |
| Complexity tier | Large |