This project is a reference architecture for building FastAPI applications at Beta Acid. It demonstrates a clean, maintainable structure for a FastAPI app where users can enter a character name, which triggers a call to the SWAPI API to fetch details about the character and store them in a database. Accompanying blog post.
The application allows users to enter the name of a Star Wars character, which triggers the following steps:
- A call is made to the SWAPI API to fetch the character's data.
- The character's name is formatted.
- The character data is stored in a database.
This project serves as a reference architecture to demonstrate best practices for organizing FastAPI projects, focusing on separation of concerns, testing strategies, and modularization. This project focuses on application architecture and does not cover CI/CD, deployment, Docker, or other operational best practices.
Note that type safety should be used whenever possible including parameters and return types.
The goal is to achieve high test coverage with mocked dependencies at every layer, ensuring that each component works in isolation.
- Each layer of the architecture (clients, services, routers, etc.) is tested individually.
- All dependencies, including external services like SWAPI and database calls, are mocked in unit tests.
- We only test public methods to avoid coupling tests to internal implementation details,
- No external service calls are made during unit tests.
- Integration testing will be handled by the front end's end-to-end testing.
- In the case where we can not perform front end tests, use a few integration tests to test the entire flow with real API calls and database interactions.
- These tests are minimal and only serve as sanity checks to ensure the app functions as expected in a real-world scenario.
- Integration tests ensure that the different layers of the architecture work together correctly.
To ensure that the file names clearly describe their purpose, we follow a naming convention that reflects the functionality of each file. This helps maintain clarity, especially in larger projects. Here's how we structure the file names:
Services: These files coordinate the logic and interactions between external systems, databases, and internal business logic. We name them based on the service they provide. For example, instead of service/character.py
, we would use service/character_service.py
, making it clear that this file handles business logic related to Star Wars characters.
Routers: These files define the API endpoints. We include "router" in the file name to clarify that this file handles route definitions. Instead of router/character.py
, we would name it router/character_router.py
.
The application is designed to be modular, with clear separation of concerns. Here's a breakdown of the main components:
The main.py
file serves as the entry point for the application. It initializes the FastAPI app, makes any app-wide configurations, and includes the necessary routers:
from fastapi import FastAPI
from app.routers import characters_router
app = FastAPI()
app.include_router(characters_router)
The routers are responsible for defining the API routes. They remain very clean and only handles request validation and forwarding the call to the service layer:
@characters_router.post("/", response_model=StarWarsCharacterRead)
async def create_character(
input_character: StarWarsCharacterCreate, db: Session = Depends(get_db_session)
) -> StarWarsCharacterRead:
return add_new_character(input_character, db)
The service layer acts as the main coordinator of business logic. It handles calls to external services (SWAPI) and the database, formats the character name, and ensures data is processed correctly:
def add_new_character(input_character: StarWarsCharacterCreate, db: Session) -> StarWarsCharacterRead:
swapi_json = get_character_from_swapi(input_character.name)
swapi_character = transform_swapi_character_json_to_pydantic(swapi_json)
formatted_name = format_star_wars_name(swapi_character.name)
swapi_character.name = formatted_name
return insert_new_character(db, swapi_character)
External dependencies such as SWAPI API calls and database operations are handled in the clients
directory. Each client is responsible for interacting with a specific external system, ensuring clean separation of concerns.
For example, the swapi_networking_client.py
handles SWAPI API interactions:
def get_character_from_swapi(name: str) -> dict:
response = requests.get(f"https://swapi.dev/api/people/?search={name}")
response.raise_for_status()
return response.json()
The domain logic is responsible for handling business rules and calculations. The domain logic ensures that the core business rules are applied consistently across the application.
For example, the vehicle_calculations.py
file handles calculations related to vehicle efficiency:
def convert_consumables_to_days(consumables: str) -> int:
Utility functions are used for common tasks that are shared across different parts of the application. These functions are generally stateless and reusable.
For example, the characters_utils.py
file contains a utility function for formatting Star Wars character names:
def format_star_wars_name(name: str) -> str:
To run the project locally, follow these steps:
-
Clone the repository:
-
Create and activate a virtual environment:
-
On macOS/Linux:
python -m venv .venv source .venv/bin/activate
-
On Windows:
python -m venv .venv .venv\Scripts\activate
-
-
Install dependencies:
pip install -r requirements.txt
-
Set up the environment variables:
- Copy the
example.env
file and rename it to.env
. - Edit the
.env
file to include your PostgreSQL connection information:
DATABASE_URL=postgresql://<username>@localhost/star_wars
- Copy the
-
Set up the database (using Alembic for migrations):
alembic upgrade head
-
Start the FastAPI application:
uvicorn main:app --reload
You can now visit http://127.0.0.1:8000/docs
to interact with the API through the automatically generated Swagger UI.
To run all tests, use the following command:
pytest
To run only the unit tests:
pytest tests/unit_tests/
To run the integration tests:
pytest tests/integration_tests/