This page wires FastAPI to a relational database using SQLAlchemy 2.0 (sync style) and the Depends system: one session per request, clean separation between DB models and Pydantic schemas.
pip install sqlalchemy
# db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
DATABASE_URL = "postgresql://user:pass@localhost:5432/appdb"
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
class Base(DeclarativeBase):
pass
# db/models.py
from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from db.session import Base
class Item(Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), index=True)
price: Mapped[float]
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
owner: Mapped["User"] = relationship(back_populates="items")
from db.session import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
FastAPI opens a session before the endpoint and closes it after — including on exceptions. Endpoints never create sessions themselves; they receive db: Session = Depends(get_db).
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
app = FastAPI()
@app.post("/items/", response_model=ItemOut, status_code=201)
def create_item(item_in: ItemCreate, db: Session = Depends(get_db)):
item = Item(**item_in.model_dump())
db.add(item)
db.commit()
db.refresh(item) # reload DB-generated fields (id)
return item
@app.get("/items/{item_id}", response_model=ItemOut)
def read_item(item_id: int, db: Session = Depends(get_db)):
item = db.get(Item, item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return item
from pydantic import BaseModel, ConfigDict
class ItemOut(BaseModel):
model_config = ConfigDict(from_attributes=True) # Pydantic v2; v1: orm_mode = True
id: int
name: str
price: float
from_attributes=True lets response_model=ItemOut read data from the ORM object (item.name) instead of only dicts. Returning the ORM object directly keeps passwords or internal columns out when the output schema excludes them.
from sqlalchemy import select
@app.get("/items/", response_model=list[ItemOut])
def list_items(min_price: float = 0, db: Session = Depends(get_db)):
stmt = (
select(Item)
.where(Item.price >= min_price)
.order_by(Item.price.desc())
.limit(50)
)
return db.scalars(stmt).all()
# lazy vs eager loading:
stmt = select(Item).options(selectinload(Item.owner)) # avoid N+1 queries
SQLAlchemy 2.0 uses a unified select() style. Beware the N+1 problem: accessing item.owner in a loop triggers one query per item unless you eager-load with selectinload or joinedload.
Base.metadata.create_all(bind=engine) # quick start (dev only)
For real projects use Alembic: versioned migrations (alembic revision --autogenerate, alembic upgrade head) so schema changes are reviewable and reversible. create_all never alters existing tables.
A: The session closed before you read a lazy relationship. Set expire_on_commit=False so objects stay usable after commit(), eager-load relationships, or serialize within the request (the response_model conversion happens while the session is still open).
A: Keep the unit of work inside the request: db.commit() per logical operation, or open a transaction in get_db and commit/rollback in the finally for all-or-nothing endpoints. Do not share one session across requests — sessions are not thread-safe.
A: Sessions that are never closed (missing finally: db.close()) or leaked connections from forgotten commits. Sync SQLAlchemy blocks a thread per query — with many concurrent sync endpoints, tune pool_size/max_overflow and consider the async driver (advanced level) so the event loop handles waiting.
The pattern to memorize: get_db yield-dependency → endpoint receives a Session → SQLAlchemy models for storage → Pydantic schemas (from_attributes=True) for the API boundary. With Alembic for migrations, this is the backbone of most FastAPI services.