Scaffold async database connection#3
Conversation
|
|
||
| load_dotenv() | ||
|
|
||
| DATABASE_URL = os.environ["DATABASE_URL"] |
There was a problem hiding this comment.
DATABASE_URL is missing from the environment, this will raise a cryptic KeyError.
For a better developer experience, consider using .get() with a helpful error message:
| DATABASE_URL = os.environ["DATABASE_URL"] | |
| 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." | |
| ) |
This gives a clearer error message that points developers to the solution.
| _session_factory = async_sessionmaker(engine, expire_on_commit=False) | ||
|
|
||
|
|
||
| async def get_session() -> AsyncGenerator[AsyncSession]: |
There was a problem hiding this comment.
📝 Type annotation: The AsyncGenerator type needs two parameters: the yield type and the send type.
| 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
Nonewhen.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.
There was a problem hiding this comment.
in Python 3.13 the None second argument is the default, so writing it out is redundant
|
|
||
| engine = create_async_engine(DATABASE_URL, echo=False) | ||
|
|
||
| _session_factory = async_sessionmaker(engine, expire_on_commit=False) |
There was a problem hiding this comment.
💡 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.
|
|
||
| DATABASE_URL = os.environ["DATABASE_URL"] | ||
|
|
||
| engine = create_async_engine(DATABASE_URL, echo=False) |
There was a problem hiding this comment.
💡 Dev tip: echo=False is the right default for production, but during development you might want echo=True to see the actual SQL queries being executed.
You could make this configurable:
engine = create_async_engine(
DATABASE_URL,
echo=os.environ.get("ENVIRONMENT") == "development"
)Seeing the SQL is incredibly helpful when debugging query performance or checking that your ORM code generates what you expect.
| app = FastAPI(lifespan=lifespan) | ||
|
|
||
|
|
||
| @app.get("/health") |
There was a problem hiding this comment.
💡 Consistency: For consistency with the rest of the codebase (which uses async throughout), consider making this async def:
| @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.
Covers async DB connection layer.
Changes
Why these packages
Key patterns