Scalabel FastAPI project (Layered Architecture)

Introduction

Khalil Saidane
4 min readOct 1, 2023

If you started using FastAPI recently and you are enjoying its high performance and its compatibility with OpenAPI (previously known as Swagger) but still missing some of the features that other frameworks like Django provides like the integrated ORM, database migrations management, a simple command to start a project as well as an application. this article is trying to solve this by proposing a project and application template that are ready to use.

Key features of using this project and app template

  1. A project structure inspired from real world example and Django.
  2. SQLAlchemy as the ORM integrated.
  3. Alembic for database migrations management.
  4. Poetry for dependency management.
  5. Pytest with a fixture for test database.
  6. Generic CRUD class for all models.
  7. A mechanism for injecting repositories and services.
  8. Layered architecture implementation

How to use it?

Requirements: Python 3.6+, cookiecutter, and poetry.

  1. Initiate the project.
$ cookiecutter https://github.com/khalilSaidane/fastapi-project-template.git

This will generate the project structure interactively with some default settings, in this case I just changed the project slug.

project_slug [my_fastapi_project]: myfirst_project
project_name [My FastApi App]:
database_url [sqlite:///./sql_app.db]:
allowed_hosts [127.0.0.1, localhost]:
secret_key [A secret key]:
prefix []:
version [0.1.0]:
description []:
FastAPI Project

This will be our project folder that contains:

  • myfirst_project: A Package for a FastAPI project
  • db: DB related stuff
  • migrations: Alembic migrations
  • base.py: import all modes here, so that Base has them before being imported by Alembic
  • database.py: Where we instantiate SQLAlchemy declarative_base
  • errors.py: DB Exceptions
  • utils: Utils functions
  • dependencies: Dependencies for route definitions
  • dependencies: Dependencies for route definitions
  • resources: Strings that are used in web responses
  • tests: Pytest tests
  • api.py: Where we include other API’s routers in the project router
  • main.py: FastApi application creation and configuration
  • settings.py: A flat setting file

2. Create virtual environment

Create virtual environment and install requirements.

$ cd myfirst_project
$ python3 -m venv venv
$ source venv/bin/activate
$ source $HOME/.poetry/env
$ poetry install

3. Create an application

$ cookiecutter https://github.com/khalilSaidane/fastapi-application-template.git

This is will generate the application template interactively, you just provide the app name.

app_name [my_fastapi_app]: users
FastAPI application
  • users: A Package for a FastAPI application.
  • models.py: Mapped models
  • reposiories.py: Where we declare our Repository classes that will query the db
  • schemas.py: Pydantic schemas for data validation in web routes
  • services.py: Where we declare our Service

4. Create user model and run migrations

from sqlalchemy import Boolean, Column, Integer, String
from myfirst_project.db.base import Base

class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
password = Column(String)
is_active = Column(Boolean, default=True)

Import the model in my-project/db/migrations/env.py
just after line 31 where we import Base

# Import your models so alembic will detect changes 
from users.models import User

Next step is to run Alembic migrations.

$ cd myfirst_project
$ alembic revision -m "Creating the user model" --autogenerate
$ alembic upgrade head

5. Create user service and user repository

myfirst_project/users/reposiories.py

The session in the Repository class will be injected automatically, the only requirement is to always inherit from BaseRepository a class provided by the project template. You can check also CRUDBRepository here.

from myfirst_project.utils.crud_repository import CRUDBRepository
from users.models import User


class UserRepository(CRUDBRepository):
model = User

def search_user_by_email(self, email: str):
return self.session.query(User) \
.filter(User.email.like(email)) \
.filter(User.is_active.is_(True)) \
.all()

myfirst_project/users/services.py

The Repository will be injected through constructor, don’t forget to add the type hint and inherit from BaseService. Based on this information the dependency injection mechanism will inject the desired repository.

from myfirst_project.utils.services import BaseService
from users.repositories import UserRepository


class UserService(BaseService):

def __init__(self, user_repo: UserRepository):
self.user_repo = user_repo

def add_user(self, user):
return self.user_repo.create(user)

def get_user(self, user_id):
return self.user_repo.get(user_id)

def get_all_users(self):
return self.user_repo.get_multi()

def search_user_by_email(self, email: str):
return self.user_repo.search_user_by_email(email)

6. Create the schema and web router

from pydantic import BaseModel


class UserBase(BaseModel):
email: str


class UserCreate(UserBase):
password: str


class User(UserBase):
id: int

class Config:
orm_mode = True

Now let’s add the web routers we will use class based views a feature provided by fastapi-utilities library.

Notice there is no instantiation of the service, it will be injected using: Depends(get_service(UserService))

from typing import List
from fastapi import APIRouter, Depends
from fastapi_utils.cbv import cbv
from myfirst_project.dependencies.service import get_service
from users.schmeas import UserCreate, User
from users.services import UserService

users_router = APIRouter()


@cbv(users_router)
class UserView:
user_service: UserService = Depends(get_service(UserService))

@users_router.post("/", response_model=User)
def create_user(self, user: UserCreate):
return self.user_service.add_user(user)

@users_router.get("/", response_model=List[User])
def get_all_users(self):
return self.user_service.get_all_users()

@users_router.get("/{email}", response_model=List[User])
def search_user_by_email(self, email: str):
return self.user_service.search_user_by_email(email)

Finally we just need to add the users_router to the project router in myfirst_project/api.py.

from fastapi import APIRouter
from users.routers import users_router

api_router = APIRouter()
api_router.include_router(users_router, tags=["Users"], prefix="/users")

7. Run the project

$ cd myfirst_project
$ uvicorn main:app --reload

8. Testing

Go to my first_project/tests and create a new file test_users.py

def test_create_user(client):
response = client.post(
"/users/",
json={
"email": "email@gmail.com",
"password": "string"
}
)
assert response.status_code == 200
assert response.json() == {
"email": "email@gmail.com",
"id": 1,
"is_active": True
}

Then run pytest tests

$ cd myfirst_project
$ poetry run pytest -vv

--

--