FastAPI Project Structure Best Practices: A Scalable Directory Layout Guide

Organize FastAPI projects into an app/ package with sub-modules for routers, schemas, models, CRUD, core configuration, and dependencies, using an application factory pattern in main.py to prevent circular imports and enable testability.

FastAPI's minimalist design allows you to start with a single file, but production services require a clean architecture. The tiangolo/fastapi repository demonstrates a proven project structure that separates concerns across dedicated modules, making large codebases maintainable and testable.

Why FastAPI Project Structure Matters

FastAPI intentionally imposes no folder conventions, as seen in fastapi/applications.py where the FastAPI class is implemented without filesystem assumptions. However, real-world applications quickly outgrow single-file scripts. A structured layout prevents circular imports, enables unit testing with TestClient, and aligns with Docker layer caching strategies documented in the official deployment guides.

The following structure reflects the "Bigger Applications" tutorial pattern from docs/en/docs/tutorial/bigger-applications.md. Each directory has a single responsibility:

.
├── app
│   ├── __init__.py          # optional: expose `app = FastAPI()`

│   ├── main.py              # creates the FastAPI instance & includes routers

│   ├── core
│   │   ├── config.py        # Pydantic Settings

│   │   ├── security.py      # OAuth2, JWT helpers

│   │   └── logger.py
│   ├── crud
│   │   ├── user.py
│   │   └── item.py
│   ├── db
│   │   ├── base.py          # SQLAlchemy Base

│   │   └── session.py       # SessionLocal, engine

│   ├── models
│   │   ├── user.py
│   │   └── item.py
│   ├── schemas
│   │   ├── user.py
│   │   └── item.py
│   ├── routers
│   │   ├── __init__.py
│   │   ├── users.py
│   │   └── items.py
│   └── deps
│       └── dependencies.py
├── tests
│   ├── conftest.py          # pytest fixtures (client, db)

│   ├── test_users.py
│   └── test_items.py
├── Dockerfile
├── requirements.txt
└── README.md

Implementing the Core Components

Application Factory Pattern in main.py

Centralize the FastAPI instance creation in app/main.py to prevent circular imports and enable testing. As implemented in fastapi/applications.py, the FastAPI class accepts configuration parameters like title and version.


# app/main.py

from fastapi import FastAPI
from app.routers import users, items
from app.core.config import settings

def get_application() -> FastAPI:
    """Factory that creates the FastAPI instance."""
    app = FastAPI(
        title=settings.PROJECT_NAME,
        version=settings.VERSION,
        openapi_url="/openapi.json",
    )
    # Register routers

    app.include_router(users.router, prefix="/users", tags=["users"])
    app.include_router(items.router, prefix="/items", tags=["items"])
    return app

app = get_application()

This pattern appears in the "Bigger Applications" tutorial, which demonstrates that keeping the app instance in one file prevents import conflicts when routers need to import the application.

Domain-Driven Routers with APIRouter

Group routes by domain into separate modules under app/routers/, using APIRouter instances for each logical entity. According to docs/en/docs/tutorial/bigger-applications.md, this pattern mirrors Flask blueprints and allows you to mount entire domain routers with app.include_router().


# app/routers/users.py

from fastapi import APIRouter, Depends, HTTPException, status
from typing import List

from app import crud, schemas, deps

router = APIRouter()

@router.get("/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100,
               db=Depends(deps.get_db)):
    """Return a list of users."""
    return crud.user.get_multi(db, skip=skip, limit=limit)

@router.post("/", response_model=schemas.User, status_code=status.HTTP_201_CREATED)
def create_user(user_in: schemas.UserCreate, db=Depends(deps.get_db)):
    """Create a new user."""
    return crud.user.create(db, obj_in=user_in)

Database Session Management with Dependencies

Encapsulate database sessions in app/deps/dependencies.py using generator functions that yield SQLAlchemy or SQLModel sessions. This approach, documented in the "Dependencies" section of the Bigger Applications tutorial, leverages FastAPI's dependency injection system to ensure sessions are properly closed after each request.


# app/deps/dependencies.py

from typing import Generator
from sqlalchemy.orm import Session
from app.db.session import SessionLocal

def get_db() -> Generator[Session, None, None]:
    """Yield a DB session and ensure it is closed after the request."""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Centralized Configuration in Core

Store environment variables and secrets in app/core/config.py using Pydantic Settings. This centralizes validation and type coercion for configuration values, preventing the leakage of sensitive data into business logic.


# app/core/config.py

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    PROJECT_NAME: str = "My FastAPI Project"
    VERSION: str = "1.0.0"
    DATABASE_URL: str = "postgresql://user:pass@localhost/db"
    
    class Config:
        env_file = ".env"

settings = Settings()

Testing Strategy

Place tests in a top-level tests/ directory and use fixtures to instantiate the application without side effects. FastAPI's own test suite in tests/test_cli.py demonstrates this pattern using TestClient.


# tests/conftest.py

import pytest
from fastapi.testclient import TestClient
from app.main import get_application

@pytest.fixture(scope="module")
def client() -> TestClient:
    """Provide a TestClient that uses the FastAPI app."""
    app = get_application()
    with TestClient(app) as c:
        yield c

Using a factory function like get_application() allows you to create fresh app instances for each test module, preventing state leakage between tests.

Docker and Deployment

Optimize container builds by copying dependency files before application code. The FastAPI Docker guide at docs/en/docs/deployment/docker.md recommends this layer-caching strategy.


# Dockerfile

FROM python:3.12-slim

WORKDIR /code

# Install only requirements first to leverage Docker cache

COPY ./requirements.txt /code/
RUN pip install --no-cache-dir -r /code/requirements.txt

# Copy the application code

COPY ./app /code/app
COPY ./main.py /code/

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

This ordering ensures that Docker caches the heavy pip install layer unless requirements.txt changes, significantly speeding up rebuilds during development.

Summary

  • Separate concerns by organizing code into routers/, schemas/, models/, crud/, core/, and deps/ sub-packages within an app/ directory.
  • Centralize the FastAPI instance using a factory function in app/main.py to prevent circular imports and enable testable application creation.
  • Group routes with APIRouter into domain-specific modules under app/routers/, keeping endpoint definitions isolated and maintainable.
  • Isolate dependencies in app/deps/dependencies.py to manage database sessions and authentication logic via FastAPI's injection system.
  • Optimize Docker builds by copying requirements.txt before application code to maximize layer caching.

Frequently Asked Questions

Should I use a single main.py file or the full package structure for my FastAPI project?

Start with a single file for prototypes, but migrate to the package structure once you exceed a few endpoints. The tiangolo/fastapi documentation demonstrates that the APIRouter pattern becomes essential when you need to split logic across multiple files, preventing circular import issues that commonly occur in flat structures.

How do I avoid circular imports when splitting my FastAPI app into multiple modules?

Use an application factory pattern by defining a get_application() function in app/main.py rather than creating the FastAPI instance at module import time. As implemented in fastapi/applications.py, delaying instantiation until the factory is called prevents import-time side effects and allows routers to import the app instance without circular dependencies.

Where should I store database connection logic in a FastAPI project?

Encapsulate database sessions in app/deps/dependencies.py using generator functions that yield SQLAlchemy or SQLModel sessions. This approach, documented in the "Bigger Applications" tutorial, leverages FastAPI's dependency injection system to ensure sessions are properly closed after each request while keeping database logic out of route handlers.

What is the best way to organize FastAPI routers for a large application?

Group routes by domain into separate modules under app/routers/, using APIRouter instances for each logical entity such as users or items. According to the official tutorial in docs/en/docs/tutorial/bigger-applications.md, this pattern mirrors Flask blueprints and allows you to mount entire domain routers with app.include_router() while maintaining strict isolation between business domains.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →