Guide

FastAPI fundamentals explained

FastAPI is a modern Python web framework for building HTTP APIs on top of Starlette and Pydantic. You declare request and response shapes with type hints; the framework validates JSON bodies, generates OpenAPI documentation, and runs async handlers efficiently under ASGI servers like Uvicorn. Teams reach for FastAPI when they want Flask-like simplicity with automatic schema validation, first-class async I/O, and interactive docs out of the box — without the full batteries-included weight of Django. This guide covers the request lifecycle, path operations and Pydantic models, dependency injection, async database patterns, middleware and error handling, testing, a Harbor Payments order API worked example, a framework decision table, pitfalls, and a checklist — alongside our Python fundamentals guide, REST API design overview, and OpenAPI documentation guide.

What FastAPI is and how requests flow

FastAPI sits on Starlette (routing, middleware, WebSockets) and Pydantic (data validation and serialization). An ASGI server such as Uvicorn accepts TCP connections, parses HTTP, and hands each request to your FastAPI application. The framework matches the URL and HTTP method to a path operation function, resolves dependencies in order, validates path parameters, query strings, headers, cookies, and body against your type annotations, runs your handler (sync or async def), serializes the return value to JSON, and sends the response.

That pipeline is why FastAPI feels fast to develop: you write ordinary Python functions with typed parameters instead of manually parsing request.json() and hand-rolling error responses. Invalid input returns 422 Unprocessable Entity with a structured error list before your business logic runs — a guardrail that prevents half-valid objects from reaching the database.

Core concepts

  • Path operation — a function decorated with @app.get, @app.post, etc., mapped to a URL template and HTTP verb.
  • Pydantic model — a BaseModel subclass defining fields, types, defaults, and validators for request/response bodies.
  • Dependency (Depends) — reusable callables injected into path operations (DB sessions, auth, pagination).
  • APIRouter — modular sub-application for grouping related endpoints under a prefix.
  • BackgroundTasks — fire-and-forget work after the response is sent (email, webhooks).
  • OpenAPI / Swagger UI — auto-generated schema at /openapi.json and interactive docs at /docs.

Path operations and Pydantic models

A minimal FastAPI app declares routes with decorators. Path parameters use the function signature; query parameters with defaults become optional filters; body payloads use Pydantic models:

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field

app = FastAPI(title="Harbor Payments API", version="1.0.0")

class OrderCreate(BaseModel):
    customer_id: str = Field(min_length=1)
    amount_cents: int = Field(gt=0, le=10_000_000)
    currency: str = Field(pattern=r"^[A-Z]{3}$")

class OrderOut(BaseModel):
    id: str
    status: str

@app.post("/orders", response_model=OrderOut, status_code=status.HTTP_201_CREATED)
async def create_order(payload: OrderCreate) -> OrderOut:
    order_id = await persist_order(payload)
    return OrderOut(id=order_id, status="pending")

response_model filters and validates the outgoing JSON — fields not in the model are stripped, which prevents accidental leakage of internal columns like internal_notes. Use response_model_exclude_none=True when optional fields should disappear from sparse responses.

Pydantic v2 (bundled with current FastAPI) uses a Rust core for validation speed. Define model_config = ConfigDict(str_strip_whitespace=True) for trimming, field_validator for cross-field rules, and model_validator(mode="after") when the whole object must be checked together — e.g. ensuring end_date > start_date.

Status codes and errors

Return explicit status codes via the decorator or JSONResponse(status_code=...). Raise HTTPException for expected failures — 404 when a resource is missing, 409 on duplicate idempotency keys, 403 when authorization fails. Register exception handlers for domain errors so every endpoint does not repeat try/except boilerplate:

@app.exception_handler(OrderNotFound)
async def order_not_found_handler(request, exc):
    return JSONResponse(status_code=404, content={"detail": str(exc)})

Dependency injection

Depends() is FastAPI's composition root. Dependencies can depend on other dependencies, forming a directed acyclic graph resolved per request. Common patterns:

  • Database session — yield an async SQLAlchemy session, commit on success, rollback on exception, close in finally.
  • Authentication — read Authorization: Bearer header, validate JWT, return current user or raise 401.
  • Pagination — parse limit and cursor query params with bounds (limit: int = Query(20, le=100)).
  • Idempotency — read Idempotency-Key header, look up prior response in Redis before executing side effects.

Use Annotated types (Python 3.9+) to avoid repeating Depends(get_db) on every parameter:

DbSession = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]

@app.get("/orders/{order_id}")
async def get_order(order_id: str, db: DbSession, user: CurrentUser):
    ...

Dependencies marked with use_cache=False run fresh each time — useful when a dependency must not be shared across multiple parameters in one request.

Async I/O, databases, and background work

FastAPI handlers may be def (sync) or async def. Use async when you await network or database I/O. Never call blocking libraries inside async def without offloading to a thread pool — a synchronous psycopg2 query in an async route blocks the entire event loop. Prefer asyncpg + SQLAlchemy 2.0 async engine, or run sync code via run_in_threadpool.

Structure larger apps with APIRouter modules: orders.router mounted at /v1/orders. Apply shared dependencies at the router level — e.g. require authentication for every route under /admin.

BackgroundTasks suits lightweight post-response work. For durable jobs (PDF generation, payout settlement), push to a queue (Celery, ARQ, or your existing message broker) instead — background tasks die if the process restarts.

Middleware and CORS

Add CORSMiddleware when browsers call your API from a separate origin. Keep allow_origins explicit in production — wildcard * with credentials enabled is invalid and dangerous. Request-ID middleware (generating X-Request-ID) simplifies log correlation across services.

Worked example: Harbor Payments order API

Harbor Payments processes B2B invoices. Their order service exposes three endpoints: create order (idempotent), fetch status, and list by customer. The team chose FastAPI for Pydantic validation of money fields and auto-generated docs for integrators.

POST /v1/orders accepts OrderCreate, requires Idempotency-Key header, writes to PostgreSQL inside a transaction, enqueues a webhook delivery task, returns 201 with Location header. Duplicate keys return the cached 200 response without double-charging.

GET /v1/orders/{order_id} uses a DB dependency and returns 404 when the UUID is unknown. Internal fields (risk_score) stay out of OrderOut.

GET /v1/customers/{customer_id}/orders paginates with cursor encoding (created_at,id tuple base64url) rather than offset — offset pagination degrades on large tables. OpenAPI documents every field; partners generate TypeScript clients from the published schema.

Tests use TestClient (sync) or httpx.AsyncClient with ASGITransport for async routes. The CI pipeline runs contract tests against the exported OpenAPI spec so accidental breaking changes fail before deploy.

Framework decision table

Need Prefer Why
Typed JSON API, async, auto OpenAPI FastAPI Validation + docs + performance without boilerplate
Minimal microservice, sync only, tiny surface Flask or Starlette Less magic; fewer dependencies for hello-world services
Admin UI, ORM, auth, migrations built-in Django + DRF Batteries included; better for content-heavy monoliths
High-concurrency I/O, npm ecosystem Node.js (Express/Fastify) Single language with frontend; event loop native
CPU-bound compute API Go or Rust Python GIL limits CPU parallelism per process
ML model serving with GPU FastAPI + dedicated worker pool Async HTTP front-end; inference in sync/thread/GPU workers

Common pitfalls

  • Blocking the event loop — sync database drivers or time.sleep inside async def stalls all concurrent requests.
  • Skipping response_model — returning ORM objects directly can leak passwords, tokens, or internal IDs.
  • Mutable default dependencies — sharing a list/dict default across requests; always yield fresh instances per request.
  • Giant monolithic main.py — split routers early; otherwise merge conflicts and import cycles accumulate.
  • Trusting client validation only — Pydantic validates shape, not authorization; always check the caller owns the resource.
  • BackgroundTasks for critical work — no durability guarantee; use a queue for money-moving side effects.
  • Open CORS in production — restrict origins; pair with proper auth, not as a substitute for it.

Production checklist

  • Run behind Uvicorn/Gunicorn with multiple workers; put nginx or a cloud load balancer in front for TLS termination.
  • Pin dependencies in requirements.txt or Poetry lockfile; scan for CVEs in CI.
  • Export OpenAPI to version control; lint with Spectral; run contract tests.
  • Use structured logging (JSON) with request IDs; expose /health and /ready probes.
  • Configure DB connection pooling (SQLAlchemy pool size, PgBouncer) — one session per request, not per query.
  • Set explicit timeouts on outbound HTTP calls; use httpx with limits.
  • Document auth in OpenAPI security schemes; test 401/403 paths.
  • Pair with JWT fundamentals and idempotency patterns for payment-grade APIs.

Key takeaways

  • FastAPI maps Python type hints to validated HTTP APIs with automatic OpenAPI documentation.
  • Pydantic models guard both inbound and outbound JSON; response_model prevents data leaks.
  • Dependency injection keeps database sessions, auth, and pagination DRY across endpoints.
  • Async handlers excel at I/O-bound work but require non-blocking database and HTTP clients.
  • Production services need routers, structured errors, durable background jobs, and contract-tested OpenAPI specs.

Related reading