Folder Structure
Standard FastAPI layout — routers, dependencies, services, and schemas.
Every FastAPI service follows the same layout. It maps one-to-one onto the Express structure so engineers can move between Node and Python services without relearning where things live.
Project layout
backend-api/
├── app/
│ ├── main.py # Entry point — create_app() + FastAPI instance
│ │
│ ├── api/ # Route definitions (thin — delegate to services)
│ │ ├── deps.py # Shared dependencies (auth, db session)
│ │ └── v1/
│ │ ├── __init__.py # api_router — includes all v1 routers
│ │ ├── users.py
│ │ └── projects.py
│ │
│ ├── services/ # Business logic (no FastAPI imports here)
│ │ ├── users.py
│ │ ├── projects.py
│ │ ├── push.py # FCM send helpers
│ │ ├── uploads.py # S3 presigned URLs
│ │ └── email/ # Email provider adapters
│ │ ├── __init__.py # Factory (ses | sendgrid | brevo)
│ │ ├── ses.py
│ │ ├── sendgrid.py
│ │ └── brevo.py
│ │
│ ├── middleware/ # ASGI middleware & exception handlers
│ │ ├── errors.py
│ │ └── rate_limit.py
│ │
│ ├── models/ # SQLAlchemy ORM models
│ │ └── user.py
│ │
│ ├── schemas/ # Pydantic request/response models
│ │ ├── users.py
│ │ └── projects.py
│ │
│ ├── db/ # Database config & session factory
│ │ ├── session.py # Async engine + session maker
│ │ └── migrations/ # Alembic migrations
│ │
│ ├── core/ # Cross-cutting config & security
│ │ ├── config.py # Pydantic settings
│ │ ├── security.py # Cognito JWT verification
│ │ └── logging.py
│ │
│ └── lib/ # Shared clients
│ ├── supabase.py
│ ├── s3.py
│ └── fcm.py
│
├── tests/
│ ├── unit/
│ └── integration/
│
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
└── uv.lockLayer responsibilities
| Layer | Responsibility | Must NOT |
|---|---|---|
| api/v1/ | Define paths, declare dependencies, call service | Contain business logic |
| api/deps.py | Auth, db session, pagination as Depends() | Call the database directly |
| schemas/ | Pydantic validation & serialization | Hold business rules |
| services/ | Business logic, orchestration | Import fastapi / Request |
| models/ · db/ | SQLAlchemy models, session management | Handle HTTP concerns |
Router example
# app/api/v1/projects.py
from fastapi import APIRouter, Depends
from app.api.deps import CurrentUser, require_scope
from app.schemas.projects import ProjectCreate, ProjectOut
from app.services import projects as projects_service
router = APIRouter(prefix="/projects", tags=["projects"])
@router.get("", response_model=list[ProjectOut])
async def list_projects(user: CurrentUser):
return await projects_service.list_by_user(user.sub)
@router.post("", response_model=ProjectOut, status_code=201)
async def create_project(
payload: ProjectCreate,
user: CurrentUser,
_: None = Depends(require_scope("write:projects")),
):
return await projects_service.create(user.sub, payload)Schema example
# app/schemas/projects.py
from datetime import datetime
from pydantic import BaseModel
class ProjectCreate(BaseModel):
name: str
description: str | None = None
class ProjectOut(BaseModel):
id: str
name: str
owner_id: str
created_at: datetime
model_config = {"from_attributes": True}Service example
# app/services/projects.py
from sqlalchemy import select
from app.db.session import async_session
from app.models.project import Project
async def list_by_user(user_id: str) -> list[Project]:
async with async_session() as session:
result = await session.execute(
select(Project).where(Project.owner_id == user_id)
)
return list(result.scalars().all())Naming conventions
| Item | Convention | Example |
|---|---|---|
| Routers | {resource}.py in api/v1/ | users.py |
| Services | {resource}.py in services/ | users.py |
| Schemas | {resource}.py in schemas/ | users.py |
| ORM models | singular {resource}.py in models/ | user.py |
| Modules / functions | snake_case | list_by_user |