FASTAPI / 6. DATABASE WITH SQLMODEL

Database Integration with SQLModel

SQL databases with Pydantic-native ORM — SQLite, PostgreSQL, MySQL


EXPLANATION

SQLModel is the official companion library for FastAPI, created by the same author (Sebastián Ramírez). It merges Pydantic models and SQLAlchemy ORM into one class — your data model is your API model.

The pattern: one class drives everything:
• Define a class with table=True → it becomes a SQL table (SQLAlchemy)
• Use the same class for request/response validation (Pydantic)
• Create separate "Create" and "Read" variants for input vs output

SQLModel under the hood:
• Uses SQLAlchemy for the actual SQL generation and execution
• Uses Pydantic for validation and serialization
• Session is from SQLModel but wraps SQLAlchemy's Session
• Supports SQLite (dev), PostgreSQL, MySQL (production)

The session pattern with Depends():
• get_db() opens a Session, yields it, then closes it (finally)
• FastAPI calls get_db() before the route, injects the session
• After the route returns (or raises), the session closes
• This guarantees no leaked connections

CRUD operations:
• Create → session.add(obj) then session.commit() then session.refresh(obj)
• Read → session.get(Model, id) or session.exec(select(Model).where(...))
• Update → fetch object, mutate fields, session.add(obj), session.commit()
• Delete → session.delete(obj), session.commit()

Relationships: SQLModel supports one-to-many and many-to-many using SQLAlchemy's Relationship and FK columns. Use selectinload for eager loading to avoid N+1 queries.

ARCHITECTURE

Single class → multiple roles:

  class Task(SQLModel, table=True):   ← SQL table definition
      id: int | None = Field(primary_key=True)
      title: str
      done: bool = False
               │
        ┌──────┼──────┐
        ↓             ↓
  SQLAlchemy ORM   Pydantic Model
  (creates table)  (validates data)

  Database session lifecycle:
  ┌──────────────────────────────────────┐
  │  def get_db():                       │
  │      with Session(engine) as db:     │
  │          yield db     ← route runs   │
  │      # auto-closed here              │
  └──────────────────────────────────────┘

  SQL generated automatically:
  session.exec(select(Task).where(Task.done == False))
  → SELECT * FROM task WHERE done = 0

CODE

PYTHON
1from fastapi import FastAPI, Depends, HTTPException
2from sqlmodel import Field, Session, SQLModel, create_engine, select
3from typing import Annotated
4
5# ── Database setup ─────────────────────────────────────
6DATABASE_URL = "sqlite:///./tasks.db"
7engine = create_engine(DATABASE_URL, echo=True) # echo=True logs SQL
8
9def create_tables():
10 SQLModel.metadata.create_all(engine)
11
12# ── Models ─────────────────────────────────────────────
13class TaskBase(SQLModel):
14 title: str
15 description: str | None = None
16 done: bool = False
17
18class Task(TaskBase, table=True): # the actual DB table
19 id: int | None = Field(default=None, primary_key=True)
20
21class TaskCreate(TaskBase): # for POST body (no id)
22 pass
23
24class TaskRead(TaskBase): # for GET response (has id)
25 id: int
26
27# ── Dependency ─────────────────────────────────────────
28def get_db():
29 with Session(engine) as db:
30 yield db
31
32DB = Annotated[Session, Depends(get_db)]
33
34# ── App ────────────────────────────────────────────────
35app = FastAPI(lifespan=lambda app: (create_tables(), None) and None)
36
37@app.on_event("startup") # simpler alternative to lifespan
38def on_startup():
39 create_tables()
40
41# CREATE
42@app.post("/tasks/", response_model=TaskRead, status_code=201)
43def create_task(task: TaskCreate, db: DB):
44 db_task = Task.model_validate(task)
45 db.add(db_task)
46 db.commit()
47 db.refresh(db_task)
48 return db_task
49
50# READ ALL
51@app.get("/tasks/", response_model=list[TaskRead])
52def get_tasks(db: DB, done: bool | None = None):
53 query = select(Task)
54 if done is not None:
55 query = query.where(Task.done == done)
56 return db.exec(query).all()
57
58# READ ONE
59@app.get("/tasks/{task_id}", response_model=TaskRead)
60def get_task(task_id: int, db: DB):
61 task = db.get(Task, task_id)
62 if not task:
63 raise HTTPException(status_code=404, detail="Task not found")
64 return task
65
66# UPDATE
67@app.patch("/tasks/{task_id}", response_model=TaskRead)
68def update_task(task_id: int, task_update: TaskCreate, db: DB):
69 task = db.get(Task, task_id)
70 if not task:
71 raise HTTPException(status_code=404, detail="Task not found")
72 task_data = task_update.model_dump(exclude_unset=True)
73 task.sqlmodel_update(task_data)
74 db.add(task)
75 db.commit()
76 db.refresh(task)
77 return task
78
79# DELETE
80@app.delete("/tasks/{task_id}", status_code=204)
81def delete_task(task_id: int, db: DB):
82 task = db.get(Task, task_id)
83 if not task:
84 raise HTTPException(status_code=404, detail="Task not found")
85 db.delete(task)
86 db.commit()
← PREV5. JWT AuthenticationNEXT →7. Middleware, CORS & Routers