🚀
0. Project Structure
++++
Engineering
Apr 2026×18 min read

A complete map of the repository layout and a deep walkthrough of every recurring code pattern — frozen dataclasses, SQLAlchemy 2.0 mapped columns, Pydantic ConfigDict, annotated DI providers, and more.

0. Project Structure & Code Patterns

Driptanil Datta
Driptanil DattaSoftware Developer

Reading the Folder Tree

Before diving into any layer, it helps to hold the whole tree in your head. Every directory in this project has a specific job, and knowing which file lives where prevents you from adding code to the wrong place.

.
├── app/
│   └── server.py                   # Uvicorn entry point
├── scripts/
│   └── init_db.py                  # Database bootstrap CLI
├── tests/
│   ├── conftest.py                 # Pytest path setup
│   ├── test_auth_middleware.py     # Auth enforcement tests
│   ├── test_logging.py             # Request logging tests
│   ├── test_todo_crud.py           # Service-layer unit tests
│   └── test_todo_router_pydantic.py # Schema and router contract tests
├── server/
│   ├── api/
│   │   ├── config/src/
│   │   │   ├── main.py             # Alternative app factory (for test injection)
│   │   │   └── state.py            # Typed AppState dataclass
│   │   └── src/
│   │       ├── main.py             # Primary app factory (create_app)
│   │       ├── dependencies.py     # All DI providers in one place
│   │       ├── db/
│   │       │   ├── models.py       # SQLAlchemy ORM entities (TodoEntity)
│   │       │   └── session.py      # Engine and session factory helpers
│   │       ├── middleware/
│   │       │   └── auth_middleware.py  # HTTP auth enforcement
│   │       ├── repositories/
│   │       │   └── todo_repository.py # Persistence + domain mapping
│   │       ├── routers/
│   │       │   ├── __init__.py     # init_routes — mounts all routers
│   │       │   ├── auth_router.py  # /api/auth endpoints
│   │       │   └── todo_router.py  # /api/todos endpoints
│   │       ├── services/
│   │       │   ├── auth_service.py # JWT issuance and validation
│   │       │   └── todo_service.py # Todo business logic
│   │       └── utils/
│   │           ├── error.py        # Shared error helpers
│   │           └── logging.py      # Loguru configuration + middleware
│   ├── migration/
│   │   └── src/
│   │       ├── lib.py              # Migrator registry (Migration dataclass)
│   │       ├── runner.py           # upgrade() / downgrade() async functions
│   │       └── m20260408_000001_create_todos_table.py  # First migration
│   └── shared/
│       └── src/
│           ├── models/
│           │   ├── auth.py         # AuthUser domain dataclass
│           │   └── todo.py         # Todo domain dataclass + TodoStatus enum
│           ├── schemas/
│           │   ├── auth.py         # LoginRequest, TokenResponse, AuthUserResponse
│           │   └── todo.py         # CreateTodoRequest, TodoResponse
│           └── utils/
│               └── error.py        # Shared utilities
└── pyproject.toml                  # Poetry deps, Ruff config, Pytest config

The Three Module Roots

The project is divided into three top-level namespaces under server/, each with a distinct responsibility:

server/api/src/ — runtime API code. This is where FastAPI, SQLAlchemy, middleware, routers, services, and repositories live. Everything here is HTTP- and database-aware.

server/shared/src/ — shared domain code. Plain Python dataclasses and Pydantic schemas. No FastAPI imports. No SQLAlchemy. This is the language the whole application speaks internally — domain models flow from persistence through services through to API responses.

server/migration/src/ — schema evolution. Async migration modules and the runner that applies them. Completely separate from the application runtime.

The rule is: code in server/api/src/ can import from server/shared/src/, but server/shared/src/ must never import from server/api/src/. The shared layer has no knowledge of HTTP or database mechanics.


Code Patterns

1. Frozen Dataclass — Domain Models

Domain models in server/shared/src/models/ are plain Python @dataclass(frozen=True) objects:

# server/shared/src/models/todo.py
class TodoStatus(str, Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
 
 
@dataclass(frozen=True)
class Todo:
    id: UUID
    title: str
    description: str | None
    status: TodoStatus
    created_at: datetime
    updated_at: datetime

frozen=True makes instances immutable — you can't accidentally mutate a Todo after it's created. Business logic that needs a modified todo (e.g., completing it) constructs a new Todo instance with the changed fields rather than mutating the existing one. This eliminates a whole class of subtle bugs where a service mutates an object that's also referenced somewhere else.

TodoStatus inherits from both str and Enum. The str base means the enum values serialize directly to strings ("pending", "completed") in JSON without any custom serializer — FastAPI and Pydantic handle it transparently.

The AuthUser model follows the same pattern:

# server/shared/src/models/auth.py
@dataclass(frozen=True)
class AuthUser:
    id: UUID
    name: str
    email: str
    created_at: datetime
    updated_at: datetime

2. SQLAlchemy 2.0 Mapped Column — ORM Entities

The TodoEntity in server/api/src/db/models.py uses SQLAlchemy 2.0's typed mapped column syntax:

# server/api/src/db/models.py
class TodoEntity(Base):
    __tablename__ = "todos"
 
    id: Mapped[str] = mapped_column(
        String(36), primary_key=True, default=lambda: str(uuid4())
    )
    title: Mapped[str] = mapped_column(String(255), nullable=False)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    status: Mapped[TodoStatus] = mapped_column(
        Enum(TodoStatus), default=TodoStatus.PENDING, nullable=False
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        default=lambda: datetime.now(UTC),
        onupdate=lambda: datetime.now(UTC),
        nullable=False,
    )

Mapped[str] is the 2.0 style — it gives the ORM full type information at the class level, enabling better IDE support and static analysis. mapped_column() replaces the old Column() API.

The UUID is stored as String(36) because SQLite doesn't have a native UUID column type. The conversion to and from UUID happens in the repository's _map_to_domain method — the ORM entity is just a persistence detail.

expire_on_commit=False is set on the session factory. Without this, SQLAlchemy expires all attributes on every commit, meaning a second access to any field after a commit would trigger another database round-trip. With async sessions, that's a coroutine you'd have to await — inside a sync property access that can't be awaited. Setting expire_on_commit=False prevents that, at the cost of the session holding slightly stale data (acceptable here since repositories are short-lived).


3. App Factory Pattern — create_app()

The application is created by a factory function in server/api/src/main.py, not at module level:

# server/api/src/main.py
def create_app() -> FastAPI:
    configure_logging()
 
    app = FastAPI(title=SERVICE_NAME, lifespan=lifespan, ...)
 
    # Database setup
    engine = create_async_engine("sqlite+aiosqlite:///./test.db")
    async_session = async_sessionmaker(engine, expire_on_commit=False)
 
    # Service initialization
    session = async_session()
    repo = TodoRepository(session)
    todo_service = TodoService(repo)
    auth_service = AuthService()
 
    # State injection
    app.state.todo_service = todo_service
    app.state.auth_service = auth_service
 
    attach_request_logging(app)
    attach_auth_middleware(app)
    init_routes(app)
 
    return app

The entry point app/server.py is intentionally thin — just one call:

# app/server.py
from server.api.src.main import create_app
 
app = create_app()

The factory pattern means tests can call create_app() too (or the alternative create_application() in server/api/config/src/main.py when they need to inject a test database engine). You never have a module-level app = FastAPI() that's hard to control in tests.

The wiring order inside create_app() matters: logging middleware is attached first (so it wraps everything), then auth middleware (so it runs inside logging), then routes are registered last.


4. Lifespan Context — Startup and Shutdown

The lifespan context manager replaces the deprecated @app.on_event("startup") decorator:

# server/api/src/main.py
@asynccontextmanager
async def lifespan(_app: FastAPI):
    log_app_startup(service=SERVICE_NAME, version=SERVICE_VERSION)
    yield
    log_app_shutdown(service=SERVICE_NAME, version=SERVICE_VERSION)

Everything before yield runs on startup; everything after runs on shutdown. The lifespan is passed to the FastAPI constructor. This pattern composes cleanly — if you need to run migrations on startup or close an HTTP client on shutdown, you add it here, not scattered across event handlers.


5. Annotated Dependency Injection — TodoServiceDep

FastAPI's dependency injection uses Depends(), but writing Annotated[TodoService, Depends(get_todo_service)] in every route signature is verbose. The convention in this template is to define named type aliases in server/api/src/dependencies.py:

# server/api/src/dependencies.py
def get_todo_service(request: Request) -> TodoService:
    return request.app.state.todo_service
 
TodoServiceDep = Annotated[TodoService, Depends(get_todo_service)]
 
 
def get_current_user(request: Request) -> AuthUser:
    user = getattr(request.state, "auth_user", None)
    if user is None:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return user
 
AuthServiceDep = Annotated[AuthService, Depends(get_auth_service)]
CurrentUserDep  = Annotated[AuthUser,    Depends(get_current_user)]

Route signatures become clean and readable:

# Usage in router
async def create_todo(body: CreateTodoRequest, service: TodoServiceDep): ...
async def get_authenticated_user(user: CurrentUserDep) -> AuthUserResponse: ...

The get_current_user provider also acts as a safety net — if middleware failed to set auth_user for some reason, the dependency raises a 401 before the handler body runs. Auth enforcement is checked twice.

The single file location means that in tests, you can override the entire dependency by pointing the override map at a mock provider — one change swaps the dependency for every route that uses it.


6. Middleware Attachment Pattern

Middleware is not registered directly on the FastAPI instance — it's attached through named functions:

# Attachment functions
attach_request_logging(app)  # in server/api/src/utils/logging.py
attach_auth_middleware(app)   # in server/api/src/middleware/auth_middleware.py

Each function uses @app.middleware("http") internally:

# server/api/src/middleware/auth_middleware.py
def attach_auth_middleware(app: FastAPI) -> None:
    @app.middleware("http")
    async def auth_middleware(request: Request, call_next):
        ...

This pattern keeps middleware definition close to its implementation file. The alternative — registering middleware inside create_app() directly — works but mixes concerns. Here, create_app() reads as a wiring list: logging, auth, routes. The implementation details stay in their own modules.


7. Router Init Pattern — init_routes()

All route registration happens in a single init_routes() function in server/api/src/routers/__init__.py:

# server/api/src/routers/__init__.py
def init_routes(app: FastAPI) -> None:
    api_router = APIRouter(prefix="/api")
    api_router.include_router(auth_router.router, prefix="/auth")
    api_router.include_router(todo_router.router, prefix="/todos")
    app.include_router(api_router)

The prefix hierarchy is: app/api/auth or /todos. This means:

  • POST /api/auth/token
  • GET /api/auth/me
  • GET /api/todos/
  • POST /api/todos/
  • PATCH /api/todos/{id}/complete

Adding a new resource means creating a new router file and including it here. The init_routes() function is also directly imported by test setup (from server.api.src.routers import init_routes) so tests mount exactly the same routes as production.


8. Pydantic ConfigDict Pattern — Request and Response Schemas

All Pydantic models in server/shared/src/schemas/ use ConfigDict for consistent configuration:

# ConfigDict on every model
class CreateTodoRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    ...
 
class TodoResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")
    ...

extra="forbid" applies to both request and response models. On requests, it blocks unknown incoming fields. On responses, it means from_domain() can only pass fields the schema declares — if from_domain() accidentally passes an unexpected keyword argument, Pydantic raises a ValidationError immediately at construction time rather than silently including or ignoring it.

StringConstraints creates reusable type aliases:

TitleStr = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=200)]
DescriptionStr = Annotated[str, StringConstraints(max_length=2000)]

Declaring these as Annotated type aliases means the constraints appear once and can be reused across multiple schemas. They also propagate into the OpenAPI schema automatically — the generated /openapi.json will include minLength: 1, maxLength: 200 on the title field.


9. Migration Module Pattern — up and down

Each migration is a standalone Python module with two async functions:

# server/migration/src/m20260408_000001_create_todos_table.py
async def up(conn: AsyncConnection) -> None:
    await conn.execute(text("CREATE TABLE IF NOT EXISTS todos (...)"))
 
async def down(conn: AsyncConnection) -> None:
    await conn.execute(text("DROP TABLE IF EXISTS todos;"))

The naming convention m{YYYYMMDD}_{sequence}_{description}.py gives you:

  • Chronological ordering when sorted alphabetically
  • A unique sequence number for same-day migrations
  • A human-readable description in the filename

The Migrator registry in lib.py holds references to the up and down functions, not the module itself. This means you can import and unit-test individual migration functions directly if needed.

CREATE TABLE IF NOT EXISTS makes up idempotent — running it twice doesn't fail. This matters during testing, where the database may be reset and re-initialized between test runs.


How the Layers Connect in Practice

To close the loop, here's the full request path for POST /api/todos/ annotated against the folder map:

HTTP Request

server/api/src/utils/logging.py         # request logging middleware assigns request_id

server/api/src/middleware/auth_middleware.py  # auth middleware checks Authorization header

server/api/src/routers/todo_router.py   # router validates CreateTodoRequest body

server/api/src/dependencies.py          # TodoServiceDep resolves from app.state

server/api/src/services/todo_service.py # service creates Todo domain object

server/api/src/repositories/todo_repository.py  # repo maps Todo → TodoEntity, commits

server/api/src/db/models.py             # SQLAlchemy writes to todos table

server/shared/src/models/todo.py        # Todo dataclass returned up through repo and service

server/shared/src/schemas/todo.py       # TodoResponse.from_domain() maps to API shape

HTTP Response

Each arrow is a clean handoff. No layer reaches past its direct neighbor. The shared domain objects (Todo, AuthUser) are the common currency that all layers speak, and the schemas are the translation layer at the API boundary.

Drip

Driptanil Datta

Software Developer

Building full-stack systems, one commit at a time. This blog is a centralized learning archive for developers.

Legal Notes
Disclaimer

The content provided on this blog is for educational and informational purposes only. While I strive for accuracy, all information is provided "as is" without any warranties of completeness, reliability, or accuracy. Any action you take upon the information found on this website is strictly at your own risk.

Copyright & IP

Certain technical content, interview questions, and datasets are curated from external educational sources to provide a centralized learning resource. Respect for original authorship is maintained; no copyright infringement is intended. All trademarks, logos, and brand names are the property of their respective owners.

System Operational

© 2026 Driptanil Datta. All rights reserved.