Optimizing your AWS Serverless Costs
- Infrastructure & Architecture
Adam Smith Adam Smith
September 19, 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.
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.
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.
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 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)
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 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 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:
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:
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.
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.
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.
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.
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.
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.
At the base of the pyramid are unit tests, which provide the bulk of our test coverage. These tests are:
Above the unit tests are integration tests, which are fewer in number but cover critical pathways. For an API like this, our integration tests:
In most real-world situations, we take integration testing a step further.
When our applications include a front end, we employ tools like Playwright for end-to-end testing. This allows us to:
By investing heavily in unit tests and strategic integration tests, we free up our QA team to focus on:
This approach not only improves code quality but also accelerates the development cycle, as issues are caught early and can be addressed promptly.
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.