--- title: FastAPI config "og:title": "FastAPI configuration and setup" description: FastAPI configuration and setup --- ## Introduction The backend uses [FastAPI](https://fastapi.tiangolo.com/). 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 | Item | Description | | --------- | ----------------------------------------------- | | api | The API endpoints for the application | | crud | The CRUD operations used within the application | | schemas | The schemas used within the application | | config.py | Main application configuration | | main.py | Application | ### Root directory The root directory contains the typical files one would expect to see in a Python project. The below files are worth describing: | Item | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | package.json | Not 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.json | The configuration for deploying the FastAPI aspect of the application to Vercel. | | \*.csv | Simple seed data for the database, sourced from the [Harry Potter API](https://hp-api.onrender.com/) | ## 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](https://vercel.com/docs/functions/runtimes/python). 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](https://supabase.com/) as a database. Since Planetscale [removed their free Hobby tier](https://planetscale.com/blog/planetscale-forever), 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`](https://pypi.org/project/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](https://python-poetry.org/) 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 Add a new schema to the `schemas` directory. ```python 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. | Item | Description | |--------------------|---------------------------------------------------------------------------------------------| | `Spell` | The base class describing the columns that will be in the Supabase table | | SpellCreate | The class for creating a new `Spell` | | SpellUpdate | The class for updating an existing `Spell` | | SpellSearchResults | Describes how data will be returned in the API response. It will be a `Sequence` of `Spell` | To easily import the new `Spell` classes throughout our application, they need to be added to the `src/schemas/__init__.py` file. ```python 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: ```python 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. ```python 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. Similar to setting up the schemas, the `CRUDSpell` class needs to be added to the `src/crud/__init__.py` file. ```python src/crud/__init__.py from .crud_user import user from .crud_spell import spell ``` #### Step 3: Create the API endpoints Create the endpoints in the `src/api/api_v1/endpoints/spells.py` file. ```python 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. To include the new endpoints in the API, it must be added to the API router, located in `src/api/api_v1/api.py`. ```python 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 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: ```python 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. 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](https://supabase.com/docs/guides/database/import-data#option-1-csv-import-via-supabase-dashboard) The table security should be configured to allow the FastAPI application to access the data. By default, Supabase will have [RLS (Row Level Security)](https://supabase.com/docs/guides/auth/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`.\ 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.