Welcome to Part 1 of the 3-part FastAPI Series π¦ - View Full Code on GitHub
We are building a scalable, production-ready FastAPI codebase from the ground up. Along the way, we'll cover project structure, environment setup, dependency injection, and async I/O-bound API calls.
Whether you're working on monoliths, microservices, or event-driven systems, this guide is about laying a clean, modular, and solid foundation, one thatβs been tested in real-world environments, not just spun up for a quick demo.
Table of Contents
- π§ A Bit of Background
- π― What to Expect
- ποΈ Project Structure Overview
- ποΈ Project Initialization
- π Request Lifecycle
- π§ͺ Testing the Gears
- π Conclusion
π§ A Bit of Background
Like many developers, I started with Python. I was eager to learn (still am), and dove into every tutorial I could find. It didnβt take long before I hit Tutπ₯rial Hell, you know that endless loop where every new video feels like I've finally found the chπsen one. Spoiler alert: it never was...
But that hell did prepare me well enough to land an internship. And to my surprise, I realized I was actually light years ahead of my peers.
That internship was a deep dive into the real world: a full-scale, SaaS, enterprise-grade KPI management system built with Django, serving over 100K users (with more being onboarded regularly). It was the first time I saw Python used at scale in a production environment and it completely changed how I thought about architecture and design.
Since then, Iβve worked on Python applications across different industries, helping teams scale their systems (and occasionally refactoring parts of legacy code).
Eventually, I started hearing about the latest kid on the block: FastAPI, the "speedster" of the Python ecosystem. I checked it out, watched a bunch of tutorials... and quickly noticed a pattern: a lot of them were just theory. Surface-level. Book-knowledge.
This series is my attempt to share what Iβve learned over the years about Pythonβs real capabilities in production.
π― What to Expect
This series is practical. Itβs grounded in real-world experience and designed with β€οΈ by yours truly, the host of this series, Mr. Chikeπ¨πΎβπ» to show you how to build systems that are modular, maintainable, and scalable from day one.
So enough talk.
Letβs get to work. π₯
ποΈ Project Structure Overview
media_app/
base/ # Feature module
βββ __init__.py # Python package initialization
βββ router.py # Defines HTTP API endpoints and maps them to controller functions
βββ controller.py # Handles request-response cycle; delegates business logic to services
βββ service.py # Core business logic for async I/O operations
βββ model.py # SQLAlchemy ORM models representing database tables
βββ schema.py # Pydantic models for input validation and output serialization
βββ dependencies.py # Module-specific DI components like authentication and DB sessions
βββ tasks.py # Core business logic for CPU-bound operations
βββ movies/ # Movie feature module
βββ tv_series/ # TV series feature module
βββ static/ # (Optional) Static files (e.g., images, CSS)
βββ templates/ # (Optional) Jinja2 or HTML templates for frontend rendering
βββ docs/ # (Optional) API documentation, design specs, or OpenAPI enhancements
βββ shared/ # Project-wide shared codebase
β βββ __init__.py
β βββ config/ # Environment configuration setup
β β βββ __init__.py
β β βββ settings.py # Pydantic-based config management
β βββ dependencies/ # Shared DI functions (e.g., auth, DB session)
β βββ middleware/ # Global middlewares (e.g., logging, error handling)
β βββ services/ # Reusable services
β β βββ __init__.py
β β βββ external_apis/ # Third-party integrations (e.g., TMDB, IMDB)
β β βββ internal_operations/ # CPU-intensive logic, background tasks
β βββ utils/ # Generic helpers (e.g., slugify, formatters)
βββ scripts/ # Developer or DevOps utilities
β βββ __init__.py
β βββ sanity_check.py # A friendly reminder not to lose your mind while debugging
βββ tests/ # # Unit, Integration, System, and End-to-End (E2E) tests for app modules
β βββ __init__.py
βββ .example.env # Template for environment variables (e.g., DB_URL, API_KEY)
βββ .coveragerc # Code coverage settings
βββ .gitignore # Files and folders ignored by Git
βββ main.py # FastAPI application entrypoint
βββ pytest.ini # Pytest configuration
βββ requirements.txt # Python dependency list
βββ JOURNAL.md # Development log: issues faced, solutions, and resources
βββ README.md # Project overview, setup, and usage
ποΈ Project Initialization
Weβll start by setting up the project folder and running an initialization script to scaffold the directories and files weβll use throughout the article.
You can find the full setup.sh script in the GitHub repository.
# Create the main project folder and initialize Git
mkdir media_app && cd media_app && git init
# Add and make the setup script executable
touch setup.sh && chmod +x setup.sh
# Update 'setup.sh' with your GitHub repository link, then run the setup script to scaffold the project
./setup.sh
# Activate your Python virtual environment (if applicable)
source env/bin/activate
Now update main.py
with the content from this file, then start the project by running:
uvicorn main:app --reload --port 8000
This will launch the app with live reload enabled on port 8000. (Feel free to change the port to whatever your majesty desires.)
Now we create the module we want to work on from base
$ cp -r base movies
I know firsthand how frustrating it can be to read a technical article. it's almost like the developer assumes you're inside their head and completely understand all the complexities.
That's exactly why I'm taking my time to explain things clearly and step by step.
Now, letβs move on to the next phase...
π Request Lifecycle
The router receives an HTTP request from the user and forwards it to a controller.
The controller validates the request data often using schemas for input validation and output serialization, then calls the appropriate service or tasks function.
The service handles the core business logic for async I/O operations, such as CRUD operations on the database (using
model.py
) or interactions with external APIs.When needed, the service also uses components from
dependencies.py
, such as authentication or database session injection, to get the job done.For CPU-bound or long-running operations, the service delegates the work to
tasks.py
. These tasks are executed asynchronously using Celery, with a broker like Redis or RabbitMQ, to keep the user experience smooth. (Covered in Part 2 of this series)
π No one loves waiting, so the heavy lifting is done in the background while the user gets a quick response and moves on with their day.
- Once processing is complete, the service returns the result to the controller, which formats the response using schemas, and sends it back through the router to the client.
Now that we understand the overall process, letβs break down what it actually looks like in motion.
When a user hits the movie endpoint, hereβs what's going on in the background:
App Entry (main.py):
The FastAPI app initializes and mounts the movie_router under /movies on Line 13. This means all movie-related routes start with /movies.
Configuration Layer (shared/config/settings.py):
This layer manages the application's settings and environment variables using Pydantic for validation and dotenv for loading configuration from a .env file. The AppSettings class defines various configuration fields such as database credentials and others.
Router Layer (movies/router.py):
The router defines specific HTTP routes and forwards incoming requests to the controller. For example, a POST request to /movies/omdb/ hits the routerβs get_movie method on Line 11.
Controller Layer (movies/controller.py):
The controller acts as a middleman by receiving the validated request data, calls the service layer to handle business logic, and formats the response to send back to the client as seen from Line 10.
Schema Layer (movies/schemas.py):
This layer defines the data structure and validation rules using Pydantic models. For incoming requests, MovieSchema ensures required and optional fields (like title, actors, and year) are properly validated. For responses, MovieResponseSchema extends MovieSchema to allow automatic population from ORM models, as seen from Line 10 of the controller.
Service Layer (movies/service.py):
This layer constructs the external API URL and delegates the task of fetching data to the external API service. It handles errors gracefully, logging issues for developers and returning user-friendly messages if something goes wrong as seen from Line 19.
External API Call (shared/services/external_apis/omdb_movies.py):
The actual HTTP request to the OMDB API is made here using httpx. This function focuses on making reliable API calls and raising exceptions on errors, which the service layer catches as seen from Line 6.
Once the response is received from the external service, itβs returned back through the chain:
β from fetch_movie_omdb to
β get_movie_details (service) to
β the controller, and finally
β back to the client via the router.
This layered, modular design ensures each part has a clear responsibility, making the codebase easier to maintain, test, and scale.
Now speaking of testingβ¦ I just hope the length of this article isnβt unit-testing your patience. π§ͺπ€―
π§ͺ Testing the Gears
In this section, weβll be blending three powerful tools:
pytest
to configure and run tests across the entire module,unittest
to drill down into individual units, andcoverage
to generate reports on whatβs been tested and what hasnβt.
This hybrid approach ensures both broad and deep testing of the system while keeping things modular, clear, and production-ready.
We'll start by configuring pytest.ini alongside coverage so it can automatically detect test files and report on which parts of the codebase are covered.
Next, weβll configure .coveragerc to omit files that donβt need to be tested like (e.g, __init.py, model.py) and other boilerplate or non-critical modules, so the reports remain clean, focused and provide meaningful insights.
Now that weβve done that, we can test if the configuration is properly set up by running the command:
$ pytest
(env) mrchike@practice:~/code/contributions/education/media_app$ pytest
==================================================== test session starts ====================================================
platform linux -- Python 3.10.12, pytest-8.3.5, pluggy-1.6.0
rootdir: /home/mrchike/code/contributions/education/media_app
configfile: pytest.ini
testpaths: tests/
plugins: anyio-4.9.0, cov-6.1.1
collected 0 items
/home/mrchike/code/contributions/education/media_app/env/lib/python3.10/site-packages/coverage/control.py:915: CoverageWarning: No data was collected. (no-data-collected)
self._warn("No data was collected.", slug="no-data-collected")
====================================================== tests coverage =======================================================
_____________________________________ coverage: platform linux, python 3.10.12-final-0 ______________________________________
Name Stmts Miss Cover Missing
----------------------------------------------------
movies/controller.py 10 10 0% 1-13
movies/service.py 19 19 0% 1-32
movies/tasks.py 0 0 100%
----------------------------------------------------
TOTAL 29 29 0%
=================================================== no tests ran in 0.16s ===================================================
And just like that we have this beautiful display of Missing tests you've not attended to leading us to the next step ...
Unit testing
Now that we know which files and lines of code require testing based on the coverage report, we will proceed with writing tests for controller.py and service.py in the corresponding movies
module under the tests
folder.
Run the command once more:
$ pytest
(env) mrchike@practice:~/code/contributions/education/media_app$ pytest
================================ test session starts =================================
platform linux -- Python 3.10.12, pytest-8.3.5, pluggy-1.6.0
rootdir: /home/mrchike/code/contributions/education/media_app
configfile: pytest.ini
testpaths: tests/
plugins: anyio-4.9.0, cov-6.1.1
collected 2 items
tests/movies/test_controller.py .. [100%]
=================================== tests coverage ===================================
__________________ coverage: platform linux, python 3.10.12-final-0 __________________
Name Stmts Miss Cover Missing
----------------------------------------------------
movies/controller.py 10 0 100%
movies/service.py 19 10 47% 20-32
movies/tasks.py 0 0 100%
----------------------------------------------------
TOTAL 29 10 66%
================================= 2 passed in 2.35s ==================================
(env) mrchike@practice:~/code/contributions/education/media_app$ pytest
========================================================= test session starts ==========================================================
platform linux -- Python 3.10.12, pytest-8.3.5, pluggy-1.6.0
rootdir: /home/mrchike/code/contributions/education/media_app
configfile: pytest.ini
testpaths: tests/
plugins: anyio-4.9.0, cov-6.1.1
collected 4 items
tests/movies/test_controller.py .. [ 50%]
tests/movies/test_service.py .. [100%]
============================================================ tests coverage ============================================================
___________________________________________ coverage: platform linux, python 3.10.12-final-0 ___________________________________________
Name Stmts Miss Cover Missing
----------------------------------------------------
movies/controller.py 10 0 100%
movies/service.py 19 0 100%
movies/tasks.py 0 0 100%
----------------------------------------------------
TOTAL 29 0 100%
========================================================== 4 passed in 2.42s ===========================================================
π Conclusion
In this first part of the series, we've laid the groundwork for building a scalable, production-ready FastAPI application. We covered essential concepts like project structure, modularity, and the flow of requests through various layers of the app. Most importantly, we set up testing strategies to ensure our code is robust and maintainable.
Quick Recap:
- Project Structure: Weβve created a clean, scalable folder structure thatβs ready for real-world growth.
- Request Flow: We broke down how requests travel through the router, controller, and service layers, ensuring clear responsibility separation.
- Testing: We integrated pytest, unittest, and coverage, and wrote tests to make sure the code performs as expected.
With this solid foundation, weβre well-equipped to scale up the app and handle more advanced features in Part 2, such as background tasks for CPU-bound operations. So, stay tuned for that!
π‘ Enjoyed this article? Connect with me on:
Your support means a lot, if youβd like to buy me a coffee βοΈ to keep me fueled, feel free to check out this link. Your generosity would go a long way in helping me continue to create content like this.
Until next time, happy coding! π¨πΎβπ»π
Previously Written Articles:
- How to Learn Effectively & Efficiently as a Professional in any Field π§ β±οΈπ― [Read here]
- Build, Innovate & Collaborate: Setting Up TensorFlow for Open-Source Contribution! πβ¨ [Read here]
- Building a Secure, Scalable Learning Platform with RBAC Features πππ [Read here]
- Setting Up MKDocs for Your Django Project: A Quick Guide! [Read here]
- How to Deploy React Portfolio | Project with GitHub Pages as of 2024 [Read here]
Resources:
Top comments (2)
Pretty cool seeing someone break this down step by step with the real testing too. Stuff like this always fires me up to build my own thing.
Thanks, David. Comments like yours make my effort worth it.