FastAPI Setup

Part 1, Chapter 3


In this chapter, we'll create a new FastAPI project, set up a templating engine, and create a database with the necessary tables.

Initial Setup

We'll create a party app in which you'll be able to manage party details and a gift registry.

First, create a new project and initialize a git repo:

$ mkdir fastapi-party && cd fastapi-party
$ git init
$ git checkout -b main

To avoid adding unwanted files to git, add a .gitignore file. Then, populate it from the .gitignore file here.

Add the .gitignore file to git, and create a commit:

$ git add .gitignore
$ git commit -m 'Initial commit'

Next, sign up for a GitHub account if you don't already have one. Add a new repo for your project, making sure to add the remote origin repo to your local repo.

Push your changes from your local repository to your new remote repository:

$ git push -u origin main

Having trouble setting up your remote GitHub repo? Review How to Push to GitHub.

Create a virtual environment:

$ python3.13 -m venv venv
$ source venv/bin/activate

Create a requirements.txt file, and add the following to it:

alembic==1.14
fastapi[standard]==0.115.11
pytest==8.3
sqlmodel==0.0.24

Notes:

  1. Alembic is a tool for managing database migrations for SQLAlchemy.
  2. fastapi[standard] along with FastAPI, installs HTTPX, Jinja, python-multipart, email-validator, and Uvicorn.
  3. We'll be using pytest for writing our tests.
  4. SQLmodel, created by the same author as FastAPI, connects Pydantic and SQLAlchemy (a Python SQL toolkit and ORM, which is installed as a dependency) to simplify data modeling.

Run the installation:

(venv)$ pip install -r requirements.txt

Create a Python package where our app-related code will live and the main FastAPI file:

$ mkdir party_app
$ touch party_app/__init__.py
$ touch party_app/main.py

Code directly related to our app will be in the "party_app" package, and main.py is the main entry point for the FastAPI app.

Add the following to main.py to initialize a basic FastAPI application:

# party_app/main.py

from fastapi import FastAPI


app = FastAPI()

Templates

FastAPI comes with a Jinja wrapper, which makes the process of integrating the template language in our app fast and easy.

Inside "party_app", create a new file called dependency.py:

$ touch party_app/dependency.py

Open the file and add the following to it:

import os
from typing import Annotated

from fastapi import Depends
from fastapi.templating import Jinja2Templates


_templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates"))


def _get_templates():
    return _templates


Templates = Annotated[Jinja2Templates, Depends(_get_templates)]

Here, we created a reusable Jinja2Templates object. That's actually the only thing that needs to be done in order to use the templates, as visible in the official documentation. The _get_templates and Templates then create a dependency, so we can leverage FastAPI's dependency Injection system.

Now, we'll be able to use templates like so:

def gift_update_form_partial(
        request: Request,
        templates: Templates # dependency injection
):
    return templates.TemplateResponse(
        request=request,
        name="my_template.html",
        context={"content": "some text"},
    )

Want to learn more about dependency injection? Check out the Python Dependency Injection article.

Static Files

Your static files, which we'll add in the next chapter, will all live in the same directory. Create the directory now:

$ mkdir -p party_app/static

Mount the static files in the FastAPI app by adding the following to main.py:

# party_app/main.py

from pathlib import Path

from fastapi import FastAPI  # NEW
from fastapi.staticfiles import StaticFiles  # NEW


app = FastAPI()


# NEW
app.mount(
    "/party_app/static",
    StaticFiles(directory=Path(__file__).resolve().parent / "static"),
    name="static",
)

Base Template

We need a base template that will be used by all other templates.

Start by creating a "templates" directory:

(venv)$ mkdir -p party_app/templates

Create a new file called base.html inside "party_app/templates".

This is just a typical base template, so add the following to it:

<!--party_app/templates/base.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Party!</title>
</head>
<body>
<main>
    {% block content %}
    {% endblock %}
</main>
</body>
</html>

Database

Throughout the course we'll use SQLite, since it comes with Python itself and consequently requires no additional setup. We'll change SQLite to a more robust database before the deployment at the end of the course.

As opposed to Django, FastAPI, being lightweight, doesn't come with the tools for managing databases. So we need to use additional libraries to support that. We already installed SQLModel for ORM and Alembic for managing database migrations; we just need to set them up.

Models

SQLModel is essentially a wrapper for Pydantic and SQLAlchemy, allowing you to write a single model that can be used as both, database model and Pydantic model. Since its author is the same as for FastAPI, it provides wonderful documentation on how to integrate the two tools. This course, when setting up SQLModel, mostly follows its examples.

Create a file to hold our models:

$ touch party_app/models.py

Add the following to models.py:

from datetime import date, time
from decimal import Decimal
from typing import List, Optional
from uuid import UUID, uuid4

from sqlmodel import Field, Relationship, SQLModel, Column, String, Text


# Common base model for the Party resource.
# Defines shared fields used by both the database and form models.
class PartyBase(SQLModel):
    party_date: date
    party_time: time
    invitation: str = Field(
        sa_column=Column(Text), min_length=10
    )  # Column(Text) affects PostgreSQL (not SQLite); min_length is enforced by Pydantic.
    venue: str = Field(
        sa_column=Column(String(100))
    )  # Column(String(100)) affects PostgreSQL (not SQLite).


# Database model for the Party resource.
# Inherits from PartyBase and represents the actual table, including relationships.
class Party(PartyBase, table=True):
    uuid: UUID = Field(default_factory=uuid4, primary_key=True)
    gifts: List["Gift"] = Relationship(
        back_populates="party"
    )  # Defines ORM relationship: party.gifts returns associated Gift objects.
    guests: List["Guest"] = Relationship(
        back_populates="party"
    )  # Defines ORM relationship: party.guests returns associated Guest objects.


# Form model for the Party resource.
# Used by FastAPI to validate and process incoming form data for Party operations.
class PartyForm(PartyBase):
    pass


# Common base model for the Gift resource.
# Contains shared fields used by both the database and form models.
class GiftBase(SQLModel):
    gift_name: str = Field(sa_column=Column(String(100)))
    price: Decimal = Field(decimal_places=2, ge=0)  # Decimal constraints enforced by Pydantic (not SQLite).
    link: Optional[str]
    party_id: UUID = Field(
        default=None, foreign_key="party.uuid"
    )  # Defines a foreign key connecting the gift to a party at the database level.


# Database model for the Gift resource.
# Inherits from GiftBase and represents the gift table, including its relationship to Party.
class Gift(GiftBase, table=True):
    uuid: UUID = Field(default_factory=uuid4, primary_key=True)
    party: Party = Relationship(
        back_populates="gifts"
    )  # Defines ORM relationship: gift.party returns the associated Party object.


# Form model for the Gift resource.
# Used for validating and processing incoming gift data via FastAPI.
class GiftForm(GiftBase):
    pass


# Common base model for the Guest resource.
# Defines shared fields used by both the database and form models.
class GuestBase(SQLModel):
    name: str = Field(sa_column=Column(String(100)))
    attending: bool = False
    party_id: UUID = Field(
        default=None, foreign_key="party.uuid"
    )  # Defines a foreign key connecting the guest to a party at the database level.


# Database model for the Guest resource.
# Inherits from GuestBase and maps to the guest table, including its relationship to Party.
class Guest(GuestBase, table=True):
    uuid: UUID = Field(default_factory=uuid4, primary_key=True)
    party: Party = Relationship(
        back_populates="guests"
    )  # Establishes ORM relationship: guest.party returns the associated Party object.


# Form model for the Guest resource.
# Used by FastAPI for validating and processing incoming guest data.
class GuestForm(GuestBase):
    pass

We have three resources: Party, Gift, and Guest. But each of those resources need two sorts of representation -- one for the database and one for the form. Database representation needs additional fields, like the UUID and the relationships. The form and database representations have some fields in common, which we extracted to the base models. Since the form models have no additional fields, they just inherit from the base models.

Gift and Guest are connected to Party with a foreign key.

Due to the Relationship fields, we can easily access the connected objects (e.g., party.gifts, gift.party).

If you're not familiar with SQLModel, review the FastAPI and Pydantic - Intro tutorial, and inspect the comments in the code above.

We need to create an engine that will hold the network connection to the database.

In the project root, create a new file called db.py:

from pathlib import Path

from sqlmodel import create_engine


database_file_path = Path(__file__).resolve().parent.absolute() / "database.db"
engine = create_engine(f"sqlite:///{database_file_path}")

Migration

We'll use Alembic for handling database migrations.

Following the official documentation, we initialize it with:

(venv)$ alembic init alembic

To learn more about what the init command does, review the docs.

This should have created an "alembic" folder along with an alembic.ini file in your project root.

Update sqlalchemy.url in alembic.ini:

sqlalchemy.url = sqlite:///database.db

Import SQLModel and your own models on top of alembic/env.py and set the target_metadata:

from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from sqlmodel import SQLModel # NEW

from alembic import context

from party_app.models import * # NEW


# ... existing code
target_metadata = SQLModel.metadata # UPDATED

# ... existing code

Import SQLModel into script.py.mako:

"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
import sqlmodel             # NEW

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
    ${upgrades if upgrades else "pass"}


def downgrade() -> None:
    ${downgrades if downgrades else "pass"}

script.py.mako serves as a template for each migration. Importing SQLModel allows us to use it in the migration scripts.

After running the first migration, compare the migration script with the Mako template file to understand how it works.

Everything is prepared in order for you to create the first migration:

(venv)$ alembic revision --autogenerate -m "Initial migration"

Inside the "alembic" directory, you should now see a "versions" directory. If you open it, you'll see that an initial migration Python file appeared.

Apply the migration:

(venv)$ alembic upgrade head

A database.db file should appear in your project.

Check if all the tables were created:

$ sqlite3
sqlite> .open database.db
sqlite> .tables
alembic_version  gift             guest            party

Your database is all set!

Pre-fill the Database

Since our application uses a lot of data -- parties, gifts, and guests -- I prepared JSON files with some initial data. Inside "party_app", create a new "initial_data" directory and add the following four files to it:

  1. load_initial_data_to_db.py
  2. initial_parties.json
  3. initial_gifts.json
  4. initial_guests.json

If you check load_initial_data_to_db.py, you'll see that it uses the three JSON files to fill the database. This is only to make your life easier by providing you with test data, and while it uses SQLModel, it's not directly related to the course material. Run the file to fill your db with gifts, guests, and parties.

(venv)$ python party_app/initial_data/load_initial_data_to_db.py

You might need to set your PYTHONPATH to the root of the project to successfully run the script from the terminal. Temporarily, you can set it with (provide your own path):

# macOS/Linux:
(venv)$ export PYTHONPATH=${PWD}:$PYTHONPATH

# Windows:
(venv)$ set PYTHONPATH=C:\me\Courses\party_app;%PYTHONPATH

Database Session Dependency

If you inspect load_initial_data_to_db.py, you'll see that the whole code that deals with a database is inside the with Session(engine) as session: block. While this is not a problem for a single script, there's a better way for a larger application. Instead of creating a session in each path operation in a with block, we can use FastAPIs dependency injection.

Update party_app/dependency.py like so:

# party_app/dependency.py

import os
from typing import Annotated

from fastapi import Depends
from fastapi.templating import Jinja2Templates
from sqlmodel import Session

from db import engine


_templates = Jinja2Templates(
    directory=os.path.join(os.path.dirname(__file__), "templates")
)


def _get_templates():
    return _templates


Templates = Annotated[Jinja2Templates, Depends(_get_templates)]


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

We'll use the get_session dependency in future chapters to easily access the database session without much boilerplate code.

Commit and push the changes you made:

(venv)$ git add -A
(venv)$ git commit -m 'FastAPI basic setup'
(venv)$ git push -u origin main

What Have You Done?

In this chapter, you've created a FastAPI project and set up the templating engine and your models. Using Alembic, you've created a database with the necessary tables and added some test data. With a basic template set, we can start adding HTMX, Tailwind CSS, and Alpine.js to your project, which we'll do in the next three chapters.

By the end of this chapter, you should have the following structure:

├── .gitignore
├── alembic.ini
├── database.db
├── db.py
├── requirements.txt
├── alembic
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions
│      └── 0f1b7b1b1b1a_initial_migration.py
├── party_app
│   ├── __init__.py
│   ├── dependency.py
│   ├── main.py
│   ├── models.py
│   ├── static
│   ├── initial_data
│   │   ├── initial_gifts.json
│   │   ├── initial_guests.json
│   │   ├── initial_parties.json
│   │   └── load_initial_data_to_db.py
│   └──  templates
│      └── base.html
└── venv



Mark as Completed