Files
prompts/docs/skills/async-fastapi-sqlmodel/references/fastapi.md
T
2026-08-06 23:17:50 -05:00

7.7 KiB

FastAPI Database Integration

!!! info "Primary sources" - FastAPI lifespan events - FastAPI dependencies with yield - FastAPI dependency overrides - SQLAlchemy asyncio extension - nicegui-db application lifespan - nicegui-db database dependencies


Purpose

Connect the framework-independent database tools to FastAPI:

  • lifespan enters one application-owned database_scope(),
  • application state holds settings and 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, session management, and transaction boundaries.


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:

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

from fastapi import FastAPI

from .config import Settings
from .config import get_database_url
from .db import database_scope


@asynccontextmanager
async def lifespan(settings: Settings, app: FastAPI) -> AsyncGenerator[None]:
    app.state.settings = settings
    db_url = get_database_url(settings)

    try:
        async with database_scope(db_url) as session_factory:
            app.state.session_factory = session_factory
            yield
    finally:
        del app.state.settings
        del app.state.session_factory

The application factory binds settings to lifespan, for example with partial(lifespan, settings). 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 and clear cached engine resolution during shutdown.

The template's unconditional del app.state.session_factory mirrors an expected successful startup. If database_scope() raises before assignment, cleanup can raise AttributeError and obscure the startup error. A production hardening option is to assign a sentinel before the try or delete conditionally; that changes failure behavior and is not part of the exact template mechanics.

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:

from typing import Annotated

from fastapi import Depends
from fastapi import Request

from .session import SessionFactory


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:

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 owned_session:
        yield owned_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.


Route Usage

Read route:

@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:

@router.post("/items")
async def create_item(payload: ItemCreate, session: SessionDep) -> Item:
    async with session.begin():
        return await insert_item(session, payload)

The template exposes only SessionDep; it does not hide commit behavior in dependency teardown. Choose one visible write convention per application:

  • place async with session.begin(): around a complete write unit, which commits on success and rolls back on exception; or
  • call await session.commit() explicitly after all writes when the route is the complete unit, as the template's simple UI action does.

The context-manager form scales better to several statements and makes exception rollback visible. Direct commit() is concise but requires the route to preserve the single-commit invariant and handle any recovery needs. Do not combine both conventions in one route. Lower-level data-access functions receive the 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:

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.
app.dependency_overrides[_get_session] = get_test_session
try:
    yield app
finally:
    app.dependency_overrides.pop(_get_session, None)

See database testing 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.
  • Assuming SessionDep commits when dependency cleanup runs.
  • 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.
  • The session dependency owns request session closure but not commit behavior.
  • 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.