# FastAPI Database Integration !!! info "Primary sources" - [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/) - [FastAPI dependencies with `yield`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/) - [FastAPI dependency overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/) - [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html) --- ## Purpose Connect the framework-independent database tools to FastAPI: - lifespan enters one application-owned `database_scope()`, - application state holds the resulting session factory, - dependencies create one session per request, - `Annotated` aliases make route ownership concise and explicit. The underlying resource and transaction rules remain in [engine lifecycle](engine.md), [session management](session.md), and [transaction boundaries](transactions.md). --- ## Lifespan Ownership Enter `database_scope()` once for the complete application lifecycle. Store the session factory, not the engine, because request code needs sessions rather than direct pool access: ```python from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from fastapi import FastAPI from .engine import database_scope @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None]: database_url = app.state.settings.database_url async with database_scope(database_url) as session_factory: app.state.session_factory = session_factory try: yield finally: del app.state.session_factory app = FastAPI(lifespan=lifespan) ``` Lifespan does not construct resources per request. It enters the same framework-independent scope used by scripts, workers, and tests, keeps that scope open while requests are served, and lets it dispose the engine during shutdown. Only store the engine too when application-level code genuinely needs direct Core operations, pool instrumentation, or engine-specific diagnostics. Routes and repositories should normally receive an `AsyncSession`. --- ## Session Factory Dependency A synchronous dependency retrieves the already-created factory from application state: ```python from typing import Annotated from fastapi import Depends from fastapi import Request from .session import SessionFactory from .session import transaction_scope def get_session_factory(request: Request) -> SessionFactory: return request.app.state.session_factory type SessionFactoryDep = Annotated[SessionFactory, Depends(get_session_factory)] ``` `Depends()` does not create or cache a factory here. It only exposes the lifespan-owned object. This function is also the narrow seam that tests can override when they need a different factory. --- ## Request Session Dependencies Use a session-only dependency for reads and other request conversations that must not commit implicitly: ```python from collections.abc import AsyncGenerator from sqlmodel.ext.asyncio.session import AsyncSession async def get_session(session_factory: SessionFactoryDep) -> AsyncGenerator[AsyncSession]: async with session_factory() as session: yield session type SessionDep = Annotated[AsyncSession, Depends(get_session)] ``` The dependency creates and closes one session per request. Closing rolls back any unfinished autobegun transaction; it does not commit. Use a separate dependency when the whole route is one write transaction: ```python async def get_transaction_session(session_factory: SessionFactoryDep) -> AsyncGenerator[AsyncSession]: async with transaction_scope(session_factory) as session: yield session type TransactionSessionDep = Annotated[AsyncSession, Depends(get_transaction_session)] ``` This adapter uses `transaction_scope()` from [session management](session.md), so the same root transaction ownership applies inside and outside FastAPI. Successful dependency exit commits and closes the session. Exceptional exit rolls back and closes it. Route and service code using `TransactionSessionDep` must not call `commit()`, `rollback()`, or `close()`. --- ## Route Usage Read route: ```python @router.get("/items/{item_id}") async def get_item(item_id: int, session: SessionDep) -> Item | None: return await find_item(session, item_id) ``` Write route: ```python @router.post("/items") async def create_item(payload: ItemCreate, session: TransactionSessionDep) -> Item: return await insert_item(session, payload) ``` Choose one write convention per application: - inject `TransactionSessionDep` when the route itself is the complete transaction boundary, or - inject `SessionDep` and place `async with session.begin():` visibly around the service call. Do not combine both conventions in one route. Lower-level data-access functions continue to require an existing session and remain unaware of FastAPI. --- ## Background Work A request session belongs to that request and must not be retained by a background task. Inject or otherwise provide the application session factory, then create a new session inside the task: ```python async def run_background_job(session_factory: SessionFactory) -> None: async with session_factory.begin() as session: await process_pending_items(session) ``` If work must survive application shutdown, it needs an independently owned worker lifecycle rather than the FastAPI lifespan-owned factory. --- ## Testing and Overrides Override the narrow dependency that matches the test objective: - Override `get_session_factory` to preserve production request-session behavior with a test factory. - Override `get_session` when a test must inject one transaction-scoped session directly. - Verify each lifespan receives a fresh engine and session factory and removes application state during teardown. - Remove overrides during teardown so mutable application state does not leak between tests. ```python app.dependency_overrides[get_session] = get_test_session try: yield app finally: app.dependency_overrides.pop(get_session, None) ``` See [database testing](testing.md) for outer transactions, SAVEPOINT-backed fixtures, and database target selection. --- ## Anti-Patterns - Creating an engine or session factory in a request dependency. - Reading settings and constructing database resources from repositories. - Storing one mutable `AsyncSession` on `app.state`. - Sharing a request session with concurrent or background tasks. - Calling `commit()` inside a route that uses `TransactionSessionDep`. - Keeping `app.state.session_factory` after its `database_scope()` exits. - Using deprecated startup and shutdown event handlers alongside lifespan. --- ## Integration Checklist - Lifespan enters exactly one `database_scope()` for each application lifecycle. - Application state stores the yielded session factory. - Session dependencies create and close one session per request. - Read and transactional dependencies have distinct commit semantics. - Routes use `Annotated` aliases and receive sessions, not engines. - Background tasks create their own sessions from a still-live factory. - Tests override and restore dependencies deterministically.