Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 41

FastAPI Project Structure for Scalability

Recommended resources

Hello! Welcome back to our course on mastering FastAPI.

In our last lesson, we implemented Role-Based Access Control (RBAC), adding another layer of security to our API. As we added more logic for users, authentication, and permissions, you likely noticed our project's complexity growing. A single file or a simple structure can quickly become unwieldy.

This brings us perfectly to our new module, "Modern API Architecture and Project Structure." Today's lesson directly addresses your interest in professional, modern code organization. We'll tackle the learning outcome: Organize a FastAPI project into a scalable folder structure with routers, schemas, models, and configuration.

We will move beyond the single main.py file and design a robust structure that separates concerns, making your application easier to scale, test, and maintain. For you, coming from Django, this is analogous to moving from a simple, single-app project to a well-architected multi-app project, but with patterns like the service and repository layers that are common in modern API development.

The Limits of a Single File

Tutorials often start with a single main.py file. It's great for demonstrating a single concept, but it's not a blueprint for a real-world application. A single file inevitably leads to:

  • Mixed Concerns: HTTP routing, business logic, data validation (Pydantic schemas), and database definitions (SQLAlchemy models) all live together.
  • Poor Readability: As the file grows, finding specific pieces of logic becomes a chore.
  • Maintenance Headaches: A small change in one area can have unintended consequences in another, and testing becomes difficult.

The solution is Separation of Concerns (SoC), a design principle that's just as relevant in FastAPI as it is in Django.


A Blueprint for a Scalable Project

To achieve SoC, we'll organize our code into a layered architecture. Each layer has a distinct responsibility. A request flows down through these layers, and the response flows back up.

Here’s a visual map of the kind of directory structure we're aiming for.

FastAPI Project Directory Structure Overview
This diagram shows a common and effective way to structure a FastAPI project. It maps conceptual roles like 'Business Logic' and 'API Routes' to concrete directories like `services/` and `routers/`, providing a clear blueprint for our application.

This structure typically involves the following key directories within a main source folder (often named app or src):

  • routers/: Handles incoming HTTP requests.
  • schemas/: Defines data shapes for requests and responses.
  • services/: Contains the core business logic.
  • repositories/: Manages data access and interaction with the database.
  • models/: Defines the database tables (e.g., with SQLAlchemy).
  • config.py: Manages application settings and environment variables.

Let's explore each of these components.


1. routers/: The Presentation Layer

The first step in breaking up a monolithic main.py is to modularize your endpoints using APIRouter. This allows you to group related endpoints into their own files.

  • Responsibility: Define API paths, handle HTTP methods, process path/query parameters, and call the business logic layer.
  • Django Analogy: This layer is a combination of Django's views.py and urls.py. An APIRouter instance is like a mini-URLconf for a specific feature (e.g., users, products), and the path operation functions (@router.get(...)) are the equivalent of view functions or DRF's ViewSet methods.

The following video provides a practical walkthrough of refactoring a single-file application to use APIRouter.

Modular Project Structure With FastAPI Routers - FastAPI Beyond CRUD (Part 4)

This video by Ssali Jonathan demonstrates the first crucial step in organizing a project: splitting endpoints into separate modules using APIRouter.

Watch from the beginning to 00:57, then skip to 03:26 and watch until 10:01. Focus on: The purpose of APIRouter for making an application modular. Creating a routes.py file for a specific feature (e.g., books). Defining a router = APIRouter() instance and changing path operation decorators from @app.get to @router.get. How the main app instance uses app.include_router(book_router, prefix="/books") to bring the modular endpoints into the main application. This is directly analogous to Django's include() in urls.py.


2. schemas/ and models/: The Data Layers

Once routes are separated, the next step is to organize your data definitions. In FastAPI, this is split into two distinct types of "models":

  • schemas/ (Pydantic Models): These define the shape of your API's data—what you expect in a request body and what you'll send in a response. They are your API's public contract.

    • Django Analogy: This is the direct equivalent of serializers.py in Django REST Framework. Pydantic BaseModels serve the same purpose of data validation, serialization, and documentation as DRF's Serializer classes.
  • models/ (ORM Models): These define your database tables. If you're using SQLAlchemy, these are your declarative models that map to database tables and columns.

    • Django Analogy: This is a one-to-one mapping with Django's models.py. Both define the structure of your data at the persistence layer.

It is a common convention to keep these in separate directories to avoid confusion between the API data shape (schemas) and the database table structure (models).


3. services/ and repositories/: The Logic Layers

This is where we address your goal of learning modern architectural patterns. Simply having folders for routes and schemas is good, but where does the complex business logic live? In Django, this logic often gets spread between views, model methods, and managers. A more structured approach uses Service and Repository layers.

This architectural pattern is visualized well in the diagram below.

FastAPI Layered Architecture Diagram
This diagram illustrates the flow of a request through a layered architecture. The 'Presentation Tier' (routers) receives the request, the 'Business Tier' (services) executes the logic, and the 'Data Tier' (repositories/data managers) interacts with the database. Pydantic schemas are used as contracts between these layers.
  • repositories/ (Repository Pattern): This layer's sole responsibility is to communicate with the data source. It abstracts away the ORM-specific code. Your business logic shouldn't know if you're using SQLAlchemy or an external API; it just asks the repository for data.

    • Example: A UserRepository might have methods like get_by_email(email), create(user_data), update(user_id, data). These methods would contain the actual db.execute(select(...)) or db.add(...) calls.
  • services/ (Service Layer): This layer contains your core business logic. It orchestrates the application's operations. A service method might call one or more repository methods, perform calculations, trigger other processes (like sending an email), and enforce business rules. It acts as the bridge between the HTTP world of routers and the data world of repositories.

    • Example: A UserService might have a create_user method that first calls repository.get_by_email() to check if a user exists. If not, it hashes the password, then calls repository.create() to save the new user.

The following article explains the rationale behind these patterns from the perspective of a developer familiar with Django, which should be particularly useful for you.

Django & FastAPI — Howto’s and guidelines

The article 'Django & FastAPI — Howto's and guidelines' provides excellent context on why a layered architecture is beneficial and how to implement it. It explicitly discusses these concepts from a DDD (Domain-Driven Design) perspective.

Please read the sections titled 'Business logic and developer experience' and 'Repositories'. Pay attention to: The comparison with the typical Django structure (models, serializers, views). The definition of the repository and service patterns. The goal of separating business logic from direct data source interactions. The code example shows a ConversationRepository that deals purely with the ORM, abstracting it away from the rest of the app.


A Complete Scalable Structure

Now, let's see how all these pieces fit together in a complete project. The following video from ArjanCodes provides a superb tour of a production-ready FastAPI project that utilizes this very structure.

Anatomy of a Scalable Python Project (FastAPI)

This video, 'Anatomy of a Scalable Python Project', provides a holistic view of a well-structured FastAPI application, touching on all the components we've discussed.

Watch the following segments to see the full picture: Folder Structure Overview (01:50 - 02:31): Introduces the top-level app and test folders. Routers/API Layer (02:31 - 04:12): Shows how the main.py is minimal and includes an APIRouter from api/v1/users.py. Notice he emphasizes that this layer is 'thin' and contains no business logic. DB & Schemas (05:29 - 06:45): Explains the db folder for SQLAlchemy models and the models (schemas) folder for Pydantic contracts. Service Layer (06:45 - 07:34): This is a key part. He shows the services folder, which contains the 'actual business logic' and orchestrates database operations. Summary of Structure (11:56 - 12:34): A concise recap of why this clean separation of concerns works so well.

The structure presented in the video is a powerful and widely adopted standard. Here is a summary of the folder tree, which you can use as a template for your own projects:

my_fastapi_project/
├── app/
│   ├── __init__.py
│   ├── main.py             # FastAPI app instance, includes routers
│   ├── core/
│   │   ├── __init__.py
│   │   └── config.py       # Pydantic BaseSettings for configuration
│   ├── api/
│   │   ├── __init__.py
│   │   └── v1/
│   │       ├── __init__.py
│   │       └── routes/
│   │           ├── __init__.py
│   │           └── users.py    # APIRouter for users
│   ├── db/
│   │   ├── __init__.py
│   │   └── models.py       # SQLAlchemy models
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── user.py         # Pydantic schemas for users
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py # Business logic for users
│   └── repositories/
│       ├── __init__.py
│       └── user_repo.py    # Data access logic for users
│
├── tests/                  # Application tests
├── .env                    # Environment variables
└── requirements.txt

Conclusion

Today, you've learned the principles and practical steps for structuring a FastAPI application for scalability and maintainability. By moving from a single-file approach to a layered architecture, you create a project that is easier to reason about, test, and expand upon.

Key Takeaways:

  • Separate Concerns: The core principle is to give each component a single, well-defined responsibility.
  • APIRouter is for Modularity: Use routers to group endpoints by feature, just like Django apps and include().
  • Distinguish schemas and models: schemas/ contains Pydantic models for your API contract (like DRF Serializers), while models/ contains ORM models for your database schema (like Django's models.py).
  • Embrace Service and Repository Layers: This pattern provides a clean home for your business logic (services/) and decouples it from your data access code (repositories/), leading to a more robust and testable architecture.

Preview of the Next Lesson:

We briefly touched on the config.py file for managing settings. In our next lesson, we will dive deep into this topic: "Manage environment variables and application settings using Pydantic's BaseSettings." You'll learn how to create a robust configuration system that can handle different environments (development, testing, production) and securely manage secrets.