Skip to main content

FastAPI Project Structure

A well-organized project structure is essential for building maintainable and scalable FastAPI applications. When your project is properly structured, it becomes easier to collaborate with other developers, test your code, and add new features. In this guide, we'll explore the recommended project structure for FastAPI applications and best practices for organizing your code.

Why Project Structure Matters

Before diving into specific structures, let's understand why having a good project structure is important:

  • Maintainability: Makes it easier to find and update code
  • Scalability: Allows your project to grow without becoming unmanageable
  • Collaboration: Helps team members understand the codebase quickly
  • Testing: Facilitates writing and running tests
  • Documentation: Makes your code self-documenting

Basic FastAPI Project Structure

Let's start with a basic project structure for a small FastAPI application:

my_fastapi_app/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── dependencies.py
│ ├── models/
│ │ └── __init__.py
│ ├── routers/
│ │ └── __init__.py
│ ├── schemas/
│ │ └── __init__.py
│ └── services/
│ └── __init__.py
├── tests/
│ ├── __init__.py
│ └── test_main.py
├── .env
├── .gitignore
├── requirements.txt
└── README.md

This basic structure separates your application code, tests, and configuration files. Let's examine each component:

The app directory

This is where all your application code lives. The main.py file serves as the entry point for your application.

python
# app/main.py
from fastapi import FastAPI
from app.routers import items, users

app = FastAPI(
title="My FastAPI App",
description="A simple FastAPI application",
version="0.1.0"
)

app.include_router(items.router)
app.include_router(users.router)

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

The dependencies.py file

This file contains shared dependencies used across your application, such as database connections or authentication utilities.

python
# app/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)):
# Validate token and get user
# This is a simplified example
if token != "valid_token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return {"username": "testuser"}

The models directory

This contains your database models, typically using SQLAlchemy or other ORMs:

python
# app/models/user.py
from sqlalchemy import Column, Integer, String
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)

The routers directory

Routers help organize your API endpoints by feature or resource:

python
# app/routers/users.py
from fastapi import APIRouter, Depends, HTTPException
from app.dependencies import get_current_user
from app.schemas.user import User, UserCreate

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

@router.get("/", response_model=list[User])
async def read_users():
# Logic to fetch users from database
return [{"id": 1, "email": "[email protected]", "is_active": True}]

@router.post("/", response_model=User)
async def create_user(user: UserCreate):
# Logic to create a new user
return {"id": 2, "email": user.email, "is_active": True}

The schemas directory

Schemas (using Pydantic) define the structure of request and response data:

python
# app/schemas/user.py
from pydantic import BaseModel, EmailStr

class UserBase(BaseModel):
email: EmailStr

class UserCreate(UserBase):
password: str

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

class Config:
orm_mode = True

The services directory

Services contain business logic separated from your route handlers:

python
# app/services/user_service.py
from app.models.user import User
from app.schemas.user import UserCreate
from sqlalchemy.orm import Session
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 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

Advanced Project Structure

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

my_fastapi_app/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── dependencies.py
│ │ └── v1/
│ │ ├── __init__.py
│ │ ├── endpoints/
│ │ │ ├── __init__.py
│ │ │ ├── items.py
│ │ │ └── users.py
│ │ └── router.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── security.py
│ ├── db/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── session.py
│ │ └── init_db.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── item.py
│ │ └── user.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── item.py
│ │ └── user.py
│ └── services/
│ ├── __init__.py
│ ├── item_service.py
│ └── user_service.py
├── alembic/
│ ├── versions/
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── alembic.ini
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── test_items.py
│ │ └── test_users.py
│ └── services/
│ ├── __init__.py
│ ├── test_item_service.py
│ └── test_user_service.py
├── .env
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── requirements-dev.txt
└── README.md

This structure includes additional components:

API Versioning

Versioning your API helps maintain backward compatibility:

python
# app/api/v1/router.py
from fastapi import APIRouter
from app.api.v1.endpoints import items, users

router = APIRouter()
router.include_router(items.router)
router.include_router(users.router)
python
# app/main.py
from fastapi import FastAPI
from app.api.v1.router import router as api_v1_router

app = FastAPI(title="My Advanced FastAPI App")

app.include_router(api_v1_router, prefix="/api/v1")

Configuration Management

Centralize configuration settings:

python
# app/core/config.py
import secrets
from pydantic import BaseSettings, PostgresDsn, validator
from typing import Any, Dict, Optional

class Settings(BaseSettings):
PROJECT_NAME: str = "My FastAPI App"
API_V1_STR: str = "/api/v1"

SECRET_KEY: str = secrets.token_urlsafe(32)
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days

POSTGRES_SERVER: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None

@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_SERVER"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)

class Config:
env_file = ".env"
case_sensitive = True

settings = Settings()

Database Setup

Organize database-related code:

python
# app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from app.core.config import settings

engine = create_engine(settings.SQLALCHEMY_DATABASE_URI)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

# Dependency for endpoints
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

Real-World Example

Let's look at a complete example of a FastAPI application that follows these best practices:

Main Application File

python
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.api.v1.router import router as api_v1_router
from app.core.config import settings
from app.db.init_db import create_first_superuser

app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
)

# Set up CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Include routers
app.include_router(api_v1_router, prefix=settings.API_V1_STR)

@app.get("/")
def root():
return {"message": "Welcome to the API"}

@app.on_event("startup")
async def startup_event():
await create_first_superuser()

Router Example

python
# app/api/v1/endpoints/users.py
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session

from app.api.dependencies import get_current_active_superuser, get_current_active_user, get_db
from app.models.user import User
from app.schemas.user import User as UserSchema, UserCreate, UserUpdate
from app.services.user_service import create_user, get_users, update_user

router = APIRouter()

@router.get("/", response_model=List[UserSchema])
def read_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Retrieve users.
"""
users = get_users(db, skip=skip, limit=limit)
return users

@router.post("/", response_model=UserSchema)
def create_new_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Create new user.
"""
user = create_user(db, obj_in=user_in)
return user

@router.put("/me", response_model=UserSchema)
def update_user_me(
*,
db: Session = Depends(get_db),
user_update: UserUpdate = Body(...),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update own user.
"""
user = update_user(db, db_obj=current_user, obj_in=user_update)
return user

Best Practices for FastAPI Project Structure

Here are some best practices to follow:

  1. Separation of concerns: Keep your code organized by functionality (models, schemas, routers, services)

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

  3. API versioning: Version your APIs to maintain backward compatibility

  4. Keep routes clean: Move business logic to service functions

  5. Use environment variables: Store configuration in environment variables, not in code

  6. Error handling: Implement consistent error handling across your application

  7. Logging: Set up proper logging for debugging and monitoring

  8. Testing: Organize tests to mirror your application structure

  9. Documentation: Use FastAPI's built-in documentation features and add docstrings

  10. Type annotations: Use Python's type hints throughout your code

Summary

A well-organized FastAPI project structure is essential for building maintainable and scalable applications. By separating your code into logical components like models, schemas, routers, and services, you can make your codebase easier to understand and extend.

The basic structure works well for small applications, while the advanced structure is suitable for larger, more complex projects. Regardless of the size, following best practices like separation of concerns, dependency injection, and proper configuration management will help you build robust FastAPI applications.

Additional Resources

Exercises

  1. Convert a simple Flask application to FastAPI using the basic project structure
  2. Implement API versioning in an existing FastAPI project
  3. Extract business logic from route handlers into service functions
  4. Add proper dependency injection for database connections
  5. Set up a testing environment with pytest for a FastAPI application

With these guidelines and examples, you should now be able to structure your FastAPI applications effectively, regardless of their size and complexity.



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