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 /docs and /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 (validatorfield_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