Project Structure
FastAPI makes it easy to ship the first endpoint. The real challenge is preventing route functions from becoming a dump for validation, transactions, domain rules, and serialization.
Quick takeaway: keep routes thin, move business rules into services, treat persistence as its own layer, and use Pydantic models for boundary contracts rather than as your entire application model.
Start With the Request Flow
Why It Matters
- When routes own every responsibility, testing becomes harder and imports get tangled.
- Pydantic models and ORM models have different jobs, so treating them as the same type usually creates churn.
- Async endpoints do not automatically imply clean architecture.
A Practical Layout
app/
api/
routes/
users.py
schemas/
user.py
services/
user_service.py
repositories/
user_repository.py
db/
session.py
core/
config.py
main.pySmall Example
from collections.abc import AsyncIterator
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import session_factory
from app.schemas.user import UserCreate, UserRead
from app.services.user_service import UserService
router = APIRouter(prefix="/users", tags=["users"])
async def get_session() -> AsyncIterator[AsyncSession]:
async with session_factory() as session:
yield session
def get_user_service(session: AsyncSession = Depends(get_session)) -> UserService:
return UserService(session)
@router.post("", response_model=UserRead)
async def create_user(
payload: UserCreate,
service: UserService = Depends(get_user_service),
) -> UserRead:
return await service.create_user(payload)The route owns HTTP concerns. The service owns business behavior. The session lifecycle is explicit. That separation survives growth far better than a giant route module.
Practical Checklist
FastAPI types stay at the edge
Keep `Depends`, `Request`, `Response`, and framework-specific objects near the router layer.
Separate schemas from ORM entities
Boundary contracts and persistence entities evolve for different reasons.
Control startup side effects
Prefer explicit lifespan or startup wiring over imports that connect clients or mutate global state.