Hey guys! Let's dive into setting up a solid project structure for your FastAPI applications. Whether you're building APIs, web apps, or microservices, a well-organized project can save you tons of headaches down the road. This guide focuses specifically on medium-sized projects, striking a balance between simplicity and scalability. We'll cover everything from directory layout to module organization, so you can build robust and maintainable applications.

    Why Project Structure Matters?

    Before we get our hands dirty, let's talk about why a good project structure is so important. Think of your project as a house. If the foundation is weak or the rooms are arranged haphazardly, living in it will be a nightmare. Similarly, a poorly structured FastAPI project can lead to:

    • Increased Complexity: As your project grows, navigating and understanding the codebase becomes increasingly difficult.
    • Reduced Maintainability: Making changes or adding new features becomes a risky endeavor, as you're never quite sure what might break.
    • Difficulty in Testing: A tangled codebase makes it hard to write effective tests, leading to bugs and instability.
    • Collaboration Challenges: When multiple developers are working on the same project, a lack of structure can lead to conflicts and confusion.

    A well-defined project structure, on the other hand, promotes:

    • Readability: Code is easier to understand and navigate.
    • Maintainability: Changes and additions can be made with confidence.
    • Testability: Code is organized in a way that makes it easy to write and run tests.
    • Collaboration: Developers can work together more effectively, reducing conflicts and improving productivity.

    Ultimately, investing time in setting up a proper project structure upfront will pay off handsomely in the long run.

    Recommended Project Structure

    Alright, let's get down to the nitty-gritty. Here's a project structure that works well for medium-sized FastAPI projects:

    myproject/
    ├── app/
    │   ├── __init__.py
    │   ├── api/
    │   │   ├── __init__.py
    │   │   ├── endpoints/
    │   │   │   ├── __init__.py
    │   │   │   ├── items.py
    │   │   │   └── users.py
    │   │   ├── dependencies.py
    │   │   └── routes.py
    │   ├── core/
    │   │   ├── __init__.py
    │   │   ├── config.py
    │   │   └── security.py
    │   ├── db/
    │   │   ├── __init__.py
    │   │   ├── database.py
    │   │   ├── models.py
    │   │   └── migrations/
    │   │       ├── versions/
    │   ├── schemas/
    │   │   ├── __init__.py
    │   │   ├── items.py
    │   │   └── users.py
    │   ├── services/
    │   │   ├── __init__.py
    │   │   ├── item_service.py
    │   │   └── user_service.py
    │   ├── tests/
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   ├── test_items.py
    │   │   └── test_users.py
    │   ├── main.py
    ├── migrations/
    │   ├── alembic.ini
    │   └── versions/
    ├── .env
    ├── README.md
    ├── requirements.txt
    └── pyproject.toml
    

    Let's break down each component:

    • myproject/: The root directory of your project. Replace "myproject" with your actual project name.
    • app/: This directory contains all the application-specific code.
    • app/__init__.py: An empty file that tells Python to treat the app directory as a package.
    • app/api/: This directory houses the API-related code.
    • app/api/__init__.py: An empty file that tells Python to treat the api directory as a package.
    • app/api/endpoints/: Contains the different API endpoint files (e.g., items.py, users.py).
    • app/api/endpoints/__init__.py: An empty file that tells Python to treat the endpoints directory as a package.
    • app/api/dependencies.py: Defines API-wide dependencies.
    • app/api/routes.py: Combines the API routes.
    • app/core/: Contains core application logic and configurations.
    • app/core/__init__.py: An empty file that tells Python to treat the core directory as a package.
    • app/core/config.py: Manages application configuration settings.
    • app/core/security.py: Deals with authentication and authorization logic.
    • app/db/: Handles database-related code.
    • app/db/__init__.py: An empty file that tells Python to treat the db directory as a package.
    • app/db/database.py: Contains the database connection logic.
    • app/db/models.py: Defines the database models.
    • app/db/migrations/: Contains Alembic migration scripts.
    • app/schemas/: Defines data schemas for request and response bodies.
    • app/schemas/__init__.py: An empty file that tells Python to treat the schemas directory as a package.
    • app/schemas/items.py: Schemas related to items.
    • app/schemas/users.py: Schemas related to users.
    • app/services/: Contains business logic and service functions.
    • app/services/__init__.py: An empty file that tells Python to treat the services directory as a package.
    • app/services/item_service.py: Service functions related to items.
    • app/services/user_service.py: Service functions related to users.
    • app/tests/: Contains unit and integration tests.
    • app/tests/__init__.py: An empty file that tells Python to treat the tests directory as a package.
    • app/tests/conftest.py: Defines test fixtures and configurations.
    • app/tests/test_items.py: Tests for item-related functionality.
    • app/tests/test_users.py: Tests for user-related functionality.
    • app/main.py: The entry point of the application.
    • migrations/: Contains Alembic migration configuration.
    • migrations/alembic.ini: Alembic configuration file.
    • migrations/versions/: Contains the actual migration scripts.
    • .env: Stores environment variables.
    • README.md: A file containing project documentation.
    • requirements.txt: Lists the project dependencies.
    • pyproject.toml: Specifies build system requirements for the project.

    Deep Dive into Key Components

    Let's zoom in on some of the most important parts of this structure.

    1. API Layer (app/api/)

    The API layer is where you define your API endpoints, using FastAPI's routing capabilities. This layer is divided into a couple key subdirectories:

    • endpoints/: This directory contains individual modules for each resource (e.g., items.py, users.py). Each module defines the routes and handlers for that specific resource. For instance, items.py might contain routes for creating, reading, updating, and deleting items.

      # app/api/endpoints/items.py
      from fastapi import APIRouter, Depends
      from typing import List
      from app.schemas.items import Item, ItemCreate
      from app.services.item_service import ItemService
      
      router = APIRouter()
      
      @router.post("/", response_model=Item)
      async def create_item(item_create: ItemCreate, item_service: ItemService = Depends(ItemService)):
          return item_service.create_item(item_create)
      
      @router.get("/{item_id}", response_model=Item)
      async def read_item(item_id: int, item_service: ItemService = Depends(ItemService)):
          return item_service.get_item(item_id)
      
      @router.get("/", response_model=List[Item])
      async def read_items(skip: int = 0, limit: int = 100, item_service: ItemService = Depends(ItemService)):
          return item_service.get_items(skip=skip, limit=limit)
      
    • dependencies.py: This module defines reusable dependencies that can be injected into your API endpoints. Dependencies can be used for authentication, authorization, database connections, and other common tasks. This is a great way to keep your endpoint functions clean and focused on business logic.

      # app/api/dependencies.py
      from fastapi import Depends, HTTPException, status
      from fastapi.security import OAuth2PasswordBearer
      
      oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
      
      async def get_current_user(token: str = Depends(oauth2_scheme)):
          # Implementation to validate token and retrieve user
          # (Replace with your actual authentication logic)
          if token != "valid_token":
              raise HTTPException(
                  status_code=status.HTTP_401_UNAUTHORIZED,
                  detail="Invalid authentication credentials",
                  headers={"WWW-Authenticate": "Bearer"},
              )
          return {"username": "example_user"}
      
    • routes.py: This file serves as the central point for collecting all your API routes defined in the endpoints/ directory. It imports each endpoint module and includes their routers into the main FastAPI app. This keeps your main.py file cleaner and more organized.

      # app/api/routes.py
      from fastapi import APIRouter
      from app.api.endpoints import items, users
      
      api_router = APIRouter()
      api_router.include_router(items.router, prefix="/items", tags=["items"])
      api_router.include_router(users.router, prefix="/users", tags=["users"])
      

    2. Core Layer (app/core/)

    The core layer contains the fundamental configurations and security-related logic for your application. This helps to separate concerns related to configs and security implementations.

    • config.py: Here's where you define your application's configuration settings. This might include database connection strings, API keys, and other environment-specific variables. Consider using a library like pydantic-settings or python-dotenv to manage your configuration in a structured and secure way.

      # app/core/config.py
      from pydantic_settings import BaseSettings, SettingsConfigDict
      
      class Settings(BaseSettings):
          app_name: str = "Awesome FastAPI App"
          database_url: str = "sqlite:///./test.db"
          secret_key: str = "super-secret-key"
      
          model_config = SettingsConfigDict(env_file=".env", extra="ignore")
      
      settings = Settings()
      
    • security.py: This module handles authentication and authorization logic. It might include functions for hashing passwords, generating JWT tokens, and verifying user credentials. Using a dedicated security module keeps your authentication logic separate from your API endpoints, making it easier to maintain and update.

      # app/core/security.py
      from passlib.context import CryptContext
      from datetime import datetime, timedelta
      import jwt
      from app.core.config import settings
      
      pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
      
      def verify_password(plain_password, hashed_password):
          return pwd_context.verify(plain_password, hashed_password)
      
      def get_password_hash(password):
          return pwd_context.hash(password)
      
      def create_access_token(data: dict, expires_delta: timedelta | None = None):
          to_encode = data.copy()
          if expires_delta:
              expire = datetime.utcnow() + expires_delta
          else:
              expire = datetime.utcnow() + timedelta(minutes=15)
          to_encode.update({"exp": expire})
          encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm="HS256")
          return encoded_jwt
      

    3. Database Layer (app/db/)

    This part of the project handles everything that has to do with your database. Connecting, defining models, and running migrations.

    • database.py: This module is responsible for establishing a connection to your database. It typically uses an ORM (Object-Relational Mapper) like SQLAlchemy to interact with the database. Here, you'll configure the database engine and session management.

      # app/db/database.py
      from sqlalchemy import create_engine
      from sqlalchemy.orm import sessionmaker
      from app.core.config import settings
      
      engine = create_engine(settings.database_url)
      SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
      
      def get_db():
          db = SessionLocal()
          try:
              yield db
          finally:
              db.close()
      
    • models.py: This module defines the database models that represent your data structures. Each model corresponds to a table in the database and defines the columns and relationships between tables. These models are used by the ORM to interact with the database.

      # app/db/models.py
      from sqlalchemy import Boolean, Column, Integer, String
      from sqlalchemy.orm import relationship
      from sqlalchemy.ext.declarative import declarative_base
      
      Base = declarative_base()
      
      class User(Base):
          __tablename__ = "users"
      
          id = Column(Integer, primary_key=True, index=True)
          email = Column(String, unique=True, index=True)
          hashed_password = Column(String)
          is_active = Column(Boolean, default=True)
      
          items = relationship("Item", back_populates="owner")
      
      class Item(Base):
          __tablename__ = "items"
      
          id = Column(Integer, primary_key=True, index=True)
          title = Column(String, index=True)
          description = Column(String, index=True)
          owner_id = Column(Integer, ForeignKey("users.id"))
      
          owner = relationship("User", back_populates="items")
      
    • migrations/: This directory contains Alembic migration scripts, which are used to manage changes to your database schema. Alembic allows you to track and apply database changes in a controlled and repeatable manner.

    4. Schemas Layer (app/schemas/)

    The schemas layer defines the data structures used for request and response bodies in your API. Using Pydantic models, you can enforce data validation and serialization, ensuring that your API receives and returns data in the expected format.

    • items.py

      # app/schemas/items.py
      from pydantic import BaseModel
      
      class ItemBase(BaseModel):
          title: str
          description: str | None = None
      
      class ItemCreate(ItemBase):
          pass
      
      class ItemUpdate(ItemBase):
          pass
      
      class Item(ItemBase):
          id: int
          owner_id: int
      
          class Config:
              orm_mode = True
      
    • users.py

      # app/schemas/users.py
      from pydantic import BaseModel
      
      class UserBase(BaseModel):
          email: str
      
      class UserCreate(UserBase):
          password: str
      
      class UserUpdate(UserBase):
          pass
      
      class User(UserBase):
          id: int
          is_active: bool
      
          class Config:
              orm_mode = True
      

    5. Services Layer (app/services/)

    The services layer encapsulates the business logic of your application. Service functions are responsible for performing specific tasks, such as creating users, processing payments, or generating reports. By separating the business logic from the API endpoints, you can improve the testability and maintainability of your code.

    • item_service.py

      # app/services/item_service.py
      from sqlalchemy.orm import Session
      from app.db import models
      from app.schemas import items
      from app.db.database import get_db
      from fastapi import Depends
      
      class ItemService:
          def __init__(self, db: Session = Depends(get_db)):
              self.db = db
      
          def create_item(self, item: items.ItemCreate, user_id: int = 1):
              db_item = models.Item(**item.model_dump(), owner_id=user_id)
              self.db.add(db_item)
              self.db.commit()
              self.db.refresh(db_item)
              return db_item
      
          def get_item(self, item_id: int):
              return self.db.query(models.Item).filter(models.Item.id == item_id).first()
      
          def get_items(self, skip: int = 0, limit: int = 100):
              return self.db.query(models.Item).offset(skip).limit(limit).all()
      
    • user_service.py

      # app/services/user_service.py
      from sqlalchemy.orm import Session
      from app.db import models
      from app.schemas import users
      from app.core.security import get_password_hash
      from app.db.database import get_db
      from fastapi import Depends
      
      class UserService:
          def __init__(self, db: Session = Depends(get_db)):
              self.db = db
      
          def create_user(self, user: users.UserCreate):
              hashed_password = get_password_hash(user.password)
              db_user = models.User(email=user.email, hashed_password=hashed_password)
              self.db.add(db_user)
              self.db.commit()
              self.db.refresh(db_user)
              return db_user
      
          def get_user(self, user_id: int):
              return self.db.query(models.User).filter(models.User.id == user_id).first()
      
          def get_user_by_email(self, email: str):
              return self.db.query(models.User).filter(models.User.email == email).first()
      

    6. Tests Layer (app/tests/)

    A comprehensive test suite is crucial for ensuring the quality and reliability of your application. The tests directory should contain unit tests, integration tests, and end-to-end tests. Use a testing framework like pytest to write and run your tests.

    • conftest.py

      # app/tests/conftest.py
      import pytest
      from fastapi.testclient import TestClient
      from sqlalchemy import create_engine
      from sqlalchemy.orm import sessionmaker
      from app.main import app
      from app.db.database import Base, get_db
      from app.core.config import settings
      
      SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
      
      engine = create_engine(
          SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
      )
      TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
      
      
      @pytest.fixture()
      def override_get_db():
          def _get_db():
              try:
                  db = TestingSessionLocal()
                  yield db
              finally:
                  db.close()
      
          return _get_db
      
      
      @pytest.fixture(scope="module")
      def test_db():
          Base.metadata.create_all(bind=engine)
          yield
          Base.metadata.drop_all(bind=engine)
      
      
      @pytest.fixture()
      def client(test_db, override_get_db):
          app.dependency_overrides[get_db] = override_get_db
          client = TestClient(app)
          yield client
          app.dependency_overrides = {}
      
    • test_items.py

      # app/tests/test_items.py
      from fastapi.testclient import TestClient
      from sqlalchemy.orm import Session
      from app.db import models
      from app.schemas import items
      
      def test_create_item(client: TestClient, db: Session):
          item_data = {"title": "Test Item", "description": "This is a test item"}
          response = client.post("/items/", json=item_data)
          assert response.status_code == 200
          data = response.json()
          assert data["title"] == "Test Item"
          assert data["description"] == "This is a test item"
          assert data["owner_id"] == 1
      
      def test_read_item(client: TestClient, db: Session):
          item_data = {"title": "Test Item", "description": "This is a test item", "owner_id": 1}
          db_item = models.Item(**item_data)
          db.add(db_item)
          db.commit()
          db.refresh(db_item)
          response = client.get(f"/items/{db_item.id}")
          assert response.status_code == 200
          data = response.json()
          assert data["title"] == "Test Item"
          assert data["description"] == "This is a test item"
          assert data["owner_id"] == 1
      

    Main Application File (app/main.py)

    The main.py file is the entry point of your FastAPI application. It's responsible for creating the FastAPI app instance, including the API router, and starting the server.

    # app/main.py
    from fastapi import FastAPI
    from app.api.routes import api_router
    
    app = FastAPI()
    
    app.include_router(api_router)
    

    Environment Variables (.env)

    The .env file is used to store sensitive information, such as API keys, database passwords, and other environment-specific variables. This file should never be committed to your version control system. Use a library like python-dotenv to load environment variables from the .env file into your application.

    # .env
    DATABASE_URL=postgresql://user:password@host:port/database
    SECRET_KEY=your-secret-key
    

    Dependencies (requirements.txt and pyproject.toml)

    The requirements.txt file lists all the Python packages that your project depends on. You can generate this file using pip freeze > requirements.txt. Alternatively, you can use pyproject.toml to manage project dependencies and build configurations. This file is used by modern packaging tools like poetry and pipenv.

    Conclusion

    And there you have it! A well-structured FastAPI project, ready to tackle medium-sized applications. Remember, this is just a starting point. Feel free to adapt this structure to fit your specific needs and preferences. The key is to be consistent and intentional in your organization. A little planning upfront can save you a lot of headaches down the road. Happy coding, guys!