The Complete FastAPI Guide
The Complete FastAPI Guide — Basic to Advanced
Table of Contents
1. Setup & Your First App
2. Path Parameters
3. Query Parameters
4. Request Body & Pydantic Models
5. Response Models & Status Codes
6. Path Operation Configuration
7. Error Handling
8. Dependency Injection
9. Databases with SQLAlchemy
10. Authentication (OAuth2 + JWT)
11. Middleware & CORS
12. Background Tasks
13. File Uploads
14. WebSockets
15. Routers & Project Structure
16. Testing
17. Async Deep Dive
18. Deployment
19. Cheat Sheet
The Complete FastAPI Guide — Basic to Advanced
A hands-on reference for learning FastAPI, with runnable code at every step.
Table of Contents
1. Setup & Your First App
2. Path Parameters
3. Query Parameters
4. Request Body & Pydantic Models
5. Response Models & Status Codes
6. Path Operation Configuration
7. Error Handling
8. Dependency Injection
9. Databases with SQLAlchemy
10. Authentication (OAuth2 + JWT)
11. Middleware & CORS
12. Background Tasks
13. File Uploads
14. WebSockets
15. Routers & Project Structure
16. Testing
17. Async Deep Dive
18. Deployment
19. Cheat Sheet
1. Setup & Your First App
pip install fastapi "uvicorn[standard]"
[Link] :
from fastapi import FastAPI
app = FastAPI()
@[Link]("/")
def read_root():
return {"message": "Hello, FastAPI"}
Run it:
uvicorn main:app --reload
Visit [Link] → your JSON response.
Visit [Link] → auto-generated interactive Swagger UI.
Visit [Link] → alternative docs.
This auto-documentation is FastAPI’s signature feature — it’s generated from your Python type hints, which is why type hints
matter so much throughout this guide.
2. Path Parameters
@[Link]("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
Because item_id: int is type-hinted, FastAPI: - Converts the string from the URL into an int - Validates it — /items/abc
returns a 422 error automatically - Documents it as an integer in /docs
Enum path parameters (restrict allowed values)
from enum import Enum
class ModelName(str, Enum):
resnet = "resnet"
vgg = "vgg"
@[Link]("/models/{model_name}")
def get_model(model_name: ModelName):
return {"model_name": model_name, "message": "chosen"}
Order matters
Fixed paths must be declared before dynamic ones:
@[Link]("/users/me")
def read_current_user():
return {"user": "current"}
@[Link]("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}
3. Query Parameters
Anything not in the path becomes a query parameter:
@[Link]("/items/")
def list_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
Call as: /items/?skip=5&limit=20
Optional parameters
from typing import Optional
@[Link]("/items/{item_id}")
def read_item(item_id: str, q: Optional[str] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
Required query parameter (no default)
@[Link]("/items/{item_id}")
def read_item(item_id: str, needy: str):
return {"item_id": item_id, "needy": needy}
Validation with Query
from fastapi import Query
from typing import Annotated
@[Link]("/items/")
def read_items(
q: Annotated[str | None, Query(min_length=3, max_length=50)] = None
):
return {"q": q}
4. Request Body & Pydantic Models
Define the shape of incoming JSON with a Pydantic model:
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@[Link]("/items/")
def create_item(item: Item):
item_dict = item.model_dump()
if [Link]:
item_dict["price_with_tax"] = [Link] + [Link]
return item_dict
Send a request:
{
"name": "Laptop",
"price": 999.99,
"tax": 50.0
}
FastAPI validates types, generates docs, and returns a 422 with clear error details if the body is malformed.
Combining path, query, and body params
@[Link]("/items/{item_id}")
def update_item(item_id: int, item: Item, q: str | None = None):
result = {"item_id": item_id, **item.model_dump()}
if q:
result["q"] = q
return result
FastAPI figures out from the types which is path, query, or body — no extra syntax needed.
Field validation
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0, description="Must be positive")
Nested models
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
price: float
images: list[Image] | None = None
5. Response Models & Status Codes
Control exactly what shape is returned (e.g. to hide a password field):
class UserIn(BaseModel):
username: str
password: str
email: str
class UserOut(BaseModel):
username: str
email: str
@[Link]("/users/", response_model=UserOut)
def create_user(user: UserIn):
return user # password is stripped automatically by response_model
Status codes
from fastapi import status
@[Link]("/items/", status_code=status.HTTP_201_CREATED)
def create_item(item: Item):
return item
response_model_exclude_unset
Useful for PATCH-style partial updates, so defaults don’t overwrite real data:
@[Link]("/items/{id}", response_model=Item, response_model_exclude_unset=True)
def get_item(id: int):
...
6. Path Operation Configuration
@[Link](
"/items/",
response_model=Item,
summary="Create an item",
description="Create an item with all the information",
tags=["items"],
deprecated=False,
)
def create_item(item: Item):
return item
tags group endpoints in the /docs UI — essential once you have many routes.
7. Error Handling
from fastapi import HTTPException
items = {"foo": "The Foo item"}
@[Link]("/items/{item_id}")
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]}
Custom exception handlers
from fastapi import Request
from [Link] import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
[Link] = name
@app.exception_handler(UnicornException)
def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"{[Link]} caused an error"},
)
8. Dependency Injection
This is FastAPI’s most powerful concept — reusable, composable logic shared across routes.
from fastapi import Depends
def common_params(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@[Link]("/items/")
def list_items(commons: dict = Depends(common_params)):
return commons
@[Link]("/users/")
def list_users(commons: dict = Depends(common_params)):
return commons
Class-based dependencies
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
[Link] = skip
[Link] = limit
@[Link]("/items/")
def list_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
return commons
Dependencies with yield (setup/teardown, e.g. DB sessions)
def get_db():
db = SessionLocal()
try:
yield db
finally:
[Link]()
@[Link]("/items/")
def list_items(db=Depends(get_db)):
return [Link](Item).all()
Sub-dependencies & global dependencies
app = FastAPI(dependencies=[Depends(verify_token)]) # applies to every route
9. Databases with SQLAlchemy
pip install sqlalchemy
[Link] :
from sqlalchemy import create_engine
from [Link] import sessionmaker, declarative_base
SQLALCHEMY_DATABASE_URL = "sqlite:///./[Link]"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
[Link] :
from sqlalchemy import Column, Integer, String
from database import Base
class ItemDB(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
price = Column(Integer)
[Link] :
from database import engine, SessionLocal, Base
from models import ItemDB
[Link].create_all(bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
[Link]()
@[Link]("/items/")
def create_item(item: Item, db=Depends(get_db)):
db_item = ItemDB(name=[Link], price=[Link])
[Link](db_item)
[Link]()
[Link](db_item)
return db_item
For async DB access, use sqlalchemy[asyncio] with AsyncSession , or a driver like asyncpg .
10. Authentication (OAuth2 + JWT)
pip install "python-jose[cryptography]" "passlib[bcrypt]"
from datetime import datetime, timedelta
from jose import JWTError, jwt
from [Link] import CryptContext
from [Link] import OAuth2PasswordBearer, OAuth2PasswordRequestForm
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def verify_password(plain, hashed):
return pwd_context.verify(plain, hashed)
def create_access_token(data: dict):
to_encode = [Link]()
expire = [Link]() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return [Link](to_encode, SECRET_KEY, algorithm=ALGORITHM)
@[Link]("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail="Incorrect username or password")
token = create_access_token({"sub": [Link]})
return {"access_token": token, "token_type": "bearer"}
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = [Link](token, SECRET_KEY, algorithms=[ALGORITHM])
username = [Link]("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid token")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
return username
@[Link]("/users/me")
def read_users_me(current_user: str = Depends(get_current_user)):
return {"username": current_user}
Use /docs — it auto-provides an “Authorize” button once OAuth2PasswordBearer is set up.
11. Middleware & CORS
from [Link] import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["[Link]
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Custom middleware
import time
from fastapi import Request
@[Link]("http")
async def add_process_time_header(request: Request, call_next):
start = [Link]()
response = await call_next(request)
[Link]["X-Process-Time"] = str([Link]() - start)
return response
12. Background Tasks
Run code after returning a response (e.g. sending an email):
from fastapi import BackgroundTasks
def write_log(message: str):
with open("[Link]", "a") as f:
[Link](message + "\n")
@[Link]("/send-notification/{email}")
def send_notification(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(write_log, f"notification sent to {email}")
return {"message": "Notification sent"}
For heavier async jobs, use Celery, RQ, or Arq instead of BackgroundTasks .
13. File Uploads
from fastapi import UploadFile, File
@[Link]("/upload/")
async def upload_file(file: UploadFile = File(...)):
contents = await [Link]()
return {"filename": [Link], "size": len(contents)}
Multiple files:
@[Link]("/upload-multiple/")
async def upload_multiple(files: list[UploadFile] = File(...)):
return {"filenames": [[Link] for f in files]}
14. WebSockets
from fastapi import WebSocket
@[Link]("/ws")
async def websocket_endpoint(websocket: WebSocket):
await [Link]()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message received: {data}")
15. Routers & Project Structure
For real apps, split routes into separate files using APIRouter .
app/
├── [Link]
├── routers/
│ ├── [Link]
│ └── [Link]
├── [Link]
├── [Link]
└── [Link]
routers/[Link] :
from fastapi import APIRouter, Depends
router = APIRouter(prefix="/items", tags=["items"])
@[Link]("/")
def list_items():
return [{"name": "item1"}]
[Link] :
from fastapi import FastAPI
from routers import items, users
app = FastAPI()
app.include_router([Link])
app.include_router([Link])
16. Testing
pip install pytest httpx
from [Link] import TestClient
from main import app
client = TestClient(app)
def test_read_root():
response = [Link]("/")
assert response.status_code == 200
assert [Link]() == {"message": "Hello, FastAPI"}
def test_create_item():
response = [Link]("/items/", json={"name": "Book", "price": 12.5})
assert response.status_code == 200
assert [Link]()["name"] == "Book"
Run with pytest .
17. Async Deep Dive
Use async def when your route awaits I/O (DB calls, HTTP requests, etc.) using async-compatible libraries:
@[Link]("/data")
async def get_data():
async with [Link]() as client:
response = await [Link]("[Link]
return [Link]()
Rule of thumb: - Use async def + await only with async libraries ( httpx , asyncpg , async SQLAlchemy). - Use plain def if
you’re calling blocking/sync code (like plain requests or blocking DB drivers) — FastAPI runs these in a thread pool automatically
so they don’t block the event loop. - Never call blocking code inside an async def route without await ing it properly — it will
block the entire server.
18. Deployment
Production run (no --reload ):
uvicorn main:app --host [Link] --port 8000 --workers 4
Or with Gunicorn managing Uvicorn workers:
gunicorn main:app -w 4 -k [Link]
Minimal Dockerfile :
FROM python:3.12-slim
WORKDIR /app
COPY [Link] .
RUN pip install --no-cache-dir -r [Link]
COPY . .
CMD ["uvicorn", "main:app", "--host", "[Link]", "--port", "8000"]
Put Nginx or a cloud load balancer in front for TLS and reverse proxying.
19. Cheat Sheet
Task Snippet
Path param def f(item_id: int):
Query param def f(q: str = None):
Request body def f(item: PydanticModel):
Response model @[Link]("/x", response_model=Out)
Status code @[Link]("/x", status_code=201)
Raise error raise HTTPException(404, "msg")
Dependency Depends(some_func)
DB session dep def get_db(): yield db
Auth Depends(oauth2_scheme)
Background task background_tasks.add_task(fn)
Router APIRouter(prefix="/x")
Test client TestClient(app)
Suggested learning order
1. Sections 1–5 → build basic CRUD API
2. Section 7–8 → clean error handling + dependencies
3. Section 9–10 → real database + auth
4. Section 15 → refactor into routers
5. Section 16 → add tests
6. Section 17–18 → optimize and ship
Practice tip: build one project (e.g. a to-do API) and add one new concept from this guide to it at a time — auth, then a database,
then tests, then deployment. That’s the fastest way to actually retain this.