The whole setup is available as a template on GitHub
When I first started using FastAPI, I found it very challenging to structure my project properly. Writing everything in one or two files is not exactly ideal when it comes to bigger projects.
So the basic requirement was to create an easy-to-follow project structure that integrates SQLAlchemy and Unit Tests nicely. Furthermore, I wanted to achieve a clear separation of concerns by using different modules.
Basic folder structure
We structure our project into two modules: src and test:
.
└── backend/
├── src/
│ ├── config/
│ ├── routes/
│ ├── util/
│ └── __init__.py
├── test/
│ ├── routes/
│ ├── test_util/
│ ├── __init.py__
│ ├── app_test.py
│ └── conftest.py
├── main.py
└── requirements.txt
Basic FastAPI
The application start is the main.py
. In this file, we can define our FastAPI server as expected:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse
app = FastAPI(
title=APP_NAME,
version=VERSION
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True
)
# Redirect / -> Swagger-UI documentation
@app.get("/")
def main_function():
"""
# Redirect
to documentation (`/docs/`).
"""
return RedirectResponse(url="/docs/")
Some general app parameters are defined in the config.py
located in /config
. Those values could also be environment variables later in deployment.
APP_NAME = "My fancy app"
VERSION = "v1.0.0"
Routes
When it comes to the individual routes, I took inspiration from SvelteKit. They use file-based routing, which makes searching for a certain piece of code pretty straightforward — you just have to traverse the file structure the same way the URL is structured. This seems like a good idea for APIs, too, so I went with this approach, despite having the downside of creating files with the same names over and over again.
Let's say we build a very basic API for booking meeting rooms. We would probably face the following endpoints:
/rooms GET
POST
DELETE
PUT
/bookings GET
POST
DELETE
PUT
/users GET
POST
DELETE
PUT
As well as the following database tables:
rooms bookings users
========= ============= =====
id id id
name booker_id name
capacity starting_time mail
building ending_time
room_id
In this example, we would create three folders inside the routes
-directory:
.
└── backend/
└── src/
└── routes/
├── users
├── bookings
├── rooms
└── __init.py__
Additionally, I like to keep the authentication-logic on its own route.
Inside each of these folders, we would have three files:
-
main.py
: Place for the actual API-Endpoint-Function, could look like this:
@router.get("/") async def get_all_users(user: User = Depends(get_current_active_user), db: Session = Depends(get_db)): """ # Get a list of all users **Access:** - Admins get a list of all users. - Users with lower rights get a list with only the enabled users. """ if user.super_admin: return get_users_admin(db=db) else: return get_users(db=db)
-
controller.py
: Handles your logic
def get_user_by_id(user_id: int, db: Session): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException( status_code=404, detail="There is no user with this id." ) return user
schemas.py
: Home to Pydantic schemas relevant for this route
To use those routes, we have to create a router for each route in the corresponding {route}/main.py
:
router = APIRouter(
prefix="/users",
tags=["Users"],
responses={404: {"description": "Not found"}},
)
We then import those routers into the applications main.py
:
from src.routes import bookings, users
# ---- Do this for all of your routes ----
app.include_router(users.main.router)
app.include_router(bookings.main.router)
# ----------------------------------------
Database integration
With the above steps done, we have successfully organized our FastAPI server into several folders. However, access to a database is still not possible. We will use SQLAlchemy as an ORM.
Following the FastAPI-docs, we first need to specify our connection parameters inside src/config/database.py
:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
db_username = "user"
db_password = "password123"
db_url = "127.0.0.1:3306"
db_name = "example"
connectionString = f'mariadb+pymysql://{db_username}:{db_password}@{db_url}/{db_name}'
# use echo=True for debugging
engine = create_engine(connectionString, echo=False)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
Depending on which database you use, you may need to modify the connection string and install a different pip-package. For MariaDB, the relevant ones are mariadb
, pymysql
and SQLAlchemy==1.4.36
.
Keep in mind, that SQLAlchemy >= v2.0 introduced serious changes, so you will need to modify the template, if you wish to use the new version!
For interacting with the database from an endpoint called, we will use dependency injection. We define our dependency in src/util/db_dependency.py
:
from src.config.database import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
We can now create db-model-files for every route, e.g. src/routes/users/models.py
:
from src.config.database import Base
from sqlalchemy import Column, String, Integer
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(length=50))
mail = Column(String(length=100))
Lastly, we import every model into our global main.py
and create the specified tables from there:
from src.config.database import engine
from src.routes.users import main, models
from src.routes.auth import main, models
users.models.Base.metadata.create_all(bind=engine)
auth.models.Base.metadata.create_all(bind=engine)
Unit tests
For Unit Tests, /test
mirrors the folder structure of /src
. The tests are organized in the same routing-structure as before.
This article explained the basic idea behind the project structure. You can find a complete template on GitHub. It also includes authentication, an email template (e.g. notifying the user about security issues or sending password reset links) and the full test configuration.
Even though this structure has worked well for me in some projects, there may be even better organized approaches out there. So, please let me know in the comments: what are your opinions on this project structure & how do you approach FastAPI projects?
Top comments (0)