Introducing Our Clean and Modular FastAPI Reference Architecture
Adam Smith
September 20, 2024
•
8 min read
At Beta Acid, we believe in building software like a startup—fast, efficient, and adaptable. Today, we're excited to open-source a reference API app that showcases our clean, modular FastAPI architecture designed for both agility and robustness. FastAPI is a popular Python web framework for building APIs quickly.
Building Fast Without Compromise
Speed is a key factor in software development, offering a distinct advantage by bringing products to market faster. But it’s just as important to balance this with strong code quality and maintainability. When collaborating with larger, more established businesses, the emphasis shifts toward rigorous testing and ensuring scalability to support an expanding user base. Our FastAPI reference app strikes a balance between rapid development and long-term reliability. It enables developers to create applications quickly while maintaining a clean codebase that's easy to test, scale, and modify with confidence.
What's Inside?
Our reference app is a Star Wars character API that allows users to enter a character name, fetch details from the SWAPI API, and store them in a database. While the app's functionality is straightforward, it embodies architectural best practices suitable for projects of any size.
Key Features:
- Clean, Modular Structure: Each component of the application is designed with a clear separation of concerns, making the codebase intuitive and maintainable.
- Robust Testing Strategy: High test coverage is achieved through a well-defined testing pyramid, ensuring each module works correctly both in isolation and together.
- Scalability: The architecture is designed to be scalable, allowing for easy expansion as project requirements grow.
- Ease of Maintenance: With clear naming conventions and modular components, developers can confidently make changes without fearing unintended side effects.
The Architecture Breakdown
Main File
The main.py
file serves as the application's entry point. It initializes the FastAPI app and includes the necessary routers.
from fastapi import FastAPI
from app.routers import characters_router
app = FastAPI()
app.include_router(characters_router)
Routers
Routers define the API endpoints and manage request validation, keeping the logic clean by delegating core business operations to the service layer. This helps ensure the routers stay focused on routing and error handling, without becoming overloaded with business logic.
@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)
Service Layer
The service layer acts as the core coordinator of business logic. It processes input data, applies business rules, and orchestrates the flow between different components. External API calls and database interactions are delegated to the clients, keeping the service layer decoupled from external dependencies and enhancing testability.
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)
Clients
Clients handle interactions with external services and databases. They encapsulate the logic for making API calls or database operations, providing a clean interface for the service layer to use. This isolation enhances modularity and makes the codebase easier to test and maintain.
For example, the client for interacting with the SWAPI API:
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()
Similarly, database interactions are managed by database clients or repositories:
def insert_new_character(db: Session, character: StarWarsCharacter) -> StarWarsCharacterRead:
db_character = StarWarsCharacterModel.from_pydantic(character)
db.add(db_character)
db.commit()
db.refresh(db_character)
return db_character.to_pydantic()
Domain Logic
Domain logic contains the core business rules and calculations, ensuring that the application's fundamental operations are consistent and reliable. By centralizing business logic, we make it reusable and easier to manage.
def convert_consumables_to_days(consumables: str) -> int:
Utils
Utility functions provide reusable, stateless operations that support various parts of the application. These are common functions that don't fit neatly into other categories but are essential for code reusability.
def format_star_wars_name(name: str) -> str:
Benefits of This Architecture
Confidence in Code Changes
With a robust testing strategy and clear separation of concerns, developers can modify and extend the codebase with confidence. Changes are validated at multiple levels, reducing the risk of introducing bugs.
Scalability
The modular design and testing strategy make scaling the application straightforward. As new features are added, existing tests provide assurance that current functionality remains unaffected.
Maintainability
Clear naming conventions, a well-organized directory structure, and a comprehensive test suite make the codebase easy to navigate and maintain. This reduces onboarding time for new developers and simplifies ongoing maintenance.
Testability
By isolating external dependencies and focusing on testing public methods, the architecture enables comprehensive and efficient testing. This ensures each component functions correctly both individually and as part of the whole system.
Flexibility
The architecture is adaptable to different project needs, whether it's rapid prototyping or building enterprise-level applications. The principles applied here can be extended or customized to fit various scenarios.
Our Testing Pyramid Approach
A key aspect of our architecture is the emphasis on a testing pyramid approach. This strategy ensures a solid foundation of high-quality, reliable code, allowing us to move quickly without sacrificing stability.
Unit Tests at the Base
At the base of the pyramid are unit tests, which provide the bulk of our test coverage. These tests are:
- High coverage: We write comprehensive unit tests for 80%+ of public methods, ensuring that each component works correctly in isolation.
- Fast and Reliable: By mocking external dependencies, unit tests run quickly and provide immediate feedback.
- Focused: Each test targets a small piece of functionality, making it easier to pinpoint issues.
Integration Tests in the Middle
Above the unit tests are integration tests, which are fewer in number but cover critical pathways. For an API like this, our integration tests:
- Hit the Actual API Endpoints: We test the application by making real HTTP requests to our API, simulating client behavior.
- Use Real External Services and Databases: These tests interact with actual external services like the SWAPI API and use the database to verify that all components work together as expected.
- Verify End-to-End Functionality: They ensure that data flows correctly through the system, from the API request to the database and back.
In most real-world situations, we take integration testing a step further.
Front-End Integration Testing
When our applications include a front end, we employ tools like Playwright for end-to-end testing. This allows us to:
- Test the Front End and Back End Together: By automating browser interactions, we verify that the user interface works seamlessly with the live API.
- Uncover Integration Issues: These tests can catch problems that unit or back-end integration tests might miss, such as mismatches between API responses and front-end expectations.
Our QA Team stays at the top
By investing heavily in unit tests and strategic integration tests, we free up our QA team to focus on:
- Quick feedback on in-sprint work: QA tests new features as they are finished by the development team.
- Writing New Integration Tests: After the initial testing, they can add to the front end test scripts
- Exploratory Testing: With confidence in the automated test coverage, QA can spend the rest of their time exploring edge cases and performance issues.
This approach not only improves code quality but also accelerates the development cycle, as issues are caught early and can be addressed promptly.
Conclusion
At Beta Acid, we're committed to combining speed with quality. Our FastAPI reference architecture and testing pyramid approach embody this commitment, providing a foundation that is both agile and robust. By focusing on high unit test coverage, strategic integration tests, and comprehensive front-end testing with tools like Playwright, we ensure that our codebase is reliable and our QA team is empowered to enhance the product further. We invite you to explore the codebase, utilize it in your projects, and contribute to its growth. Together, we can build software that's both fast and dependable.
Explore more of our projects and insights on our blog. If you have questions or are interested in collaborating, feel free to reach out—we'd love to hear from you.
SHARE THIS STORY
Get in touch
Whether your plans are big or small, together, we'll get it done.
Let's get a conversation going. Shoot an email over to projects@betaacid.co, or do things the old fashioned way and fill out the handy dandy form below.