Python for JS Devs · FastAPI
Prisma/TypeORM vibes — but learn the modern typed 2.0 style, not the legacy patterns the AI code used.
SQLAlchemy is the standard Python ORM. Version 2.0 introduced a typed style that plays beautifully with the type hints you learned in Lesson 06. The AI-generated project you had used the old 1.x style — this lesson is the modern replacement.
Mapped[], not ColumnBase = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer,
primary_key=True)
email = Column(String)from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column)
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True)
name: Mapped[str | None] # nullable via | NoneThe column type comes from the Mapped[...] hint — no more __allow_unmapped__ workarounds. Nullability is expressed with | None, exactly like your other type hints.
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
engine = create_engine(settings.database_url)
SessionLocal = sessionmaker(engine)
def get_db(): # the yield-dependency from Lesson 14
with SessionLocal() as db:
yield db
select(), not db.query()db.query(User).filter(
User.email == e
).first()from sqlalchemy import select
stmt = select(User).where(User.email == e)
user = db.scalars(stmt).first() # typed resultWrites are unchanged and intuitive: db.add(obj), then db.commit(), and db.refresh(obj) to reload DB-generated fields (like an autoincrement id).
new = User(email="a@b.co", name="kd")
db.add(new)
db.commit()
db.refresh(new) # now new.id is populated
def routes (FastAPI threadpools them safely). For async def routes, use the async stack: create_async_engine, AsyncSession, and await db.execute(stmt). Pick one lane and stay in it. Migrations are handled by Alembic (uv add alembic) — the "django migrate"/Prisma-migrate equivalent.
response_model (Lesson 13), not the raw ORM object. Add model_config = {"from_attributes": True} to the output model so it can read attributes off the ORM instance.
Using SQLite for speed (create_engine("sqlite:///./app.db")), define the modern User model, create the table with Base.metadata.create_all(engine), then in a plain def route: insert a user (add/commit/refresh) and read it back with select(User).where(...). A round-trip through a real DB in the 2.0 style is the win.
id: Mapped[int] = mapped_column(...) — the hint carries the type.select(User).where(...) executed via db.scalars()/db.execute() is the 2.0 style.def routes (threadpooled). Async DB needs async def + AsyncSession.