Litestar app with advanced-alchemy and DTOs on SQLite: working versions (tested 2026-10-15)
A Litestar app wired to SQLAlchemy through the advanced-alchemy plugin, with separate read and write DTOs over aiosqlite, tested offline with litestar.testing TestClient.
passed4 passed · 0 failed2026-10-15 13:23 UTC- Status
- passed
- Last passed
- 2026-10-15 13:23 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 38741369695
The code
Exactly the files that passed. Your agent can pull them with get_recipe("litestar-dto-sqlalchemy") (install), or see them on GitHub.
litestar==2.24.0 advanced-alchemy==1.11.0 aiosqlite==0.22.1 pytest==9.1.1
from advanced_alchemy.base import BigIntAuditBase
from advanced_alchemy.extensions.litestar import (
SQLAlchemyAsyncConfig,
SQLAlchemyDTO,
SQLAlchemyDTOConfig,
SQLAlchemyPlugin,
)
from litestar import Litestar, get, post
from litestar.exceptions import NotFoundException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column
class Author(BigIntAuditBase):
__tablename__ = "author"
name: Mapped[str]
email: Mapped[str] = mapped_column(unique=True)
class AuthorWriteDTO(SQLAlchemyDTO[Author]):
# Clients may not set server-managed fields.
config = SQLAlchemyDTOConfig(exclude={"id", "created_at", "updated_at"})
class AuthorReadDTO(SQLAlchemyDTO[Author]):
config = SQLAlchemyDTOConfig(exclude={"created_at", "updated_at"})
@post("/authors", dto=AuthorWriteDTO, return_dto=AuthorReadDTO, status_code=201)
async def create_author(data: Author, db_session: AsyncSession) -> Author:
db_session.add(data)
# Commit explicitly so the row is visible to other requests. Relying on the
# plugin's commit timing is fragile across versions and response paths.
await db_session.commit()
# Reload server-managed columns (id, timestamps) that the commit expired,
# so the response DTO does not trigger a lazy load in async code.
await db_session.refresh(data)
return data
@get("/authors/{author_id:int}", return_dto=AuthorReadDTO)
async def get_author(author_id: int, db_session: AsyncSession) -> Author:
author = await db_session.get(Author, author_id)
if author is None:
raise NotFoundException(f"author {author_id} not found")
return author
def create_app(url: str) -> Litestar:
# create_all runs on the app lifespan, so TestClient must be used as a context manager.
db = SQLAlchemyAsyncConfig(connection_string=url, create_all=True)
return Litestar(
route_handlers=[create_author, get_author],
plugins=[SQLAlchemyPlugin(config=db)],
)
import pytest
from litestar.testing import TestClient
from app import create_app
@pytest.fixture
def client(tmp_path):
# A file-backed SQLite DB: each pooled connection to ":memory:" would see its own empty database.
app = create_app(f"sqlite+aiosqlite:///{tmp_path / 'test.db'}")
# The context manager runs the lifespan, which creates the tables.
with TestClient(app) as c:
yield c
def test_create_returns_201_without_audit_fields(client):
r = client.post("/authors", json={"name": "Ada", "email": "ada@example.com"})
assert r.status_code == 201
body = r.json()
assert body["id"] and body["name"] == "Ada"
assert "created_at" not in body and "updated_at" not in body
def test_created_author_is_visible_to_later_requests(client):
# A new request gets a new session, so this only passes if the create was committed.
new_id = client.post("/authors", json={"name": "Grace", "email": "g@example.com"}).json()["id"]
r = client.get(f"/authors/{new_id}")
assert r.status_code == 200
assert r.json()["email"] == "g@example.com"
def test_missing_field_is_400(client):
r = client.post("/authors", json={"name": "No Email"})
assert r.status_code == 400
def test_unknown_id_is_404(client):
assert client.get("/authors/999").status_code == 404
[pytest] pythonpath = .
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.
FastAPI with async SQLAlchemy 2 on aiosqlite, tested offline with per-test in-memory DBs
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.
Litestar app with DI, role guards and exception handlers
A Litestar app with a dependency-injected repository, a role guard on DELETE, and a custom exception handler, tested offline with create_test_client and an in-memory fake.
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.
All litestar recipesAll advanced-alchemy recipesAll aiosqlite recipes