← Back to articles
Mastering FastAPI: A Comprehensive Guide from Basics to Proficiency
A systematic approach to mastering FastAPI's core concepts and practical techniques, guided by the Feynman Technique, Simon Learning Method, SQ3R Reading Method, and Cornell Note-Taking System.
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 commonscommon_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
- Misconception: FastAPI requires a lot of configuration code — Reality: FastAPI is nearly zero-config; type annotations are the configuration
- Misconception: You must use
async deffor high performance — Reality: FastAPI intelligently handles bothdefandasync def. Synchronous functions run in a thread pool and won't block the event loop - 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
- 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 runComplete 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 resultsRequest 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 = NoneResponse 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 itemError 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 responseCORS (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_userDatabase 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_userBackground 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:
| Feature | WSGI | ASGI |
|---|---|---|
| Concurrency model | Synchronous, one request at a time | Asynchronous, handles multiple requests concurrently |
| WebSocket | Not supported | Native support |
| Long connections | Difficult | Native support |
| Typical server | Gunicorn (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_validatorand@model_validator - Serialization modes: Define different input/output schemas
model_validate(): Create model instances from dicts (replaces V1'sparse_obj)model_dump(): Convert models to dicts (replaces V1'sdict())
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.pyOverriding 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 myimageUvicorn Multi-Worker Mode:
# Start multiple workers for production
fastapi run app/main.py --workers 4 --port 80Key Deployment Concepts:
| Concept | Description | Common Tools |
|---|---|---|
| HTTPS | TLS Termination Proxy handles encryption | Traefik, Caddy, Nginx |
| Run on startup | Auto-start process | Docker, Systemd, Kubernetes |
| Auto-restart | Automatic recovery after crashes | Docker, Kubernetes, Supervisor |
| Replication | Multiple processes for more throughput | Uvicorn --workers, K8s |
| Memory management | Each process has its own memory space | Adjust worker count based on server resources |
Comparison with Other Frameworks
| Feature | FastAPI | Flask | Django REST |
|---|---|---|---|
| Async support | Native ASGI | Limited (Flask 2.0+) | Limited (Django 3.1+) |
| Auto API docs | Built-in (Swagger + ReDoc) | Third-party (Flask-RESTX) | Third-party (drf-spectacular) |
| Type system | Native Python type hints | None | None (manual Serializers) |
| Data validation | Built-in (Pydantic) | Manual or third-party | Built-in (Serializers) |
| Dependency injection | Built-in powerful DI | None | None |
| Performance | Very high | Medium | Medium |
| ORM integration | Framework-agnostic | Framework-agnostic | Built-in Django ORM |
| Learning curve | Low (just need Python) | Low | Medium-high (need to know Django) |
| Best for | API-first, microservices | Simple APIs, prototyping | Full-stack apps, CMS |
4. Key Notes (Cornell Note-Taking Method)
Quick Reference: Key Concepts
| Cue / Keyword | Detailed Notes |
|---|---|
| Path Operation | Declare routes with @app.get/post/put/delete/patch decorators, binding HTTP methods to handler functions |
| Path Parameters | Declared with {param} in URL, auto-matched to function params, supports type conversion and validation (Path(ge=1)) |
| Query Parameters | Auto-detected when function params are not in the path; default value = optional, no default = required |
| Request Body | Declare JSON structure with Pydantic BaseModel, used with @app.post/put |
| Response Model | response_model=SomeModel parameter declares return type, auto-filters output fields |
| Dependency Injection | Depends() declares dependencies; functions/classes/generators all work; supports nesting |
| Middleware | @app.middleware("http") intercepts requests/responses; used for logging, CORS, timing, etc. |
| CORS | CORSMiddleware configures cross-origin policy: allow_origins/methods/headers |
| OAuth2 + JWT | OAuth2PasswordBearer + PyJWT for token-based authentication |
| Background Tasks | BackgroundTasks.add_task() executes async background operations |
| WebSocket | @app.websocket("/ws") declares bidirectional communication endpoints |
| Pydantic V2 | Rust-core validation engine; BaseModel, Field, field_validator |
| ASGI | Asynchronous Server Gateway Interface; implemented by Uvicorn; supports concurrency, WebSocket |
| TestClient | httpx-based test client; test without starting a real server |
| FastAPI CLI | fastapi dev for development (auto-reload), fastapi run for production |
Quick Reference: Core APIs
| API / Decorator | Purpose | Example |
|---|---|---|
@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 validation | Path(ge=1, le=1000) |
Query() | Query parameter validation | Query(min_length=3, max_length=50) |
Body() | Request body field constraints | Body(embed=True) |
Field() | Pydantic model field constraints | Field(gt=0, max_length=100) |
Depends() | Declare dependency | Depends(get_db) |
HTTPException | Return HTTP error | HTTPException(status_code=404, detail="Not found") |
status | HTTP status code constants | status.HTTP_201_CREATED |
UploadFile | File upload handling | file: UploadFile |
Form() | Form data | username: str = Form() |
Cookie() | Cookie parameter | session_id: str = Cookie() |
Header() | Header parameter | user_agent: str = Header() |
BackgroundTasks | Background tasks | background_tasks.add_task(func) |
TestClient | Test client | client = TestClient(app) |
APIRouter() | Route grouping | router = APIRouter(prefix="/api") |
Section Summary
FastAPI's design philosophy is Type-Driven Development:
- Declare once, apply everywhere — A single type annotation serves data validation, documentation generation, and editor support
- Dependency Injection is the glue — Connect databases, authentication, configuration, and all components through
Depends() - Pydantic is the data core — All inputs and outputs pass through Pydantic model validation and conversion
- ASGI is the performance foundation — Native async support enables FastAPI to handle high-concurrency scenarios
- Standards compliance is the guarantee — Built on OpenAPI and JSON Schema, enabling seamless ecosystem integration
5. Review & Practice (SQ3R: Recite & Review)
Core Takeaways Review
- FastAPI's core loop: Type annotations → Auto-validation → Auto-documentation → Auto-serialization
Annotated[type, Depends(func)]is the modern FastAPI recommended pattern; type aliases (CommonsDep = Annotated[...]) reduce repetition- Dependency injection supports functions, classes, and generators (
yield), and can be nested into dependency trees async defanddefcan be mixed — FastAPI correctly handles synchronous/asynchronous calls- Three essentials for production deployment: HTTPS (TLS proxy), multiple workers (replication), auto-restart
- Use
dependency_overridesto 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 (supportskipandlimitquery 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 todoDELETE /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
- Path parameter order:
/users/memust be declared before/users/{user_id}, otherwise"me"will be captured as theuser_id - Synchronous blocking code: Calling synchronous IO operations (like
requests.get()) insideasync defblocks the event loop. Usehttpx.AsyncClientinstead, or change the function todef - Pydantic V1/V2 differences:
parse_obj()→model_validate(),.dict()→.model_dump(),class Config→model_config - Forgetting yield cleanup: In dependencies with
yield, if an exception is raised beforeyield, the code afteryieldwon't execute - CORS is not a backend issue: CORS is a browser security policy; Postman/curl is unaffected. In production, configure
allow_originsprecisely — don't use["*"]
Further Reading
- FastAPI Official Documentation — The most comprehensive learning resource, including tutorials and advanced guides
- Pydantic V2 Documentation — Deep dive into the data validation engine
- Starlette Documentation — Understand the ASGI toolkit underlying FastAPI
- Full Stack FastAPI Template — Official full-stack project template
- FastAPI Source Code — Reading the source is the best way to deeply understand the framework