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
BaseModelsubclass 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.jsonand 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: Bearerheader, validate JWT, return current user or raise401. - Pagination — parse
limitandcursorquery params with bounds (limit: int = Query(20, le=100)). - Idempotency — read
Idempotency-Keyheader, 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.sleepinsideasync defstalls 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.txtor 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
/healthand/readyprobes. - Configure DB connection pooling (SQLAlchemy pool size, PgBouncer) — one session per request, not per query.
- Set explicit timeouts on outbound HTTP calls; use
httpxwith 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_modelprevents 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
- Python fundamentals explained — syntax, packaging, and asyncio basics
- REST API design explained — resources, verbs, status codes, and pagination
- OpenAPI explained — specs, codegen, and contract testing
- PostgreSQL fundamentals explained — MVCC, pooling, and production patterns