FastAPI has a few defaults that make API services easier to maintain: request validation, typed contracts, and built-in docs. These are the patterns I reuse most.
Quick start Link to heading
pip install fastapi uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
uvicorn main:app --reload
That item_id: int type hint isn’t just documentation - FastAPI validates it. Hit /items/abc and you get a 422 with a clear error message. No more manual type checking.
Why this works well in production Link to heading
- Automatic validation with Pydantic models.
- Auto-generated docs at
/docsand/redoc. - Clear contracts from type hints and response models.
- Async support when your stack is async end-to-end.
Request bodies with Pydantic Link to heading
This is where FastAPI shines:
from pydantic import BaseModel, Field
class CreateItem(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0)
description: str | None = None
@app.post("/items/")
def create_item(item: CreateItem):
return item
Invalid requests get rejected before your code even runs. The error messages tell you exactly what’s wrong.
Dependency injection Link to heading
This took me a while to appreciate, but it’s powerful:
from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
return db.query(User).filter(User.id == user_id).first()
The Depends() pattern handles setup/teardown cleanly and makes testing easy - just override the dependency.
Minimal project layout Link to heading
app/
main.py
api/
routes.py
schemas/
user.py
services/
users.py
Keeping routes, schemas, and business logic separate makes larger APIs easier to maintain.
Gotchas Link to heading
Pydantic v2 migration: If you’re starting fresh, use Pydantic v2. The syntax changed (validator → field_validator, etc.) - see Pydantic v2 migration tips for details. FastAPI 0.100+ supports both but v2 is faster.
Sync vs async: Don’t mix blocking I/O in async routes. If you’re using a sync database driver, use sync route functions. FastAPI handles this correctly but it’s easy to accidentally block the event loop.
Response model: Use response_model to control what gets returned:
@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
return db_user # password_hash gets stripped out
Further reading Link to heading
- FastAPI docs - genuinely excellent documentation
- Pydantic v2 migration guide - if you’re updating old code
- HN: Django vs FastAPI in 2025 - good discussion on when to use each