Skip to content

Backend (FastAPI)

How the backend is structured and how to add new features.

Layered structure

Each module follows the same layered pattern:

modules/projects/
├── models.py      # SQLAlchemy models — DB layer
├── schemas.py     # Pydantic schemas — validation/serialization
├── service.py     # Business logic — no FastAPI imports here
├── router.py      # FastAPI routes — thin, validation only
└── tests/

Rule: routers don't contain business logic. They validate input, call a service, return output. Business logic lives in services.

Dependency injection

FastAPI's Depends is how we wire things together. Common dependencies:

from fastapi import Depends
from src.core.deps import get_db, get_current_user, require_permission

@router.post("/projects")
async def create_project(
    payload: ProjectCreate,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_current_user),
    _: None = Depends(require_permission("projects:write")),
):
    return await projects_service.create(db, user, payload)

Tenant context

Tenant is set by middleware and accessible via request.state.tenant. Don't accept tenant from request body or query params — never trust the client.

# Good — tenant comes from authenticated context
project = await projects_service.create(db, current_tenant, payload)

# Bad — never accept tenant_id from the client
project = await projects_service.create(db, payload.tenant_id, payload)

Database access

All DB access via SQLAlchemy async. Use the session from Depends(get_db).

from sqlalchemy import select

stmt = select(Project).where(Project.id == project_id)
result = await db.execute(stmt)
project = result.scalar_one_or_none()

The session's search_path is set to the tenant's schema by middleware. Every query is automatically scoped.

Error handling

Use the custom exceptions in src.core.exceptions:

from src.core.exceptions import NotFoundError, PermissionDeniedError

if not project:
    raise NotFoundError(f"Project {project_id} not found")

These are converted to proper HTTP responses by the global exception handler.

See also