Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added backend/app/__init.py__
Empty file.
34 changes: 34 additions & 0 deletions backend/app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
from collections.abc import AsyncGenerator

from dotenv import load_dotenv
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlmodel import SQLModel

load_dotenv()

DATABASE_URL = os.environ.get("DATABASE_URL")
if not DATABASE_URL:
raise ValueError(
"DATABASE_URL environment variable is required. "
"See .env.example for setup instructions."
)

engine = create_async_engine(
DATABASE_URL, echo=os.environ.get("ENVIRONMENT") == "development"
)

# keeps objects usable after commit (relevant for async sessions)
_session_factory = async_sessionmaker(engine, expire_on_commit=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Learning note: expire_on_commit=False is important for async SQLAlchemy/SQLModel usage.

Why: By default, SQLAlchemy expires all objects after a commit, requiring a fresh query to access attributes. With async code, you can't lazily reload attributes after the session is closed (you'd need await). Setting this to False keeps objects usable after commit.

Tradeoff: You won't automatically see updates from other sessions, but that's fine for request-scoped sessions where each request gets a fresh session anyway.

Worth adding a brief comment here since it's a common async gotcha.



async def get_session() -> AsyncGenerator[AsyncSession]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Type annotation: The AsyncGenerator type needs two parameters: the yield type and the send type.

Suggested change
async def get_session() -> AsyncGenerator[AsyncSession]:
async def get_session() -> AsyncGenerator[AsyncSession, None]:

Why this matters: AsyncGenerator[AsyncSession, None] means:

  • Yields values of type AsyncSession
  • Accepts None when .send() is called (which FastAPI's dependency injection never does)

Pyright should catch this — if it doesn't, double-check your pyright config is active.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in Python 3.13 the None second argument is the default, so writing it out is redundant

"""FastAPI dependency — yields one session per request, closed on exit."""
async with _session_factory() as session:
yield session


async def create_db_and_tables() -> None:
"""Create every table registered in SQLModel.metadata (runs on startup)."""
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
20 changes: 20 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.database import create_db_and_tables


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
await create_db_and_tables()
yield


app = FastAPI(lifespan=lifespan)


@app.get("/health")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Consistency: For consistency with the rest of the codebase (which uses async throughout), consider making this async def:

Suggested change
@app.get("/health")
@app.get("/health")
async def health() -> dict[str, str]:

Why: While FastAPI handles sync functions fine by running them in a thread pool, using async def everywhere:

  • Makes the async pattern consistent across all endpoints
  • Avoids any thread pool overhead (tiny but exists)
  • Signals to future developers that this is an async codebase

Not required, but a good habit in an async-first project.

async def health() -> dict[str, str]:
return {"status": "ok"}
6 changes: 0 additions & 6 deletions backend/main.py

This file was deleted.

7 changes: 6 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
dependencies = [
"fastapi[standard]>=0.115",
"sqlmodel>=0.0.22",
"asyncpg>=0.30",
"python-dotenv>=1.0",
]
[tool.ruff]
target-version = "py313"
line-length = 88
Expand Down
Loading