Introduction

The backend uses FastAPI. FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.8+ based on standard Python type hints

You can run the api only by running the pnpm run dev command from a terminal within the api directory. Alternatively, you can directly run the run.py file.

API structure

The API root directory is structured as follows:

api
├── src
│   ├── api
│   ├── crud
│   ├── schemas
│   ├── __init__.py
│   ├── config.py
│   └── main.py
├── tests
├── .env
├── .env.example
├── harry-potter-db-seed-spells.csv
├── harry-potter-db-seed-users.csv
├── package.json
├── poetry.lock
├── pyproject.toml
├── requirements.txt
├── run.py
└── vercel.json

Src directory

The src directory is where all the application code sits. Below briefly explains each folder/file

ItemDescription
apiThe API endpoints for the application
crudThe CRUD operations used within the application
schemasThe schemas used within the application
config.pyMain application configuration
main.pyApplication

Root directory

The root directory contains the typical files one would expect to see in a Python project. The below files are worth describing:

ItemDescription
package.jsonNot typical in a Python programme; used due to the nature of the monorepo. Running pnpm run dev at the project root will execute the dev script in this package.json
vercel.jsonThe configuration for deploying the FastAPI aspect of the application to Vercel.
*.csvSimple seed data for the database, sourced from the Harry Potter API

Dependencies

Python 3.9

Because the project is being deployed on Vercel, Python version 3.9 must be used as this is the latest supported version of Python. For more information, read the official documentation.

If deploying to somewhere other than Vercel, check which version of Python you may use and adjust the project according to your needs.

Supabase

The project uses Supabase as a database. Since Planetscale removed their free Hobby tier, Supabase has seemed like a good alternative to use. You do not have to use Supabase with the backend, but the project is written from the perspective of using it.

If you are using Supabase, supabase-py-async is already included as a project dependency within the pyproject.toml. If you are not using Supabase, this can be removed.

Poetry

Poetry is used to manage the virtual environment and project dependencies. A requirements.txt has been generated to enable the installation of Python packages via the pip install command.

If you do not use Poetry, you can remove the poetry.lock and pyproject.toml files.

You will need a requirements.txt when deploying. Vercel also accepts a Pipenv file if you use Pipenv, otherwise, you’ll need the requirements.txt for the api to build correctly

Adding your own endpoints

Given the project structure, there are three areas that you must be aware of when adding your own endpoints with new models. The main areas are:

  • Schemas
  • CRUD
  • API

Example: Creation of Spells endpoints

Step 1: Create a schema

1

Add a schema

Add a new schema to the schemas directory.

src/schemas/spell.py
from typing import ClassVar, Sequence

from pydantic import BaseModel


class Spell(BaseModel):
    id: str
    name: str
    description: str
    table_name: ClassVar[str] = "spells"


class SpellCreate(BaseModel):
    id: str
    name: str
    description: str


class SpellUpdate(BaseModel):
    id: str
    name: str
    description: str


class SpellSearchResults(BaseModel):
    results: Sequence[Spell]

The table_name value needs to be included in the base Spell class. This must be the same as the name of the table created in Supabase. It is used in the CRUD operations to identify the table to work with.
ItemDescription
SpellThe base class describing the columns that will be in the Supabase table
SpellCreateThe class for creating a new Spell
SpellUpdateThe class for updating an existing Spell
SpellSearchResultsDescribes how data will be returned in the API response. It will be a Sequence of Spell
2

Add to __init__.py

To easily import the new Spell classes throughout our application, they need to be added to the src/schemas/__init__.py file.

src/schemas/__init__.py
from .user import User, UserCreate, UserSearchResults, UserUpdate
from .spell import Spell, SpellCreate, SpellSearchResults, SpellUpdate

This allows us to import from src/schemas like so:

from src.schemas import Spell, SpellCreate, SpellSearchResults, SpellUpdate

Step 2: Setup CRUD operations

Specific CRUD operations can be created for each endpoint. However, generic CRUD operations in src/crud/base.py can also be used without modification.

1

Create CRUD file

src/crud/crud_spell.py
from fastapi import HTTPException
from supabase_py_async import AsyncClient

from src.crud.base import CRUDBase
from src.schemas import Spell, SpellCreate, SpellUpdate


class CRUDSpell(CRUDBase[Spell, SpellCreate, SpellUpdate]):
    async def get(self, db: AsyncClient, *, id: str) -> Spell | None:
        try:
            return await super().get(db, id=id)
        except Exception as e:
            raise HTTPException(
                status_code=404,
                detail=f"{e.code}: Spell not found. {e.details}",
            )

    async def get_all(self, db: AsyncClient) -> list[Spell]:
        try:
            return await super().get_all(db)
        except Exception as e:
            raise HTTPException(
                status_code=404,
                detail=f"An error occurred while fetching spells. {e}",
            )

    async def search_all(
        self, db: AsyncClient, *, field: str, search_value: str, max_results: int
    ) -> list[Spell]:
        try:
            return await super().search_all(
                db, field=field, search_value=search_value, max_results=max_results
            )
        except Exception as e:
            raise HTTPException(
                status_code=404,
                detail=f"An error occurred while searching for spells. {e}",
            )


spell = CRUDSpell(Spell)

The CRUDSpell class inherits from the CRUDBase class located in src/crud/base.py. The Spell, SpellCreate and SpellUpdate classes are passed to the CRUDBase class to specify the types of data that will be used in the CRUD operations.

The operations that will be used in the API endpoints are functions of the CRUDSpell class.

Finally, spell is an instance of CRUDSpell. We import this instance to use in the API endpoints like so: spell.get_all(db) or spell.get(db, id=id).

This part of the project is intended to be read-only, however, the SpellCreate and SpellUpdate classes are created in the schema regardless as they are required by the CRUDBase model’s function arguments. CRUDBase can be refactored to have optional parameters, or a read-only version of the CRUDBase class can be created. This is beyond the scope of this project.

2

Add to __init__.py

Similar to setting up the schemas, the CRUDSpell class needs to be added to the src/crud/__init__.py file.

src/crud/__init__.py
from .crud_user import user
from .crud_spell import spell

Step 3: Create the API endpoints

1

Create the endpoints

Create the endpoints in the src/api/api_v1/endpoints/spells.py file.

src/api/api_v1/endpoints/spells.py
from typing import Literal, Optional, Union

from fastapi import APIRouter, HTTPException

from src.api.deps import SessionDep
from src.crud import spell
from src.schemas import Spell, SpellSearchResults

router = APIRouter()


@router.get("/get/", status_code=200, response_model=Spell)
async def get_spell(session: SessionDep, spell_id: str) -> Spell:
    """Returns a spell from a spell_id.

    **Returns:**
    - spell: spell object.
    """
    return await spell.get(session, id=spell_id)


@router.get("/get-all/", status_code=200, response_model=list[Spell])
async def get_all_spells(session: SessionDep) -> list[Spell]:
    """Returns a list of all spells.

    **Returns:**
    - list[spell]: List of all spells.
    """
    return await spell.get_all(session)


@router.get("/search/", status_code=200, response_model=SpellSearchResults)
async def search_spells(
    session: SessionDep,
    search_on: Literal["spells", "description"] = "spell",
    keyword: Optional[Union[str, int]] = None,
    max_results: Optional[int] = 10,
) -> SpellSearchResults:
    """
    Search for spells based on a keyword and return the top `max_results` spells.

    **Args:**
    - keyword (str, optional): The keyword to search for. Defaults to None.
    - max_results (int, optional): The maximum number of search results to return. Defaults to 10.
    - search_on (str, optional): The field to perform the search on. Defaults to "email".

    **Returns:**
    - SpellSearchResults: Object containing a list of the top `max_results` spells that match the keyword.
    """
    if not keyword:
        results = await spell.get_all(session)
        return SpellSearchResults(results=results)

    results = await spell.search_all(
        session, field=search_on, search_value=keyword, max_results=max_results
    )

    if not results:
        raise HTTPException(
            status_code=404, detail="No spells found matching the search criteria"
        )

    return SpellSearchResults(results=results)

Here we are calling spell object which we setup in step 2.

The SessionDep (located in src/api/deps.py) dependency is used to access the database.

The response_model parameter is used to specify the type of data that will be returned from the endpoint. This is used to generate TypeScript types for the frontend.

2

Add the endpoint to the router

To include the new endpoints in the API, it must be added to the API router, located in src/api/api_v1/api.py.

src/api/api_v1/api.py
from fastapi import APIRouter
from src.api.api_v1.endpoints import users, spells

api_router = APIRouter()
api_router.include_router(users.router, prefix="/users", tags=["users"], responses={404: {"description": "Not found"}})
api_router.include_router(spells.router, prefix="/spells", tags=["spells"], responses={404: {"description": "Not found"}})

Step 4: Create the table in Supabase

1

Create the table

Create a new table in your Supabase dashboard. It is imperative that the table name matches the table_name value in the Spell class in src/schemas/spell.py.

Columns should be added to match the Spell class. For example, we have the following Spell class:

class Spell(BaseModel):
    id: str
    name: str
    description: str
    table_name: ClassVar[str] = "spells"

In this example, the table should be named spells (table names are case-sensitive) with the columns id, name, and description.

For data types, id can be automatically assigned - in this example, it is set to a uuid. The name and description fields are strings, therefore their column’s type is set to text. The id column should be the primary key.

You do not need to add the table_name column as this is used in the FastAPI code to identify the table to work with.

2

Seed the table with data

The data can be seeded using the Supabase dashboard by uploading the harry-potter-db-seed-spells.csv file. For a more detailed explanation, please see the official documentation

3

Configure table security

The table security should be configured to allow the FastAPI application to access the data. By default, Supabase will have RLS (Row Level Security) enabled.

If left un-configured, the database will return an empty array. As this is a simple read-only project, I am turning RLS off. However, for true CRUD operations, it should be configured to use authentication which is beyond the scope of this project.


To disable RLS, in the table settings, select configure RLS and then disable RLS.\

4

Add table connection to .env

Once the table has been created, you must ensure that the DB_URL and DB_API_KEY parameters are populated in your .env file located in the root of the API directory.

These values come from the Supabase dashboard by going to settings and then API. Copy the Project URL (DB_URL) and the Project API Key (DB_API_KEY).

DB_USERNAME and DB_PASSWORD should also included in the .env as they are configured in the src/config.py. If you do not include these you will receive an error from Pydantic.

Their inclusion is a pre-cursor to authentication, but they are not actually used in this project scaffold.

Step 5: Test your new endpoints

You can now test your new endpoints using the FastAPI Swagger UI or by making requests to the API.