1. Overview & Questions (SQ3R: Survey & Question)

SQ3R Step 1: Survey the big picture and formulate key questions.

What is FastAPI?

FastAPI is a modern, high-performance web framework for building APIs with Python, based on standard Python type hints. Created by Sebastian Ramirez (@tiangolo), it has been widely adopted by companies like Microsoft, Uber, and Netflix since its release.

Core value propositions of FastAPI:

  • Fast: Extremely high performance, on par with NodeJS and Go (thanks to Starlette and Pydantic) — one of the fastest Python web frameworks
  • Fast to code: Feature development speed increased by approximately 200%–300%. Declare parameters once and get validation, serialization, and documentation for free
  • Fewer bugs: Developer errors reduced by approximately 40%. The type system catches many errors at development time
  • Intuitive: Excellent editor support with autocompletion everywhere
  • Standards-based: Built on and fully compatible with OpenAPI (formerly Swagger) and JSON Schema standards

FastAPI stands on the shoulders of two giants: Starlette handles the web layer (routing, middleware, ASGI), while Pydantic handles the data layer (validation, serialization, settings management).

Key Questions

  • When should you use FastAPI? — RESTful APIs, microservices, machine learning service backends, async IO-intensive applications
  • What advantages does FastAPI have over Flask/Django REST? — Native async, auto-generated docs, type-driven, higher performance
  • What prerequisites are needed? — Python 3.10+, basic HTTP knowledge, Python type hints
  • Why is FastAPI so fast? — ASGI async protocol, Pydantic V2 Rust core, Starlette high-performance routing

Technology Landscape

FastAPI Application
├── Core Fundamentals
│   ├── Path Operations (Routing) — HTTP methods and path declarations
│   ├── Path Parameters — Dynamic variables in URLs
│   ├── Query Parameters — URL ?key=value
│   ├── Request Body — Pydantic models for JSON data
│   ├── Response Models — Type-safe return values
│   └── Status Codes — HTTP status code declarations
├── Advanced Usage
│   ├── Dependency Injection — Powerful DI system
│   ├── Middleware — Request/response interception
│   ├── CORS — Cross-Origin Resource Sharing
│   ├── Security & Authentication — OAuth2, JWT, HTTP Basic
│   ├── Database Integration — SQLAlchemy and other ORMs
│   ├── Background Tasks
│   ├── WebSocket — Bidirectional real-time communication
│   └── File Uploads & Form Data
├── Deep Dive
│   ├── ASGI Protocol & Starlette
│   ├── Pydantic V2 Validation Engine (Rust core)
│   ├── OpenAPI Auto-Generation
│   ├── Testing Strategies (TestClient)
│   └── Deployment (Docker, Uvicorn, Gunicorn)
└── Ecosystem Tools
    ├── FastAPI CLI — Development and deployment CLI
    ├── Swagger UI / ReDoc — Interactive API documentation
    └── pydantic-settings — Configuration management

2. Explained in Plain Language (Feynman Technique)

Feynman Technique core idea: If you can't explain something in simple language, you don't truly understand it.

Core Concepts

Path Operations

FastAPI defines API endpoints using the "decorator + function" pattern. @app.get("/") means "when someone visits the / path with a GET method, execute the function below."

from fastapi import FastAPI
 
app = FastAPI()
 
@app.get("/")
def read_root():
    return {"Hello": "World"}

That's it — a decorator, a function, and a return value. FastAPI automatically converts the returned dictionary into a JSON response.

Path Parameters

Dynamic parts of the URL are declared with curly braces {}. FastAPI automatically extracts and converts the type:

@app.get("/items/{item_id}")
def read_item(item_id: int):
    # item_id is automatically converted to int
    # non-numeric input returns a clear 422 error
    return {"item_id": item_id}

Note item_id: int — this is standard Python type annotation. FastAPI uses it for data validation and documentation generation.

Query Parameters

When function parameters are not in the path, FastAPI automatically recognizes them as query parameters:

@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10):
    # Access /items/?skip=5&limit=20
    return {"skip": skip, "limit": limit}

Parameters with default values are optional; parameters without defaults are required.

Request Body & Pydantic Models

When sending JSON data to an API, declare the data structure using Pydantic's BaseModel:

from pydantic import BaseModel
 
class Item(BaseModel):
    name: str
    price: float
    is_offer: bool | None = None
 
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_name": item.name, "item_id": item_id}

One declaration gives you: automatic JSON parsing, data validation (name must be a string, price must be a number), editor autocompletion (item.name has type hints), and auto-generated API documentation.

Dependency Injection

Dependency injection means "let FastAPI call certain functions for you and pass the results to your route function." It's one of FastAPI's most powerful features:

from typing import Annotated
from fastapi import Depends, FastAPI
 
app = FastAPI()
 
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}
 
@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
 
@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons

common_parameters is shared between two routes — FastAPI automatically calls it and injects the result. No registration, no inheritance, no configuration beyond the decorator.

Automatic API Documentation

FastAPI auto-generates interactive API documentation based on your code:

  • Swagger UI: Visit http://127.0.0.1:8000/docs — test your API directly in the browser
  • ReDoc: Visit http://127.0.0.1:8000/redoc — beautiful reference documentation

You don't need to write a single line of documentation code. Your type annotations are the documentation.

Analogies & Metaphors

  • FastAPI is like a fully automated restaurant: You just write the menu (type annotations), and the kitchen automatically inspects ingredients (validation), cooks (processing), plates (serialization), and prints the menu for guests (documentation generation)
  • Pydantic models are like customs checkpoints: All incoming data must pass through strict inspection — wrong type? Rejected with a specific error message. Only qualified data proceeds to your business logic
  • Dependency Injection is like food delivery: Your route function doesn't need to fetch ingredients itself (database connections, user authentication). FastAPI acts as a "delivery system," bringing results to you on demand
  • ASGI vs WSGI is like a "highway" vs a "regular road": WSGI (used by Flask/Django) can only handle one car at a time; ASGI can handle multiple cars simultaneously, enabling FastAPI's high concurrency

Common Misconceptions Clarified

  1. Misconception: FastAPI requires a lot of configuration code — Reality: FastAPI is nearly zero-config; type annotations are the configuration
  2. Misconception: You must use async def for high performance — Reality: FastAPI intelligently handles both def and async def. Synchronous functions run in a thread pool and won't block the event loop
  3. Misconception: FastAPI can only build APIs — Reality: Through Starlette's features, FastAPI also supports WebSocket, Server-Sent Events (SSE), template rendering, static files, and more
  4. Misconception: Pydantic models are only for request bodies — Reality: Pydantic models also work for response models, configuration management (pydantic-settings), query parameter models, and more

3. Cone of Depth (Simon Learning Method)

Focused effort, goal-oriented, cone-shaped depth — start from the core and progressively expand outward.

Level 1: Core Fundamentals

Installation & Startup

# Create a virtual environment and install
pip install "fastapi[standard]"

[standard] includes uvicorn (ASGI server), httpx (test client), and other common dependencies. After creating main.py, run:

# Development mode (auto-reload)
fastapi dev
 
# Production mode
fastapi run

Complete Basic Example

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class Item(BaseModel):
    name: str
    price: float
    is_offer: bool | None = None
 
@app.get("/")
def read_root():
    return {"Hello": "World"}
 
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}
 
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_name": item.name, "item_id": item_id}

Path Parameters & Validation

Path parameters support multiple data types and validation rules:

from fastapi import Path
from typing import Annotated
 
@app.get("/items/{item_id}")
async def read_item(
    item_id: Annotated[int, Path(title="The ID of the item to get", ge=1, le=1000)]
):
    return {"item_id": item_id}

ge=1 means greater than or equal to 1; le=1000 means less than or equal to 1000. Invalid values trigger an automatic 422 error response.

Query Parameters & String Validation

from fastapi import Query
from typing import Annotated
 
@app.get("/items/")
async def read_items(
    q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
    skip: Annotated[int, Query(ge=0)] = 0,
    limit: Annotated[int, Query(le=100)] = 10,
):
    return {"q": q, "skip": skip, "limit": limit}

Request Body — Nested Models

Pydantic supports multi-level nested JSON structures:

from pydantic import BaseModel
 
class Image(BaseModel):
    url: str
    name: str
 
class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []
    image: Image | None = None
 
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

Request Body — Field Constraints

from pydantic import BaseModel, Field
 
class Item(BaseModel):
    name: str = Field(examples=["Foo"])
    description: str | None = Field(
        default=None, title="The description of the item",
        max_length=300
    )
    price: float = Field(gt=0, description="The price must be greater than zero")
    tax: float | None = None

Response Model

Use the response_model parameter to declare the return type, enabling output filtering and documentation:

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class User(BaseModel):
    username: str
    email: str
    full_name: str | None = None
    disabled: bool | None = None
 
class UserOut(BaseModel):
    username: str
    email: str
    full_name: str | None = None
 
@app.get("/users/me", response_model=UserOut)
async def read_user_me():
    # Even though the return contains the disabled field,
    # it won't appear in the response
    return {
        "username": "currentuser",
        "email": "user@example.com",
        "full_name": "Current User",
        "disabled": False,
    }

Status Codes

from fastapi import FastAPI, status
from pydantic import BaseModel
 
app = FastAPI()
 
class Item(BaseModel):
    name: str
    description: str | None = None
 
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    return item

Error Handling

from fastapi import FastAPI, HTTPException
 
app = FastAPI()
 
items = {"foo": "The Foo Wrestlers"}
 
@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

HTTPException is not a regular Python exception — it's FastAPI-specific and returns a proper HTTP error response.

Cookie and Header Parameters

from fastapi import Cookie, Header, FastAPI
from typing import Annotated
 
app = FastAPI()
 
@app.get("/items/")
async def read_items(
    ads_id: Annotated[str | None, Cookie()] = None,
    user_agent: Annotated[str | None, Header()] = None,
    x_token: Annotated[list[str] | None, Header()] = None,
):
    return {"ads_id": ads_id, "User-Agent": user_agent, "x_token": x_token}

Level 2: Advanced Usage

Dependency Injection System (In Depth)

Classes as dependencies: Not only functions, but any callable object (including classes) can serve as dependencies:

from typing import Annotated
from fastapi import Depends, FastAPI
 
app = FastAPI()
 
class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit
 
@app.get("/items/")
async def items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    return {"q": commons.q, "skip": commons.skip, "limit": commons.limit}

Sub-dependencies: Dependencies can be nested, forming a dependency tree:

from typing import Annotated
from fastapi import Depends, FastAPI
 
app = FastAPI()
 
def query_extractor(q: str | None = None):
    return q
 
def query_checker(
    extracted_q: Annotated[str, Depends(query_extractor)]
):
    if extracted_q == "admin":
        raise ValueError("Admin queries not allowed")
    return extracted_q
 
@app.get("/items/")
async def read_items(q: Annotated[str, Depends(query_checker)]):
    return {"q": q}

Global dependencies: Apply dependencies to the entire application or router:

from fastapi import Depends, FastAPI, Header, HTTPException
 
async def verify_token(x_token: str = Header()):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")
 
# All routes will execute verify_token first
app = FastAPI(dependencies=[Depends(verify_token)])

Dependencies with yield: For resource management (e.g., database sessions):

from typing import Annotated
from fastapi import Depends, FastAPI
 
app = FastAPI()
 
# Simulate a database connection
async def get_db():
    db = {"connected": True}
    try:
        yield db
    finally:
        db["connected"] = False  # Cleanup
 
@app.get("/items/")
async def read_items(db: Annotated[dict, Depends(get_db)]):
    return {"db_status": db}

Middleware

Middleware is a function that executes before each request reaches the route and after the response is returned:

from fastapi import FastAPI, Request
import time
 
app = FastAPI()
 
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

CORS (Cross-Origin Resource Sharing)

When developing with separate frontend and backend, you must configure CORS:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
 
app = FastAPI()
 
origins = [
    "http://localhost:3000",
    "http://localhost:8080",
]
 
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Security & Authentication (OAuth2 + JWT)

FastAPI includes a complete security toolkit. Here's a full OAuth2 + JWT authentication example:

from datetime import datetime, timedelta, timezone
from typing import Annotated
import jwt
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from passlib.context import CryptContext
 
# Configuration
SECRET_KEY = "your-secret-key-keep-it-secret"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
 
app = FastAPI()
 
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
class Token(BaseModel):
    access_token: str
    token_type: str
 
class TokenData(BaseModel):
    username: str | None = None
 
class User(BaseModel):
    username: str
    disabled: bool | None = None
 
class UserInDB(User):
    hashed_password: str
 
# Simulated user database
fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "hashed_password": pwd_context.hash("secret"),
        "disabled": False,
    }
}
 
def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)
 
def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)
 
def authenticate_user(username: str, password: str):
    user_dict = fake_users_db.get(username)
    if not user_dict:
        return False
    user = UserInDB(**user_dict)
    if not verify_password(password, user.hashed_password):
        return False
    return user
 
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + (
        expires_delta or timedelta(minutes=15)
    )
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
 
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    username: str = payload.get("sub")
    if username is None:
        raise credentials_exception
    user_dict = fake_users_db.get(username)
    if user_dict is None:
        raise credentials_exception
    return UserInDB(**user_dict)
 
@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
        )
    access_token = create_access_token(
        data={"sub": user.username},
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
    )
    return Token(access_token=access_token, token_type="bearer")
 
@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user

Database Integration (SQLAlchemy)

from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, Session, DeclarativeBase
 
# Database configuration
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
 
class Base(DeclarativeBase):
    pass
 
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    email = Column(String)
 
Base.metadata.create_all(bind=engine)
 
app = FastAPI()
 
# Database session dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
 
class UserCreate(BaseModel):
    username: str
    email: str
 
class UserResponse(BaseModel):
    id: int
    username: str
    email: str
 
    class Config:
        from_attributes = True
 
@app.post("/users/", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
    db_user = User(username=user.username, email=user.email)
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user
 
@app.get("/users/{user_id}", response_model=UserResponse)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.id == user_id).first()
    if not db_user:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user

Background Tasks

from fastapi import FastAPI, BackgroundTasks
 
app = FastAPI()
 
def write_log(message: str):
    with open("log.txt", mode="a") as log:
        log.write(message + "\n")
 
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_log, f"Notification sent to {email}")
    return {"message": "Notification sent in the background"}

WebSocket

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
 
app = FastAPI()
 
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)
        await manager.broadcast(f"Client #{client_id} left the chat")

File Uploads & Form Data

from fastapi import FastAPI, UploadFile, File, Form
from typing import Annotated
 
app = FastAPI()
 
@app.post("/upload/")
async def create_upload_file(
    file: Annotated[UploadFile, File(description="A file to upload")],
    description: Annotated[str, Form()],
):
    contents = await file.read()
    return {
        "filename": file.filename,
        "size": len(contents),
        "description": description,
    }

Level 3: Deep Dive

ASGI Protocol & Starlette

ASGI (Asynchronous Server Gateway Interface) is the asynchronous successor to WSGI. Key differences:

FeatureWSGIASGI
Concurrency modelSynchronous, one request at a timeAsynchronous, handles multiple requests concurrently
WebSocketNot supportedNative support
Long connectionsDifficultNative support
Typical serverGunicorn (for Flask/Django)Uvicorn (for FastAPI/Starlette)

FastAPI inherits from Starlette, a lightweight ASGI framework. FastAPI adds on top of Starlette:

  • Automatic data validation via Python type hints (through Pydantic)
  • Automatic OpenAPI documentation generation
  • Dependency injection system
  • Security utilities (OAuth2, etc.)

Request processing flow:

Client Request
  → Uvicorn (ASGI server) receives
    → Starlette middleware chain
      → FastAPI dependency injection resolution
        → Pydantic data validation
          → Route function execution
        ← Pydantic response serialization
      ← Starlette middleware chain
    ← Uvicorn returns response

Pydantic V2 Validation Engine

Pydantic V2 is a ground-up rewrite with the core validation logic written in Rust, delivering approximately 5-50x performance improvement over V1.

Key features:

  • BaseModel: Declare data models with automatic type conversion and validation
  • Field constraints: Field(gt=0, max_length=100) etc.
  • Model validators: @field_validator and @model_validator
  • Serialization modes: Define different input/output schemas
  • model_validate(): Create model instances from dicts (replaces V1's parse_obj)
  • model_dump(): Convert models to dicts (replaces V1's dict())
from pydantic import BaseModel, field_validator
 
class User(BaseModel):
    name: str
    age: int
    email: str
 
    @field_validator("age")
    @classmethod
    def age_must_be_positive(cls, v):
        if v < 0:
            raise ValueError("Age must be positive")
        return v
 
# Automatic type conversion and validation
user = User(name="Alice", age=30, email="alice@example.com")
print(user.model_dump())  # {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}

OpenAPI Auto-Generation

FastAPI automatically generates an OpenAPI 3.1 specification from your code, which is the foundation for all auto-generated documentation:

from fastapi import FastAPI
 
app = FastAPI(
    title="My API",
    description="A sample API built with FastAPI",
    version="1.0.0",
    docs_url="/docs",       # Swagger UI path
    redoc_url="/redoc",     # ReDoc path
    openapi_url="/openapi.json",  # OpenAPI Schema path
)

The following information is automatically documented for each route:

  • HTTP method and path
  • Path parameters, query parameters, request body
  • Response models and status codes
  • Validation rules (length, range, etc.)
  • Example data

Testing Strategies

FastAPI uses TestClient (based on httpx) for testing, without needing to start a real server:

from fastapi.testclient import TestClient
from fastapi import FastAPI
 
app = FastAPI()
 
@app.get("/")
async def read_root():
    return {"msg": "Hello World"}
 
client = TestClient(app)
 
def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}

Run with pytest:

pytest test_main.py

Overriding dependencies (replacing dependencies during testing):

from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends
 
app = FastAPI()
 
async def get_db():
    yield "real_db"
 
async def get_mock_db():
    yield "mock_db"
 
@app.get("/items/")
async def read_items(db: str = Depends(get_db)):
    return {"db": db}
 
def test_with_mock():
    app.dependency_overrides[get_db] = get_mock_db
    client = TestClient(app)
    response = client.get("/items/")
    assert response.json() == {"db": "mock_db"}
    app.dependency_overrides.clear()

Deployment

Docker Deployment (Recommended):

Dockerfile:

FROM python:3.14
 
WORKDIR /code
 
COPY ./requirements.txt /code/requirements.txt
 
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
 
COPY ./app /code/app
 
CMD ["fastapi", "run", "app/main.py", "--port", "80"]

Build and run:

docker build -t myimage .
docker run -d --name mycontainer -p 80:80 myimage

Uvicorn Multi-Worker Mode:

# Start multiple workers for production
fastapi run app/main.py --workers 4 --port 80

Key Deployment Concepts:

ConceptDescriptionCommon Tools
HTTPSTLS Termination Proxy handles encryptionTraefik, Caddy, Nginx
Run on startupAuto-start processDocker, Systemd, Kubernetes
Auto-restartAutomatic recovery after crashesDocker, Kubernetes, Supervisor
ReplicationMultiple processes for more throughputUvicorn --workers, K8s
Memory managementEach process has its own memory spaceAdjust worker count based on server resources

Comparison with Other Frameworks

FeatureFastAPIFlaskDjango REST
Async supportNative ASGILimited (Flask 2.0+)Limited (Django 3.1+)
Auto API docsBuilt-in (Swagger + ReDoc)Third-party (Flask-RESTX)Third-party (drf-spectacular)
Type systemNative Python type hintsNoneNone (manual Serializers)
Data validationBuilt-in (Pydantic)Manual or third-partyBuilt-in (Serializers)
Dependency injectionBuilt-in powerful DINoneNone
PerformanceVery highMediumMedium
ORM integrationFramework-agnosticFramework-agnosticBuilt-in Django ORM
Learning curveLow (just need Python)LowMedium-high (need to know Django)
Best forAPI-first, microservicesSimple APIs, prototypingFull-stack apps, CMS

4. Key Notes (Cornell Note-Taking Method)

Quick Reference: Key Concepts

Cue / KeywordDetailed Notes
Path OperationDeclare routes with @app.get/post/put/delete/patch decorators, binding HTTP methods to handler functions
Path ParametersDeclared with {param} in URL, auto-matched to function params, supports type conversion and validation (Path(ge=1))
Query ParametersAuto-detected when function params are not in the path; default value = optional, no default = required
Request BodyDeclare JSON structure with Pydantic BaseModel, used with @app.post/put
Response Modelresponse_model=SomeModel parameter declares return type, auto-filters output fields
Dependency InjectionDepends() declares dependencies; functions/classes/generators all work; supports nesting
Middleware@app.middleware("http") intercepts requests/responses; used for logging, CORS, timing, etc.
CORSCORSMiddleware configures cross-origin policy: allow_origins/methods/headers
OAuth2 + JWTOAuth2PasswordBearer + PyJWT for token-based authentication
Background TasksBackgroundTasks.add_task() executes async background operations
WebSocket@app.websocket("/ws") declares bidirectional communication endpoints
Pydantic V2Rust-core validation engine; BaseModel, Field, field_validator
ASGIAsynchronous Server Gateway Interface; implemented by Uvicorn; supports concurrency, WebSocket
TestClienthttpx-based test client; test without starting a real server
FastAPI CLIfastapi dev for development (auto-reload), fastapi run for production

Quick Reference: Core APIs

API / DecoratorPurposeExample
@app.get(path)Declare GET route@app.get("/items/{id}")
@app.post(path)Declare POST route@app.post("/items/")
@app.put(path)Declare PUT route@app.put("/items/{id}")
@app.delete(path)Declare DELETE route@app.delete("/items/{id}")
@app.middleware("http")Declare HTTP middleware@app.middleware("http")
@app.websocket(path)Declare WebSocket endpoint@app.websocket("/ws")
Path()Path parameter validationPath(ge=1, le=1000)
Query()Query parameter validationQuery(min_length=3, max_length=50)
Body()Request body field constraintsBody(embed=True)
Field()Pydantic model field constraintsField(gt=0, max_length=100)
Depends()Declare dependencyDepends(get_db)
HTTPExceptionReturn HTTP errorHTTPException(status_code=404, detail="Not found")
statusHTTP status code constantsstatus.HTTP_201_CREATED
UploadFileFile upload handlingfile: UploadFile
Form()Form datausername: str = Form()
Cookie()Cookie parametersession_id: str = Cookie()
Header()Header parameteruser_agent: str = Header()
BackgroundTasksBackground tasksbackground_tasks.add_task(func)
TestClientTest clientclient = TestClient(app)
APIRouter()Route groupingrouter = APIRouter(prefix="/api")

Section Summary

FastAPI's design philosophy is Type-Driven Development:

  1. Declare once, apply everywhere — A single type annotation serves data validation, documentation generation, and editor support
  2. Dependency Injection is the glue — Connect databases, authentication, configuration, and all components through Depends()
  3. Pydantic is the data core — All inputs and outputs pass through Pydantic model validation and conversion
  4. ASGI is the performance foundation — Native async support enables FastAPI to handle high-concurrency scenarios
  5. Standards compliance is the guarantee — Built on OpenAPI and JSON Schema, enabling seamless ecosystem integration

5. Review & Practice (SQ3R: Recite & Review)

Core Takeaways Review

  1. FastAPI's core loop: Type annotations → Auto-validation → Auto-documentation → Auto-serialization
  2. Annotated[type, Depends(func)] is the modern FastAPI recommended pattern; type aliases (CommonsDep = Annotated[...]) reduce repetition
  3. Dependency injection supports functions, classes, and generators (yield), and can be nested into dependency trees
  4. async def and def can be mixed — FastAPI correctly handles synchronous/asynchronous calls
  5. Three essentials for production deployment: HTTPS (TLS proxy), multiple workers (replication), auto-restart
  6. Use dependency_overrides to replace real dependencies during testing — no need to mock the entire framework

Hands-On Exercises

Exercise 1: Build a CRUD Todo API

Requirements:

  • GET /todos/ — List all todos (support skip and limit query parameters)
  • POST /todos/ — Create a new todo (use Pydantic model validation)
  • GET /todos/{todo_id} — Get a single todo (handle 404)
  • PUT /todos/{todo_id} — Update a todo
  • DELETE /todos/{todo_id} — Delete a todo

Exercise 2: Add Authentication

Building on Exercise 1:

  • Implement user registration and login (OAuth2 + JWT)
  • Only authenticated users can access the todo API
  • Each user can only see their own todos

Exercise 3: Docker Deployment

  • Write a Dockerfile
  • Use Docker Compose to run FastAPI and PostgreSQL together
  • Configure CORS to allow frontend access

Common Pitfalls

  1. Path parameter order: /users/me must be declared before /users/{user_id}, otherwise "me" will be captured as the user_id
  2. Synchronous blocking code: Calling synchronous IO operations (like requests.get()) inside async def blocks the event loop. Use httpx.AsyncClient instead, or change the function to def
  3. Pydantic V1/V2 differences: parse_obj()model_validate(), .dict().model_dump(), class Configmodel_config
  4. Forgetting yield cleanup: In dependencies with yield, if an exception is raised before yield, the code after yield won't execute
  5. CORS is not a backend issue: CORS is a browser security policy; Postman/curl is unaffected. In production, configure allow_origins precisely — don't use ["*"]

Further Reading