Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

FastAPI Administrator and Developer Guide

Generated May 2026 from tiangolo/fastapi at 9653034

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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

  1. Architecture Overview
  2. System Requirements
  3. Configuration Reference
  4. Deployment and Scaling
  5. Operations
  6. Performance
  7. Security
  8. Developer Internals
  9. Integration Points
  10. Module Reference
  11. Testing Strategy
  12. Code Patterns and Conventions
  13. Troubleshooting
  14. 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 AsyncExitStackMiddleware is FastAPI-specific (defined in fastapi/middleware/asyncexitstack.py). It creates an AsyncExitStack on request.state that dependency injection uses to manage generator-based dependencies (those using yield). 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, and typing-extensions. Everything else — python-multipart, orjson, ujson, python-jose, passlib, sqlmodel, httpx — is optional and only needed when you use the corresponding features. The python-multipart dependency is required at runtime only if you declare Form(), File(), or UploadFile parameters.


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) + 1 for 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_url to None disables the OpenAPI schema endpoint entirely, which also disables /docs and /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_cache decorator on get_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 .env file on every request that depends on settings. For testing, use app.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 --reload in production containers. The file-watcher consumes CPU, and the auto-reload behavior creates unpredictable restart windows. Always use multi-worker mode (--workers) or Gunicorn with UvicornWorker for 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/v1 to Uvicorn or set root_path="/api/v1" in the FastAPI() constructor if the proxy strips or rewrites the path prefix. Without this, the OpenAPI schema will generate incorrect URLs for the interactive docs. The root_path_in_servers=True default ensures the root path appears in the OpenAPI servers array.

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 lifespan context manager receives the FastAPI app instance as its argument. Resources initialized during startup should be stored on app.state and accessed from dependencies or endpoints. The yield statement 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:

  1. Stop accepting new connections
  2. Wait for in-flight requests to complete (up to --timeout-graceful-shutdown, default 0 = wait forever)
  3. Run lifespan shutdown
  4. 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 an async def endpoint blocks the entire event loop, preventing all other requests from being processed. Either use def (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=["*"] with allow_credentials=True is a security misconfiguration. The CORS spec requires that when credentials are allowed, origins must be explicitly listed. Starlette's CORSMiddleware will silently downgrade allow_credentials to False when allow_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:

  1. Inspects the endpoint function signature using inspect.signature()
  2. For each parameter with a Depends default, recursively resolves the dependency's own parameters
  3. Builds a flat execution plan (topological sort)
  4. 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 with yield uses a bare except: clause (catching BaseException), it can intercept GeneratorExit and CancelledError, leading to the error FastAPIError: "Response not awaited". Always use except Exception: in yield dependencies, never bare except:.

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):

  1. get_openapi() is called with the app's routes and metadata
  2. For each APIRoute, extract:
    • Path and HTTP methods
    • Request body schema (from Pydantic models in Body params)
    • Query/path/header/cookie parameter schemas
    • Response model schema
    • Security requirements from Security() dependencies
  3. Pydantic models are converted to JSON Schema via model.model_json_schema()
  4. When separate_input_output_schemas=True (default), input schemas use mode='validation' and output schemas use mode='serialization' — this means models with computed fields, aliases, or default factories may have different input/output representations
  5. Schemas are deduplicated and placed in components.schemas
  6. 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's FieldInfo with additional metadata (field location, alias, required status, validators).
  • GenerateJsonSchema — Custom JSON Schema generator that handles FastAPI-specific schema modifications (e.g., UploadFile represented as {"type": "string", "format": "binary"}).
  • Validation adapters — Functions that bridge between FastAPI's parameter extraction and Pydantic v2's TypeAdapter for validation.

Note

FastAPI dropped Pydantic v1 support. The PydanticV1NotSupportedError in encoders.py and utils.py is raised if a pydantic.v1 model is detected as a response model or passed to jsonable_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 sends ServerSentEvent objects over a text/event-stream connection. Accepts an async generator as its content source.

Validation rules:

  • id field must not contain null characters (\0)
  • data and raw_data are mutually exclusive on the same event
  • data is serialized via jsonable_encoder() then JSON-dumped; raw_data is 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

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

BackgroundTasks run 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. BackgroundTasks are 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 than BaseHTTPMiddleware because it avoids reading the entire request body into memory. BaseHTTPMiddleware buffers the request body to construct a Request object, 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 → serves self.openapi() as JSON
  • docs_url → serves Swagger UI HTML
  • redoc_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__() calls get_typed_signature() to inspect the endpoint's type hints
  • Dependencies are pre-compiled into a Dependant tree 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 values
  • alias — alternative name in the request
  • title, description — OpenAPI documentation
  • gt, ge, lt, le — numeric constraints
  • min_length, max_length, pattern — string constraints
  • examples — OpenAPI examples
  • deprecated — mark as deprecated
  • include_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 from Authorization: Bearer <token> header
  • If auto_error=True (default), missing/invalid credentials raise HTTPException(401)
  • If auto_error=False, returns None for missing credentials (allows optional auth)
  • OAuth2PasswordRequestForm is a regular dependency that parses application/x-www-form-urlencoded body fields: grant_type, username, password, scope (space-separated string → scopes list), client_id, client_secret
  • SecurityScopes aggregates all scopes from the dependency chain into a single SecurityScopes.scopes list and SecurityScopes.scope_str string

api_key.py internals:

  • APIKeyQuery(name="api_key") extracts from ?api_key=<value>
  • APIKeyHeader(name="X-API-Key") extracts from custom header
  • APIKeyCookie(name="session") extracts from cookie
  • All support auto_error=True/False behavior

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:

  • ServerErrorMiddleware returns full tracebacks in 500 responses (HTML format)
  • Starlette enables additional debug logging

Caution

Never enable debug=True in 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