DEV Community

Cover image for πŸ”₯ FastAPI in Production: Build, Scale & Deploy – Series A: Codebase Design
Mr Chike
Mr Chike

Posted on • Edited on

πŸ”₯ FastAPI in Production: Build, Scale & Deploy – Series A: Codebase Design

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

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
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ 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
Enter fullscreen mode Exit fullscreen mode

Now update main.py with the content from this file, then start the project by running:

uvicorn main:app --reload --port 8000
Enter fullscreen mode Exit fullscreen mode

This will launch the app with live reload enabled on port 8000. (Feel free to change the port to whatever your majesty desires.)

Up & Running

Now we create the module we want to work on from base

$ cp -r base movies
Enter fullscreen mode Exit fullscreen mode

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.

schema

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, and

  • coverage 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
Enter fullscreen mode Exit fullscreen mode
(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 ===================================================
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
(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 ===========================================================
Enter fullscreen mode Exit fullscreen mode

🏁 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)

Collapse
 
nevodavid profile image
Nevo David

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.

Collapse
 
mrchike profile image
Mr Chike

Thanks, David. Comments like yours make my effort worth it.