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.
NB: Don't forget to register for your movie api key @ https://www.omdbapi.com/apikey.aspx and update you
.envfile...
Then run the following commands:
# Refresh environment variables to reflect update
source .env
# Start app with live reloading on specified port
uvicorn main:app --reload --port 8000
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:
pytestto configure and run tests across the entire module,unittestto drill down into individual units, andcoverageto 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.