AI Hints
Home›FastAPI Tutorial›Real-World Features
FastAPI · Lesson 9 of 12

A Real Database (SQLModel)

⏱ 3 min read▶ Video at 18:44🎓 Free
Watch this lesson in the FastAPI courseWatch on YouTube ↗Full FastAPI course

Disaster strikes at Sweet Crumbs. The server restarts in the middle of the night, and every menu change, and every order, is gone. All of Maya's data lived in memory, and memory forgets. She needs a real database.

Memory forgets
From the video: Memory forgets
Chocolate Cake
Croissant
Flat White
Sourdough Loaf
She needs a real database
Restart at 3 a.m.

SQLModel + SQLite

Meet SQLModel. It is built by the author of FastAPI, and it combines SQLAlchemy and Pydantic. One class describes both your database table, and your API data. We will use SQLite, a database that lives in a single file, with nothing to install.

SQLModel + SQLite
From the video: SQLModel + SQLite
SQLAlchemytalks to the database
Pydanticvalidates your data
SQLModelone class: your table AND your API data
SQLitea database in a single file · nothing to install

A class becomes a table

A class that inherits from SQLModel, with table equals True, becomes a table. Each field becomes a column. The ID is the primary key, and starts as None, because the database assigns it. The name is indexed, so searching by name stays fast.

main.pypython
class MenuItem(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    price: float
    available: bool = True

Engine, session, startup

The engine knows how to reach the database. A session is a conversation with it. We create one session per request, with a dependency that uses yield, exactly like lesson seven. When the request ends, the session closes. And at startup, create all makes the tables, if they do not exist yet.

main.pypython
engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})

def get_session():
    with Session(engine) as session:
        yield session

SessionDep = Annotated[Session, Depends(get_session)]

@asynccontextmanager
async def lifespan(app: FastAPI):
    SQLModel.metadata.create_all(engine)
    yield

Create, list, read

Now the endpoints. To create an item, add it to the session, commit, and refresh, to receive its new ID. To list items, run a select statement. To read one, ask the session to get it by ID, and raise a four hundred and four if it is missing. Deleting works the same way.

main.pypython
@app.post("/menu/", status_code=201)
def create_item(item: MenuItem, session: SessionDep):
    session.add(item)
    session.commit()
    session.refresh(item)
    return item

@app.get("/menu/")
def list_items(session: SessionDep, limit: int = 10):
    return session.exec(select(MenuItem).limit(limit)).all()

@app.get("/menu/{item_id}")
def read_item(item_id: int, session: SessionDep):
    item = session.get(MenuItem, item_id)

Run it

Let's run it. Start the server, and a database dot db file appears. Create a croissant, and a flat white. Each one gets an ID from the database. List the menu, and both are there.

terminal
ls database.db
database.db
curl -s -X POST localhost:8000/menu/ -H 'Content-Type: application/json' -d '{"name":"Croissant","price":4.0}'
{"price":4.0,"id":1,"name":"Croissant","available":true}
curl -s -X POST localhost:8000/menu/ -H 'Content-Type: application/json' -d '{"name":"Flat White","price":5.0}'
{"price":5.0,"id":2,"name":"Flat White","available":true}
curl -s localhost:8000/menu/
[{"price":4.0,"id":1,"name":"Croissant","available":true},{"price":5.0,"id":2,"name":"Flat White","available":true}]
The database file appears
Each item gets its ID from the database

Look inside the database

You can look at the database directly, with the sqlite three command line tool. The schema shows the table that SQLModel created from our class, and the select shows the rows. The data is really stored in that file.

terminal
sqlite3 database.db ".schema menuitem"
CREATE TABLE menuitem (
	id INTEGER NOT NULL, 
	name VARCHAR NOT NULL, 
	price FLOAT NOT NULL, 
	available BOOLEAN NOT NULL, 
	PRIMARY KEY (id)
);
CREATE INDEX ix_menuitem_name ON menuitem (name);
sqlite3 database.db "select * from menuitem"
1|Croissant|4.0|1
2|Flat White|5.0|1
The table SQLModel created from our class
The rows are really stored in that file

The real test

Now the real test. Stop the server, and start it again. List the menu. Both items survived. That is the difference between memory and a database.

terminal
fastapi dev main.py
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Application startup complete.
curl -s localhost:8000/menu/
[{"price":4.0,"id":1,"name":"Croissant","available":true},{"price":5.0,"id":2,"name":"Flat White","available":true}]
Server stopped… and started again
Both items survived: memory vs. a database

The complete code for this lesson

Here is the full main.py exactly as it runs at the end of this lesson. Save it, install FastAPI with pip install "fastapi[standard]", and start it with fastapi dev main.py, then open http://127.0.0.1:8000/docs.

main.pypython
from contextlib import asynccontextmanager
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from sqlmodel import Field, Session, SQLModel, create_engine, select


class MenuItem(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    price: float
    available: bool = True


engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})


def get_session():
    with Session(engine) as session:
        yield session


SessionDep = Annotated[Session, Depends(get_session)]


@asynccontextmanager
async def lifespan(app: FastAPI):
    SQLModel.metadata.create_all(engine)
    yield


app = FastAPI(title="Sweet Crumbs API", lifespan=lifespan)


@app.post("/menu/", status_code=status.HTTP_201_CREATED)
def create_item(item: MenuItem, session: SessionDep):
    session.add(item)
    session.commit()
    session.refresh(item)
    return item


@app.get("/menu/")
def list_items(session: SessionDep, limit: int = 10):
    return session.exec(select(MenuItem).limit(limit)).all()


@app.get("/menu/{item_id}")
def read_item(item_id: int, session: SessionDep):
    item = session.get(MenuItem, item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Menu item not found")
    return item


@app.delete("/menu/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int, session: SessionDep):
    item = session.get(MenuItem, item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Menu item not found")
    session.delete(item)
    session.commit()

Summary

Now Maya's menu is safe. Use a session dependency, SQLModel classes for tables, and the same patterns you already know. But what about slow jobs, like sending a confirmation email, while the customer waits? In the next lesson, async code, background tasks, and CORS. See you there.

Key takeaways
  • SQLModel: one class, a table and an API model
  • A session per request, via a dependency
  • Data that survives a restart
Next up: Async, Background Tasks & CORS — slow jobs and talking to a web front end