Skip to main content

FastAPI Project Structure

In this tutorial, we'll explore how to organize your FastAPI projects for better maintainability, scalability, and collaboration.

Introduction

When you start building applications with FastAPI, you may initially focus on just getting the API endpoints to work. However, as your application grows, having a well-organized project structure becomes crucial. A good structure helps with:

  • Maintainability: Making it easy to find and update code
  • Scalability: Allowing the project to grow without becoming chaotic
  • Collaboration: Enabling team members to work on different parts of the codebase
  • Testing: Facilitating unit and integration testing

Let's dive into how you can structure your FastAPI applications effectively.

Basic Project Structure

Here's a recommended basic structure for a FastAPI project:

my_fastapi_app/

├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI application instance
│ ├── dependencies.py # Dependency functions
│ ├── routers/ # API endpoints organized by feature/resource
│ │ ├── __init__.py
│ │ ├── users.py
│ │ └── items.py
│ ├── models/ # Pydantic models/schemas
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ ├── crud/ # Database CRUD operations
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ └── db/ # Database configuration
│ ├── __init__.py
│ └── database.py

├── tests/ # Test directory
│ ├── __init__.py
│ ├── test_users.py
│ └── test_items.py

├── .env # Environment variables
├── requirements.txt # Dependencies
└── README.md

Let's break down each component of this structure.

Main Application File

The main.py file is the entry point of your application. It creates the FastAPI instance and includes the routers:

python
from fastapi import FastAPI
from app.routers import users, items
from app.db.database import engine, Base

# Create database tables
Base.metadata.create_all(bind=engine)

# Create FastAPI app
app = FastAPI(
title="My FastAPI App",
description="A sample FastAPI application with well-organized structure",
version="0.1.0"
)

# Include routers
app.include_router(users.router)
app.include_router(items.router)

@app.get("/")
async def root():
return {"message": "Welcome to my FastAPI application!"}

Database Configuration

In db/database.py, you would set up your database connection:

python
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os

DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")

engine = create_engine(
DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

Models and Schemas

For your data models, you'll typically have two types:

  1. SQLAlchemy models - These represent your database tables
  2. Pydantic models/schemas - These define the structure of your API requests and responses

SQLAlchemy Models Example

You might place these in a directory like app/models/sql/:

python
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from app.db.database import 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")

Pydantic Models Example

In app/models/user.py:

python
from pydantic import BaseModel, EmailStr
from typing import List, Optional

class UserBase(BaseModel):
email: EmailStr

class UserCreate(UserBase):
password: str

class User(UserBase):
id: int
is_active: bool

class Config:
orm_mode = True

Routers

Routers help organize your API endpoints by feature or resource. Here's an example of a users router in app/routers/users.py:

python
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

from app.db.database import get_db
from app.models.user import User, UserCreate
from app.crud import user as user_crud

router = APIRouter(
prefix="/users",
tags=["users"],
responses={404: {"description": "Not found"}},
)

@router.get("/", response_model=List[User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = user_crud.get_users(db, skip=skip, limit=limit)
return users

@router.post("/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = user_crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return user_crud.create_user(db=db, user=user)

@router.get("/{user_id}", response_model=User)
def read_user(user_id: int, db: Session = Depends(get_db)):
db_user = user_crud.get_user(db, user_id=user_id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user

CRUD Operations

CRUD files contain database operations for each model. For example, app/crud/user.py:

python
from sqlalchemy.orm import Session
from app.models.sql.user import User
from app.models.user import UserCreate
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def get_user(db: Session, user_id: int):
return db.query(User).filter(User.id == user_id).first()

def get_user_by_email(db: Session, email: str):
return db.query(User).filter(User.email == email).first()

def get_users(db: Session, skip: int = 0, limit: int = 100):
return db.query(User).offset(skip).limit(limit).all()

def create_user(db: Session, user: UserCreate):
hashed_password = pwd_context.hash(user.password)
db_user = User(email=user.email, hashed_password=hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user

Dependencies

Common dependencies can be placed in dependencies.py:

python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.db.database import get_db
from app.crud.user import get_user_by_email
import jwt
from datetime import datetime, timedelta

SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
except jwt.PyJWTError:
raise credentials_exception
user = get_user_by_email(db, email=email)
if user is None:
raise credentials_exception
return user

Environment Variables

Use a .env file to store configuration that might change between environments:

DATABASE_URL=sqlite:///./app.db
SECRET_KEY=your-secret-key-here
DEBUG=True

You can load these variables using the python-dotenv package:

python
from dotenv import load_dotenv
import os

load_dotenv() # Load environment variables from .env file

SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = os.getenv("DEBUG", "False").lower() == "true"

Advanced Project Structure

For larger projects, you might want a more comprehensive structure:

my_fastapi_app/

├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/ # Core modules
│ │ ├── __init__.py
│ │ ├── config.py # Configuration settings
│ │ └── security.py # Security utilities
│ ├── api/ # API endpoints
│ │ ├── __init__.py
│ │ ├── api_v1/
│ │ │ ├── __init__.py
│ │ │ ├── endpoints/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── users.py
│ │ │ │ └── items.py
│ │ │ └── router.py
│ │ └── dependencies.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── domain/ # Domain models
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ ├── schemas/ # Pydantic schemas
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ ├── crud/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── user.py
│ │ └── item.py
│ ├── db/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── session.py
│ └── services/ # Business logic
│ ├── __init__.py
│ └── user_service.py

├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── test_users.py
│ │ └── test_items.py
│ └── crud/
│ ├── __init__.py
│ ├── test_user.py
│ └── test_item.py

├── alembic/ # Database migrations
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── versions/

├── .env
├── .env.example
├── requirements.txt
├── requirements-dev.txt # Development dependencies
├── Dockerfile
├── docker-compose.yml
└── README.md

Real-World Example: Blog API

Let's see a practical example of how you might structure a blog API:

blog_api/

├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── posts.py
│ │ ├── users.py
│ │ └── comments.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── sql/
│ │ │ ├── __init__.py
│ │ │ ├── post.py
│ │ │ ├── user.py
│ │ │ └── comment.py
│ │ └── schemas/
│ │ ├── __init__.py
│ │ ├── post.py
│ │ ├── user.py
│ │ └── comment.py
│ ├── crud/
│ │ ├── __init__.py
│ │ ├── post.py
│ │ ├── user.py
│ │ └── comment.py
│ ├── db/
│ │ ├── __init__.py
│ │ └── database.py
│ └── services/
│ ├── __init__.py
│ ├── auth.py
│ └── blog.py

└── ...

Example Post Router (app/routers/posts.py):

python
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from sqlalchemy.orm import Session

from app.db.database import get_db
from app.models.schemas.post import PostCreate, Post, PostUpdate
from app.models.schemas.user import User
from app.crud import post as post_crud
from app.dependencies import get_current_user

router = APIRouter(
prefix="/posts",
tags=["posts"]
)

@router.get("/", response_model=List[Post])
def get_posts(
skip: int = 0,
limit: int = 10,
db: Session = Depends(get_db)
):
"""
Get all blog posts with pagination.
"""
return post_crud.get_posts(db, skip=skip, limit=limit)

@router.post("/", response_model=Post, status_code=status.HTTP_201_CREATED)
def create_post(
post: PostCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Create a new blog post.
"""
return post_crud.create_post(db, post=post, author_id=current_user.id)

@router.get("/{post_id}", response_model=Post)
def read_post(
post_id: int,
db: Session = Depends(get_db)
):
"""
Get a specific blog post by its ID.
"""
db_post = post_crud.get_post(db, post_id=post_id)
if db_post is None:
raise HTTPException(status_code=404, detail="Post not found")
return db_post

@router.put("/{post_id}", response_model=Post)
def update_post(
post_id: int,
post: PostUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Update a blog post.
"""
db_post = post_crud.get_post(db, post_id=post_id)
if db_post is None:
raise HTTPException(status_code=404, detail="Post not found")
if db_post.author_id != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized to update this post")
return post_crud.update_post(db, post_id=post_id, post=post)

Testing Your FastAPI Project

A well-structured project makes testing easier. Here's an example of a test file:

python
# tests/api/test_users.py
from fastapi.testclient import TestClient
import pytest
from app.main import app

client = TestClient(app)

def test_create_user():
response = client.post(
"/users/",
json={"email": "[email protected]", "password": "password123"},
)
assert response.status_code == 200
data = response.json()
assert data["email"] == "[email protected]"
assert "id" in data
user_id = data["id"]

# Test getting the created user
response = client.get(f"/users/{user_id}")
assert response.status_code == 200
data = response.json()
assert data["email"] == "[email protected]"
assert data["id"] == user_id

Best Practices

When structuring your FastAPI project, keep these best practices in mind:

  1. Separation of Concerns: Keep different parts of your application separate (e.g., API endpoints, database models, business logic)

  2. Keep Routes Simple: Route handlers should be thin - they should parse requests, call services, and format responses

  3. Use Dependency Injection: FastAPI's dependency injection system helps manage dependencies and makes testing easier

  4. Versioning: Consider versioning your API from the start (e.g., /api/v1/users)

  5. Configuration Management: Use environment variables for configuration that changes between environments

  6. Documentation: Document your API with FastAPI's built-in OpenAPI integration

  7. Use a Consistent Naming Convention: Stick to a naming pattern for files and functions

Summary

A well-structured FastAPI project is key to building maintainable and scalable web applications. By organizing your code into logical components like routers, models, schemas, and services, you make it easier to develop, test, and extend your application.

As your project grows, you can adapt the structure to fit your needs, but starting with a clean organization will save you time and headaches in the long run.

Additional Resources

Exercises

  1. Create a basic FastAPI project structure for a to-do list application
  2. Implement CRUD operations for to-do items with proper separation of concerns
  3. Add authentication to your application using JWT tokens
  4. Create unit tests for your API endpoints
  5. Set up a CI/CD pipeline to automatically test your application when code is pushed to GitHub

By following a structured approach to your FastAPI projects, you'll build more robust applications that are easier to maintain and extend over time.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)