FastAPI with async SQLAlchemy 2 on aiosqlite, tested offline with per-test in-memory DBs: working versions (tested 2026-10-15)
A FastAPI app using AsyncSession and async_sessionmaker on aiosqlite, tested with httpx.AsyncClient over ASGITransport, overriding the session dependency with a fresh in-memory database for each test.
passed4 passed · 0 failed2026-10-15 11:53 UTC- Status
- passed
- Last passed
- 2026-10-15 11:53 UTC
- Tests
- 4 / 4
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("fastapi-async-sqlalchemy-aiosqlite-pytest") (install), or see them on GitHub.
fastapi==0.143.1 sqlalchemy==2.1.4 aiosqlite==0.22.1 httpx==0.28.1 pytest-asyncio==1.4.0 pytest==9.1.1 greenlet==3.2.4
from collections.abc import AsyncIterator
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel, ConfigDict
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
DATABASE_URL = "sqlite+aiosqlite:///./app.db"
class Base(DeclarativeBase):
pass
class Item(Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
price_cents: Mapped[int]
engine = create_async_engine(DATABASE_URL)
# expire_on_commit=False keeps attributes loaded after commit, so no implicit
# lazy load (which raises MissingGreenlet in async code) when building responses.
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_session() -> AsyncIterator[AsyncSession]:
async with SessionLocal() as session:
yield session
SessionDep = Annotated[AsyncSession, Depends(get_session)]
class ItemIn(BaseModel):
name: str
price_cents: int
class ItemOut(ItemIn):
model_config = ConfigDict(from_attributes=True)
id: int
app = FastAPI()
@app.post("/items", response_model=ItemOut, status_code=201)
async def create_item(data: ItemIn, session: SessionDep):
item = Item(**data.model_dump())
session.add(item)
await session.commit()
return ItemOut.model_validate(item)
@app.get("/items/{item_id}", response_model=ItemOut)
async def read_item(item_id: int, session: SessionDep):
item = await session.get(Item, item_id)
if item is None:
raise HTTPException(status_code=404, detail="item not found")
return ItemOut.model_validate(item)
import httpx
import pytest_asyncio
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app import Base, app, get_session
@pytest_asyncio.fixture
async def client():
# A fresh in-memory database per test. StaticPool forces a single shared
# connection, because each new sqlite :memory: connection is its own empty DB.
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
poolclass=StaticPool,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
maker = async_sessionmaker(engine, expire_on_commit=False)
async def override_get_session():
async with maker() as session:
yield session
app.dependency_overrides[get_session] = override_get_session
# ASGITransport does not run the app lifespan, so do not rely on startup hooks.
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
yield c
app.dependency_overrides.clear()
await engine.dispose()
async def test_create_and_fetch(client):
r = await client.post("/items", json={"name": "pen", "price_cents": 150})
assert r.status_code == 201
item_id = r.json()["id"]
r = await client.get(f"/items/{item_id}")
assert r.json() == {"id": item_id, "name": "pen", "price_cents": 150}
async def test_missing_item_is_404(client):
r = await client.get("/items/999")
assert r.status_code == 404
async def test_invalid_payload_is_422(client):
r = await client.post("/items", json={"name": "pen", "price_cents": "lots"})
assert r.status_code == 422
async def test_each_test_gets_fresh_db(client):
# With a fresh database per test, the first insert always gets id 1.
r = await client.post("/items", json={"name": "cup", "price_cents": 900})
assert r.json()["id"] == 1
[pytest] pythonpath = . asyncio_mode = auto asyncio_default_fixture_loop_scope = function
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Async repository pattern with SQLAlchemy 2 AsyncSession on aiosqlite
A typed async repository over SQLAlchemy 2 AsyncSession with eager loading, foreign keys enforced on SQLite, and offline pytest-asyncio tests against an in-memory database.
SQLAlchemy 2 async ORM + aiosqlite with pytest-asyncio
SQLAlchemy 2.x typed async ORM (Mapped, mapped_column, async_sessionmaker, selectinload) on aiosqlite, with pytest-asyncio fixtures.
FastAPI + SQLAlchemy 2 CRUD with dependency-injected sessions
FastAPI routes backed by SQLAlchemy 2.x typed models, a per-request session dependency, and TestClient tests on SQLite.
FastAPI config via pydantic-settings as a cached dependency
Load config from env vars with pydantic-settings, inject it into FastAPI routes through a cached dependency, and test it offline with monkeypatch and dependency_overrides.
All FastAPI recipesAll SQLAlchemy recipesAll aiosqlite recipesAll httpx recipesAll pytest-asyncio recipes