Python Advance 📂 Important concepts · 4 of 6 45 min read

FastAPI Tutorial — From Zero to Production with Diagrams and Code

A practical, diagram-driven FastAPI tutorial covering every stage from your first Hello World to production deployment. Learn type-hint-driven validation, Pydantic models, path and query parameters, dependency injection, JWT authentication, SQLAlchemy integration, async vs sync endpoints, middleware, testing, and the request lifecycle — all with runnable Python examples and visual pipelines.

Section 01

The Story That Explains FastAPI

The Airport Passport Control Desk
Imagine an old-school passport counter at a busy airport. The officer takes your passport, manually reads every field, checks handwritten forms, verifies the photo, stamps it, then files paperwork by hand. Slow. Error-prone. Long queues.

Now picture a modern e-Gate. You place your passport on a scanner. In milliseconds it parses the fields, validates them against a schema, checks your face automatically, and lets you through. Any invalid field is rejected with a clear reason on screen. No guesswork.

FastAPI is the e-Gate of Python web frameworks. You declare what the incoming request should look like with type hints, and FastAPI parses, validates, documents, and routes it — automatically, at near-Go/Node.js speed.

FastAPI is a modern, high-performance web framework for building APIs with Python 3.8+ based on standard type hints. It sits on top of Starlette (for the web parts) and Pydantic (for the data parts), giving you async speed, automatic validation, and auto-generated interactive docs — all from the same function signature you'd write anyway.

💡
The Core Insight

Every other framework asks you to write the endpoint, then write validation, then write documentation, then write serialization. FastAPI uses your Python type hints as the single source of truth — one declaration produces all four automatically. Less code, fewer bugs, better docs.


Section 02

Installation and Your First API

You need Python 3.8+ and two packages: fastapi for the framework and uvicorn as the ASGI server that runs it in production.

# Install FastAPI and the Uvicorn ASGI server
pip install "fastapi[standard]"

# Or minimal install
pip install fastapi uvicorn

Hello World — The Shortest Possible API

# file: main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

Now run it:

# Development mode with auto-reload
uvicorn main:app --reload

# Or with the new fastapi CLI
fastapi dev main.py
OUTPUT
INFO: Uvicorn running on http://127.0.0.1:8000 INFO: Started reloader process [12345] INFO: Started server process [12346] INFO: Application startup complete.
🎉
Free Superpowers — Right Now

Visit http://127.0.0.1:8000/docs — you already have a fully interactive Swagger UI playground. Visit /redoc for a beautiful alternative documentation site. Both are generated automatically from your code. No extra config, no decorators, no YAML files.


Section 03

The Three Pillars of Every Endpoint

Every FastAPI endpoint deals with three sources of incoming data. Understanding these three is 80% of understanding FastAPI.

📍
Pillar 1 — Path Parameters
/items/{item_id}
Values embedded directly in the URL path. Used for identifying a specific resource. Always required. Declared inside curly braces in the route and matched by name to a function argument.
🔍
Pillar 2 — Query Parameters
?limit=10&skip=0
Key-value pairs after the ? in the URL. Used for filtering, pagination, and sorting. Any function argument that is not a path parameter and is a simple type becomes a query parameter automatically.
📦
Pillar 3 — Request Body
Pydantic model
The JSON payload sent by the client for POST/PUT/PATCH requests. Declared as a Pydantic model class. FastAPI parses the JSON, validates every field, and hands you a typed Python object.
🔑
The Golden Rule of Argument Detection

If the argument name appears in the URL path → path parameter. Else if the type is a Pydantic model → request body. Else → query parameter. FastAPI applies these rules automatically based purely on your function signature.


Section 04

Type Hints — The Heart of FastAPI

The Type Hint Contract
Think of type hints as a signed contract between you and every caller of your API. You declare "I need an integer here, a string there, an email address, and a positive price." FastAPI enforces the contract on every request. If a client sends item_id=abc when you asked for int, FastAPI never even calls your function — it returns a clean 422 error explaining exactly what went wrong.

The Same Function — Progressively Typed

# Level 0 — No hints. Everything is a string. Errors happen inside your function.
@app.get("/items/{item_id}")
def get_item(item_id):
    return {"id": item_id}

# Level 1 — Add a type hint. FastAPI now validates + converts.
@app.get("/items/{item_id}")
def get_item(item_id: int):
    return {"id": item_id}

# Level 2 — Add constraints with Path and Query.
from fastapi import Path, Query

@app.get("/items/{item_id}")
def get_item(
    item_id: int = Path(..., gt=0, le=1000),
    q:       str | None = Query(None, max_length=50),
):
    return {"id": item_id, "q": q}
Python TypeAcceptsRejects (→ 422 error)
int42, "42""forty-two", 3.14
float3.14, "3.14", 42"pi"
booltrue, false, 1, 0, "yes", "no""maybe"
UUIDValid UUID strings"abc123"
date / datetimeISO-8601 strings"18-07-2026"
EmailStra@b.com"not-an-email"
list[str]Repeated query paramsNon-string items

Section 05

Visual Diagram — Request Lifecycle

Every HTTP request travels through the same pipeline. Knowing each stage helps you plug in middleware, dependencies, and error handling at the right place.

01
HTTP Request Arrives at Uvicorn
The ASGI server (Uvicorn / Hypercorn) receives the raw TCP connection, parses HTTP headers, method, path, and body bytes. This is a pure C-level operation — extremely fast.
02
Middleware Chain (Outer Layers)
CORS, GZip, authentication, request logging, tracing — all run in registration order. Each middleware can short-circuit the request or transform it before the router sees it.
03
Route Matching
The router finds an endpoint whose method + path template matches. If no route matches → 404. If method wrong → 405. Path parameters are extracted at this stage.
04
Dependency Resolution
FastAPI walks your Depends(...) tree — DB sessions, auth checks, config objects — resolving each in order, caching per-request. Any dependency can raise HTTPException to abort.
05
Pydantic Validation of Inputs
Path params, query params, body, headers, cookies — all validated against their type hints. Any error → 422 Unprocessable Entity with a detailed JSON explanation. Your function is never called if validation fails.
06
Your Endpoint Function Runs
Sync functions run in a thread pool; async functions run on the event loop. You get typed Python objects — do your business logic, call the DB, hit external APIs.
07
Response Serialization
Your return value is validated against response_model (if declared), then serialized to JSON. Middleware runs on the way back out. The client receives clean JSON with the right status code.

Section 06

HTTP Methods — CRUD in FastAPI

A REST API maps standard HTTP verbs to database operations. FastAPI has a decorator for each verb — the mapping is one-to-one and enforced.

HTTP VerbDecoratorCRUDTypical PurposeBody?
GET@app.get()ReadFetch a resource or listNo
POST@app.post()CreateCreate a new resourceYes
PUT@app.put()Update (full)Replace an entire resourceYes
PATCH@app.patch()Update (partial)Modify some fieldsYes
DELETE@app.delete()DeleteRemove a resourceOptional

Complete CRUD API — In-Memory Item Store

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from uuid import UUID, uuid4

app = FastAPI(title="Item API", version="1.0.0")

# ── Models ────────────────────────────────────────────
class ItemIn(BaseModel):
    name:  str   = Field(..., min_length=1, max_length=80)
    price: float = Field(..., gt=0)
    stock: int   = Field(0, ge=0)

class ItemOut(ItemIn):
    id: UUID

# ── Fake in-memory DB ─────────────────────────────────
DB: dict[UUID, ItemOut] = {}

# ── CREATE ────────────────────────────────────────────
@app.post("/items", response_model=ItemOut, status_code=status.HTTP_201_CREATED)
def create_item(payload: ItemIn):
    new = ItemOut(id=uuid4(), **payload.model_dump())
    DB[new.id] = new
    return new

# ── READ (list) ───────────────────────────────────────
@app.get("/items", response_model=list[ItemOut])
def list_items(skip: int = 0, limit: int = 20):
    return list(DB.values())[skip : skip + limit]

# ── READ (one) ────────────────────────────────────────
@app.get("/items/{item_id}", response_model=ItemOut)
def get_item(item_id: UUID):
    if item_id not in DB:
        raise HTTPException(404, "Item not found")
    return DB[item_id]

# ── UPDATE ────────────────────────────────────────────
@app.put("/items/{item_id}", response_model=ItemOut)
def update_item(item_id: UUID, payload: ItemIn):
    if item_id not in DB:
        raise HTTPException(404, "Item not found")
    DB[item_id] = ItemOut(id=item_id, **payload.model_dump())
    return DB[item_id]

# ── DELETE ────────────────────────────────────────────
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: UUID):
    if DB.pop(item_id, None) is None:
        raise HTTPException(404, "Item not found")

Section 07

Path and Query Parameters — Deep Dive

Path Parameter Validation

from fastapi import FastAPI, Path
from enum import Enum

class Category(str, Enum):
    electronics = "electronics"
    books       = "books"
    clothing    = "clothing"

@app.get("/products/{category}/{product_id}")
def get_product(
    category:   Category,                                    # enum → only 3 allowed values
    product_id: int = Path(..., ge=1, le=999999,        # must be 1..999999
                            title="Product ID",
                            description="Unique product identifier"),
):
    return {"category": category, "product_id": product_id}

Query Parameter Patterns

from fastapi import Query
from typing import Annotated

@app.get("/search")
def search(
    # Required query param
    q:       Annotated[str,  Query(min_length=3, max_length=50)],
    # Optional with default
    limit:   Annotated[int,  Query(ge=1, le=100)] = 10,
    # Boolean flag
    exact:   Annotated[bool, Query()] = False,
    # List — accept ?tag=a&tag=b&tag=c
    tag:     Annotated[list[str] | None, Query()] = None,
    # Regex validation
    sort_by: Annotated[str, Query(pattern="^(name|price|date)$")] = "name",
):
    return {"q": q, "limit": limit, "exact": exact,
            "tag": tag or [], "sort_by": sort_by}
💬
Prefer Annotated — The Modern Way

Since FastAPI 0.95+, Annotated[Type, Query(...)] is the recommended syntax over the older x: str = Query(...). Annotated separates the type from the metadata cleanly, plays nicely with static type-checkers like mypy, and lets you reuse the annotation across multiple endpoints.


Section 08

Request Body with Pydantic Models

Pydantic is FastAPI's data-validation engine. Declare a class inheriting from BaseModel, and every incoming JSON payload gets parsed, validated, and transformed into a typed Python object automatically.

from pydantic import BaseModel, Field, EmailStr, HttpUrl, field_validator
from datetime import date
from typing import Annotated

class Address(BaseModel):
    street:      str
    city:        str
    postal_code: str = Field(..., pattern=r"^\d{5,6}$")
    country:     str = "IN"

class UserRegistration(BaseModel):
    username:    str      = Field(..., min_length=3, max_length=20)
    email:       EmailStr
    password:    str      = Field(..., min_length=8)
    birth_date:  date
    website:     HttpUrl | None = None
    address:     Address                         # nested model
    tags:        list[str] = []

    @field_validator("password")
    @classmethod
    def password_strength(cls, v: str) -> str:
        if not any(c.isupper() for c in v):
            raise ValueError("Password needs at least one uppercase letter")
        if not any(c.isdigit() for c in v):
            raise ValueError("Password needs at least one digit")
        return v

@app.post("/register")
def register(user: UserRegistration):
    # user is fully validated by the time we get here
    return {"status": "created", "user": user.model_dump(exclude={"password"})}

What Happens on Invalid Input

❌ Bad Request Body
username"a"
email"not-an-email"
password"weak"
birth_date"32-13-2020"
Result422 error
✅ Auto-Generated Error Response
usernameString too short (min 3)
emailNot a valid email
passwordNeeds uppercase + digit
birth_dateInvalid date format
All errorsReturned at once

Section 09

Response Models and Status Codes

Declaring a response_model gives you three benefits at once: automatic output validation, automatic filtering of sensitive fields, and rich auto-generated documentation.

from fastapi import FastAPI, status
from pydantic import BaseModel, EmailStr

# Two versions: one with the password, one without
class UserIn(BaseModel):
    username: str
    email:    EmailStr
    password: str                # client sends this

class UserOut(BaseModel):
    username: str
    email:    EmailStr           # NO password field

@app.post("/users",
          response_model=UserOut,
          status_code=status.HTTP_201_CREATED,
          summary="Register a new user",
          tags=["users"])
def create_user(user: UserIn) -> UserIn:
    # Return the whole UserIn (with password) — FastAPI filters it
    # out because response_model=UserOut has no password field.
    return user
Status CodeConstantWhen to Use
200HTTP_200_OKDefault for successful GET/PUT/PATCH
201HTTP_201_CREATEDSuccessful POST that creates a resource
204HTTP_204_NO_CONTENTSuccessful DELETE — no body returned
400HTTP_400_BAD_REQUESTBusiness-logic error the client can fix
401HTTP_401_UNAUTHORIZEDMissing / invalid credentials
403HTTP_403_FORBIDDENAuthenticated but not allowed
404HTTP_404_NOT_FOUNDResource does not exist
422HTTP_422_UNPROCESSABLE_ENTITYValidation failure (auto by FastAPI)
500HTTP_500_INTERNAL_SERVER_ERRORUnhandled server-side exception

Section 10

Dependency Injection — FastAPI's Secret Weapon

The Restaurant Kitchen
A good chef doesn't wash the vegetables, chop the onions, and make the stock every time they cook a dish. Prep cooks handle those steps in advance and the chef simply declares what they need. FastAPI's Depends() works the same way — your endpoint declares "I need a database session, a current user, and a request ID" and FastAPI prepares them for you before your function even starts.
from fastapi import Depends, HTTPException, Header
from typing import Annotated

# ── A simple dependency ───────────────────────────────
def get_pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": min(limit, 100)}

# ── A dependency with sub-dependencies ───────────────
def verify_api_key(x_api_key: Annotated[str, Header()]):
    if x_api_key != "secret-key-123":
        raise HTTPException(401, "Invalid API key")
    return x_api_key

def get_current_user(api_key: Annotated[str, Depends(verify_api_key)]):
    # Real app: look up user by api_key in DB
    return {"id": "u_42", "name": "Alice"}

# ── Yield-based dependency (context manager style) ───
def get_db():
    db = SessionLocal()
    try:
        yield db          # pass to endpoint
    finally:
        db.close()        # always cleanup, even on error

# ── Consuming all three ──────────────────────────────
@app.get("/orders")
def list_orders(
    pagination: Annotated[dict, Depends(get_pagination)],
    user:       Annotated[dict, Depends(get_current_user)],
    db:         Annotated[Session, Depends(get_db)],
):
    return {"user": user, "page": pagination, "orders": []}
Dependencies Are Cached Per Request

If three of your dependencies all depend on get_current_user, FastAPI resolves it only once per request and reuses the result. This makes complex dependency trees efficient — no repeated DB lookups for the same user.


Section 11

Authentication with JWT (OAuth2)

FastAPI ships with OAuth2 primitives built-in. Combined with the python-jose library for JWT and passlib for password hashing, you get production-grade auth in about 60 lines.

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt, JWTError
from passlib.context import CryptContext
from datetime import datetime, timedelta, timezone
from typing import Annotated

SECRET_KEY = "replace-me-with-openssl-rand-hex-32"
ALGORITHM  = "HS256"
TOKEN_TTL  = timedelta(minutes=30)

pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2  = OAuth2PasswordBearer(tokenUrl="/token")

# Fake user DB — real app: query PostgreSQL etc.
USERS = {
    "alice": {"username": "alice",
              "hashed_pw": pwd_ctx.hash("wonderland")},
}

def authenticate(username: str, password: str):
    user = USERS.get(username)
    if not user or not pwd_ctx.verify(password, user["hashed_pw"]):
        return None
    return user

def create_token(data: dict) -> str:
    payload = data.copy()
    payload["exp"] = datetime.now(timezone.utc) + TOKEN_TTL
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

@app.post("/token")
def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
    user = authenticate(form.username, form.password)
    if not user:
        raise HTTPException(401, "Wrong username or password")
    token = create_token({"sub": user["username"]})
    return {"access_token": token, "token_type": "bearer"}

def get_current_user(token: Annotated[str, Depends(oauth2)]):
    creds_exc = HTTPException(401, "Could not validate credentials",
                              headers={"WWW-Authenticate": "Bearer"})
    try:
        payload  = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        if username is None or username not in USERS:
            raise creds_exc
    except JWTError:
        raise creds_exc
    return USERS[username]

@app.get("/me")
def read_me(user: Annotated[dict, Depends(get_current_user)]):
    return {"username": user["username"]}
⚠️
Never Commit Secret Keys

The SECRET_KEY shown above is illustrative only. In production, generate one with openssl rand -hex 32 and load it from an environment variable or a secrets manager (AWS Secrets Manager, Vault, etc.). If it leaks, every issued JWT can be forged.


Section 12

Database Integration with SQLAlchemy

# file: database.py
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.orm import declarative_base, sessionmaker

DATABASE_URL = "sqlite:///./app.db"

engine       = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base         = declarative_base()

class Item(Base):
    __tablename__ = "items"
    id    = Column(Integer, primary_key=True, index=True)
    name  = Column(String, nullable=False)
    price = Column(Float,  nullable=False)
    stock = Column(Integer, default=0)

Base.metadata.create_all(bind=engine)

# file: main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from database import SessionLocal, Item

app = FastAPI()

def get_db():
    db = SessionLocal()
    try:     yield db
    finally: db.close()

class ItemSchema(BaseModel):
    name:  str
    price: float
    stock: int = 0
    class Config:
        from_attributes = True    # read ORM objects directly

@app.post("/items", response_model=ItemSchema, status_code=201)
def create(payload: ItemSchema, db: Session = Depends(get_db)):
    item = Item(**payload.model_dump())
    db.add(item)
    db.commit()
    db.refresh(item)
    return item

@app.get("/items/{item_id}", response_model=ItemSchema)
def read(item_id: int, db: Session = Depends(get_db)):
    item = db.query(Item).filter(Item.id == item_id).first()
    if not item:
        raise HTTPException(404, "Item not found")
    return item

Section 13

Middleware and CORS

Middleware wraps every request — perfect for logging, timing, CORS, GZip compression, and trace propagation. Register in order; they execute in that order on the way in and in reverse order on the way out.

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
import time

app = FastAPI()

# ── CORS — allow the browser to call your API ───────
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com", "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ── GZip — compress large responses automatically ───
app.add_middleware(GZipMiddleware, minimum_size=1000)

# ── Custom timing middleware ─────────────────────────
@app.middleware("http")
async def add_timing_header(request: Request, call_next):
    start    = time.perf_counter()
    response = await call_next(request)
    duration = time.perf_counter() - start
    response.headers["X-Process-Time"] = f"{duration:.4f}"
    return response

Section 14

Sync vs Async — When Speed Matters

🟠 Sync Endpoint — 100 req/sec
Signaturedef get_data():
Runs inThread pool
Blocks on I/OYes
Best forCPU work, blocking libs
Examplerequests, sqlite3, PIL
🚀 Async Endpoint — 5000 req/sec
Signatureasync def get_data():
Runs inEvent loop
Blocks on I/ONo — awaits
Best forNetwork I/O, DB, HTTP calls
Examplehttpx, asyncpg, aiofiles
# ── SYNC — blocks the worker while waiting ──────────
import requests

@app.get("/weather-sync/{city}")
def weather_sync(city: str):
    r = requests.get(f"https://api.weather.com/{city}", timeout=5)
    return r.json()

# ── ASYNC — releases the worker while waiting ───────
import httpx

@app.get("/weather-async/{city}")
async def weather_async(city: str):
    async with httpx.AsyncClient(timeout=5) as client:
        r = await client.get(f"https://api.weather.com/{city}")
    return r.json()
⚠️
Never Mix Blocking Calls in Async Endpoints

Calling requests.get() or time.sleep() inside an async def endpoint blocks the entire event loop — every other request waits. If you must, wrap it with await asyncio.to_thread(blocking_func, ...) or use the async alternative. When in doubt, use def — FastAPI runs it in a thread pool safely.


Section 15

Testing FastAPI Applications

FastAPI includes TestClient, a wrapper around httpx that lets you write synchronous tests without spinning up a real server. Pair it with pytest for a fast, reliable test loop.

# file: test_main.py
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_item():
    payload = {"name": "Notebook", "price": 2.99, "stock": 100}
    r = client.post("/items", json=payload)
    assert r.status_code == 201
    data = r.json()
    assert data["name"]  == "Notebook"
    assert data["price"] == 2.99
    assert "id" in data

def test_create_item_validation_error():
    payload = {"name": "", "price": -1}    # invalid
    r = client.post("/items", json=payload)
    assert r.status_code == 422
    errors = r.json()["detail"]
    assert len(errors) >= 2            # name + price both wrong

def test_get_missing_item_returns_404():
    r = client.get("/items/00000000-0000-0000-0000-000000000000")
    assert r.status_code == 404
    assert r.json()["detail"] == "Item not found"
PYTEST OUTPUT
================ test session starts ================= platform linux -- Python 3.11.4, pytest-8.0.0 collected 3 items test_main.py::test_create_item PASSED test_main.py::test_create_item_validation_error PASSED test_main.py::test_get_missing_item_returns_404 PASSED ================= 3 passed in 0.32s ==================

Section 16

FastAPI vs Flask vs Django REST

PropertyFastAPIFlaskDjango REST
Native asyncYes (ASGI)Bolted on (3.0+)Partial
Auto validationPydantic — built inManual / marshmallowSerializers
Auto docs (Swagger)Free at /docsExtension neededdrf-spectacular
Type-hint drivenYes — core designNoNo
Speed (JSON API)~30k req/s~8k req/s~6k req/s
Learning curveGentleVery gentleSteeper
Best forAPIs, microservices, ML servingSmall APIs, quick prototypesFull websites + admin
Bundled ORMNo — pick your ownNoDjango ORM
Bundled adminNoNoDjango Admin
🏆
The Practitioner's Rule

Choose FastAPI for JSON APIs, microservices, and ML model serving — it's the fastest to write, safest to run, and best-documented by default. Choose Django when you need a full website with admin, auth, and templates out of the box. Choose Flask for the smallest possible stack when you don't need auto-docs or validation.


Section 17

Deployment — From Laptop to Production

Production Server with Gunicorn + Uvicorn Workers

# Install
pip install "fastapi[standard]" gunicorn uvicorn

# Run with 4 workers
gunicorn main:app \
    -w 4 \
    -k uvicorn.workers.UvicornWorker \
    -b 0.0.0.0:8000 \
    --access-logfile - \
    --error-logfile -

Dockerfile

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["gunicorn", "main:app",
     "-w", "4",
     "-k", "uvicorn.workers.UvicornWorker",
     "-b", "0.0.0.0:8000"]
🛠️ Production Deployment Checklist
Workers
Use 2 × CPU cores + 1 workers. Each worker is a separate process, avoiding the GIL.
HTTPS
Terminate TLS at your reverse proxy (Nginx, Caddy, ALB), not in Uvicorn.
Env
Load secrets from environment variables — use pydantic-settings.
Logs
Log JSON to stdout. Let the platform aggregate (CloudWatch, Datadog, Grafana Loki).
Health
Expose a /health endpoint. Kubernetes and load balancers rely on it.
Docs
Disable /docs and /redoc in production if the API is internal — set docs_url=None.

Section 18

When to Use FastAPI

JSON REST APIs
Perfect fit for microservices, backends for SPAs and mobile apps, and public APIs. Auto-generated docs shorten integration time significantly.
SaaS backends, mobile APIs
Machine Learning Serving
Wrap PyTorch or scikit-learn models. Pydantic validates inputs so bad data never reaches your model. Async endpoints handle concurrent inference well.
ML inference, LLM proxies
Real-Time / WebSockets
Native ASGI support means WebSockets, Server-Sent Events, and streaming responses are first-class. Excellent for chat, dashboards, live updates.
chat, notifications, streaming
Server-Side Rendered Websites
FastAPI can render Jinja2 templates, but it lacks Django's admin, ORM, migrations, and forms. For content-heavy websites, Django is a better tool.
blogs, CMS, e-commerce
Heavy Legacy Sync Codebase
If your dependencies are all blocking (older DB drivers, blocking SDKs), the async advantage disappears. Flask may be a simpler choice.
legacy integrations
Static Sites
Don't use any web framework for a pure static site. Deploy pre-built HTML/CSS/JS to a CDN (Cloudflare Pages, Netlify, S3+CloudFront) instead.
docs, marketing pages

Section 19

Golden Rules

⚡ FastAPI — Non-Negotiable Rules
1
Type-hint everything. Type hints are not documentation — they are the validation, serialization, and OpenAPI generation. Skipping them turns FastAPI into a slow Flask.
2
Use Pydantic BaseModel for every request body and response. Do not accept raw dict — you lose validation, docs, and IDE support all at once.
3
Prefer Annotated[Type, Query(...)] over the older default-value syntax. It's the officially recommended style since 0.95 and works better with type-checkers.
4
Choose async or sync per endpoint, never mix. If you call blocking code, use def; FastAPI runs it in a thread pool. If everything is await-able, use async def. Blocking inside async destroys performance.
5
Always set an explicit response_model for public APIs. It filters sensitive fields (passwords, internal IDs) and locks the response schema so accidental leaks don't ship.
6
Use Depends() for anything that could be reused — DB sessions, auth, pagination, feature flags. Dependencies are cached per request and testable in isolation.
7
Split large apps into routers. Use APIRouter(prefix="/users", tags=["users"]) in a separate file and include it via app.include_router(users.router). Keeps main.py under 50 lines even for big projects.
8
Deploy behind a reverse proxy with Gunicorn + Uvicorn workers (-k uvicorn.workers.UvicornWorker). Never expose Uvicorn directly to the internet in production — you want TLS termination and access logs handled by Nginx or a cloud load balancer.