Clean Architecture Structure (FastAPI)
Approaching modular projects in Python FastAPI with clean architecture in mind is an excellent way to build scalable, maintainable, and testable applications. Here’s a suggested approach that combines FastAPI’s strengths with clean architecture principles:
project_root/
│
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── exceptions.py
│ │
│ ├── domain/
│ │ ├── __init__.py
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ └── user.py
│ │ └── schemas/
│ │ ├── __init__.py
│ │ └── user.py
│ │
│ ├── usecases/
│ │ ├── __init__.py
│ │ └── user.py
│ │
│ ├── interfaces/
│ │ ├── __init__.py
│ │ └── repositories/
│ │ ├── __init__.py
│ │ └── user_repository.py
│ │
│ ├── infrastructure/
│ │ ├── __init__.py
│ │ ├── database.py
│ │ └── repositories/
│ │ ├── __init__.py
│ │ └── sqlalchemy_user_repository.py
│ │
│ └── api/
│ ├── __init__.py
│ ├── dependencies.py
│ └── v1/
│ ├── __init__.py
│ └── endpoints/
│ ├── __init__.py
│ └── user.py
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_api/
│ │ └── test_user_api.py
│ └── test_usecases/
│ └── test_user_usecases.py
│
├── alembic/
│ ├── versions/
│ └── env.py
│
├── .env
├── .gitignore
├── requirements.txt
├── Dockerfile
└── docker-compose.ymlNow, let’s break down this structure and discuss how to implement clean architecture principles:
Domain Layer:
Use Case Layer:
Interface Adapters Layer:
Infrastructure Layer:
Main Application: Here’s how to implement some key components:
Domain Model:
from sqlalchemy import Column, Integer, String from app.infrastructure.database import Base class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) email = Column(String, unique=True, index=True) hashed_password = Column(String)Use Case:
from app.domain.models.user import User from app.interfaces.repositories.user_repository import UserRepository class UserUseCase: def __init__(self, user_repository: UserRepository): self.user_repository = user_repository async def create_user(self, username: str, email: str, password: str) -> User: # Implement business logic, validation, etc. user = User(username=username, email=email, hashed_password=self.hash_password(password)) return await self.user_repository.create(user) def hash_password(self, password: str) -> str: # Implement password hashing passRepository Interface:
from abc import ABC, abstractmethod from app.domain.models.user import User class UserRepository(ABC): @abstractmethod async def create(self, user: User) -> User: pass @abstractmethod async def get_by_id(self, user_id: int) -> User: pass # Add other methods as neededRepository Implementation:
from sqlalchemy.orm import Session from app.interfaces.repositories.user_repository import UserRepository from app.domain.models.user import User class SQLAlchemyUserRepository(UserRepository): def __init__(self, db: Session): self.db = db async def create(self, user: User) -> User: self.db.add(user) await self.db.commit() await self.db.refresh(user) return user async def get_by_id(self, user_id: int) -> User: return await self.db.query(User).filter(User.id == user_id).first()API Endpoint:
from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.api.dependencies import get_db from app.usecases.user import UserUseCase from app.infrastructure.repositories.sqlalchemy_user_repository import SQLAlchemyUserRepository from app.domain.schemas.user import UserCreate, UserResponse router = APIRouter() @router.post("/users/", response_model=UserResponse) async def create_user(user: UserCreate, db: Session = Depends(get_db)): user_repository = SQLAlchemyUserRepository(db) user_usecase = UserUseCase(user_repository) created_user = await user_usecase.create_user(user.username, user.email, user.password) return UserResponse.from_orm(created_user)
This structure adheres to clean architecture principles by:
Separating concerns into distinct layers
Ensuring that inner layers (domain and use cases) don’t depend on outer layers
Using dependency inversion for database access (through repositories)
Keeping the domain models independent of the persistence mechanism To further improve this structure:
Implement dependency injection for better testability
Use environment variables for configuration (stored in
.env)Implement proper error handling and custom exceptions
Add logging throughout the application
Implement unit and integration tests
Use Alembic for database migrations This approach allows for flexibility in changing databases, easy testing of business logic, and a clear separation of concerns. It’s scalable and maintainable, making it suitable for large, complex applications.