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:
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:
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:
- SQLAlchemy models - These represent your database tables
- 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/
:
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
:
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
:
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
:
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
:
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:
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
):
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:
# 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:
-
Separation of Concerns: Keep different parts of your application separate (e.g., API endpoints, database models, business logic)
-
Keep Routes Simple: Route handlers should be thin - they should parse requests, call services, and format responses
-
Use Dependency Injection: FastAPI's dependency injection system helps manage dependencies and makes testing easier
-
Versioning: Consider versioning your API from the start (e.g.,
/api/v1/users
) -
Configuration Management: Use environment variables for configuration that changes between environments
-
Documentation: Document your API with FastAPI's built-in OpenAPI integration
-
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
- Official FastAPI Documentation
- FastAPI Full Stack Project Example
- SQLAlchemy Documentation
- Alembic for Database Migrations
Exercises
- Create a basic FastAPI project structure for a to-do list application
- Implement CRUD operations for to-do items with proper separation of concerns
- Add authentication to your application using JWT tokens
- Create unit tests for your API endpoints
- 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! :)