Best Practices for Structuring FastAPI Projects

FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. As your project grows, having a well - structured project becomes crucial for maintainability, scalability, and ease of development. In this blog, we will explore the best practices for structuring FastAPI projects, covering fundamental concepts, usage methods, common practices, and real - world best practices.

Table of Contents

  1. Fundamental Concepts
    • Why Structure Matters
    • Key Components of a FastAPI Project
  2. Usage Methods
    • Setting Up the Project Directory
    • Defining Routes and Endpoints
    • Handling Dependencies
  3. Common Practices
    • Error Handling
    • Logging
    • Testing
  4. Best Practices
    • Database Integration
    • Authentication and Authorization
    • Code Organization and Modularity
  5. Conclusion
  6. References

Fundamental Concepts

Why Structure Matters

A well - structured project makes it easier for developers to understand the codebase, collaborate effectively, and add new features or fix bugs without introducing regressions. It also improves the overall performance and security of the application.

Key Components of a FastAPI Project

  • Main Application: The entry point of the FastAPI application, where the app instance is created.
  • Routes and Endpoints: Defined using decorators like @app.get, @app.post, etc., to handle different HTTP requests.
  • Dependencies: Reusable code components that can be injected into routes and endpoints to perform common tasks such as authentication, database access, etc.
  • Models: Used to define the data structures and perform data validation.

Usage Methods

Setting Up the Project Directory

A common project structure for a FastAPI project is as follows:

project/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── users.py
│   │   └── items.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── users.py
│   │   └── items.py
│   └── dependencies/
│       ├── __init__.py
│       └── auth.py
├── tests/
│   ├── __init__.py
│   └── test_users.py
├── requirements.txt
└── .env

Defining Routes and Endpoints

Here is a simple example of defining a route in FastAPI:

# app/routes/users.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/users/")
async def read_users():
    return {"message": "This is a list of users"}

And integrating it into the main application:

# app/main.py
from fastapi import FastAPI
from app.routes.users import router as users_router

app = FastAPI()
app.include_router(users_router)


@app.get("/")
async def root():
    return {"message": "Hello World"}

Handling Dependencies

Dependencies are used to share common functionality across routes. For example, an authentication dependency:

# app/dependencies/auth.py
from fastapi import Depends, HTTPException
from fastapi.security import APIKeyHeader

api_key_header = APIKeyHeader(name="X - API - Key")


async def get_api_key(api_key: str = Depends(api_key_header)):
    if api_key != "secret_api_key":
        raise HTTPException(status_code=401, detail="Invalid API Key")
    return api_key

Using the dependency in a route:

# app/routes/items.py
from fastapi import APIRouter, Depends
from app.dependencies.auth import get_api_key

router = APIRouter()


@router.get("/items/", dependencies=[Depends(get_api_key)])
async def read_items():
    return {"message": "This is a list of items"}

Common Practices

Error Handling

FastAPI provides built - in support for error handling. You can raise HTTPException to return custom error responses.

from fastapi import FastAPI, HTTPException

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id < 1:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item_id": item_id}

Logging

Logging is essential for debugging and monitoring. You can use the Python standard logging module.

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@app.get("/")
async def root():
    logger.info("Root endpoint accessed")
    return {"message": "Hello World"}

Testing

You can use the TestClient from fastapi.testclient to write unit and integration tests.

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

client = TestClient(app)


def test_read_users():
    response = client.get("/users/")
    assert response.status_code == 200
    assert response.json() == {"message": "This is a list of users"}

Best Practices

Database Integration

For database integration, you can use SQLAlchemy with FastAPI. Here is a simple example:

# app/models/items.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String)
# app/dependencies/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
# app/routes/items.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.models.items import Item
from app.dependencies.database import get_db

router = APIRouter()


@router.get("/items/")
async def read_items(db: Session = Depends(get_db)):
    items = db.query(Item).all()
    return items

Authentication and Authorization

Use OAuth2 with JWT (JSON Web Tokens) for authentication and authorization. FastAPI has built - in support for OAuth2.

# app/dependencies/auth.py
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from datetime import datetime, timedelta
from typing import Optional

SECRET_KEY = "your - secret - key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


def create_access_token(data: dict, expires_delta: Optional[timedelta] = 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, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    return username

Code Organization and Modularity

Keep your code modular by separating concerns into different files and directories. Use functions and classes to encapsulate functionality and make the code more reusable.

Conclusion

Structuring a FastAPI project properly is essential for its long - term success. By following the best practices outlined in this blog, including proper project directory setup, handling dependencies, error handling, logging, testing, database integration, authentication, and code modularity, you can build robust, scalable, and maintainable FastAPI applications.

References