Agents writing async SQLAlchemy reach for habits from sync code. They lazy-load a relationship inside an async route and hit MissingGreenlet, or share one AsyncSession across asyncio.gather. SQLAlchemy 2.1 adds a new trap: it no longer installs greenlet, so a project that declares plain sqlalchemy cannot start an async engine. This loadout sets the rules: a per-request session dependency, expire_on_commit=False, lazy="raise" on every relationship with selectinload for what the response needs, explicit commits with IntegrityError mapped to 409, and database URLs from the environment. The skill adds a resource end to end, with model, reviewed Alembic migration, router and async tests that assert the query count. The verifier runs the tests on SQLAlchemy 2.1.5 and FastAPI 0.143.3. It upgrades and downgrades the migration, confirms alembic check catches model drift, and shows the greenlet failure.
What's in it
- SQLAlchemy 2.1 needs sqlalchemy[asyncio]; the verifier shows the greenlet error
- lazy="raise" plus selectinload: the list test asserts exactly two SELECTs
- One AsyncSession per request, never shared across tasks
- Async Alembic env from DATABASE_URL; alembic check catches model drift
- httpx AsyncClient over ASGITransport with a fresh database per test
You'll need
- Tools:
uv,python3
Verification
| Check | Result | What we found |
|---|---|---|
| Listinglisting.yaml matches the schema and declares every file in the folder. | Pass | Schema valid; 10 files declared, none missing or extra
|
| Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. | Pass | All 10 files parse; front matter valid |
| AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. | Pass | CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
|
| Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. | Pass | Correct paths for all 6 agents; layouts re-discovered
|
| MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. | Not applicable | No MCP servers in this loadout |
| Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode or prompt-injection wording, and the listing's network flag is honest. | Pass | Clean: no credentials, risky shell or hidden text
|
| ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. | Pass | 1 reference between files resolves for every agent |
| Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. | Pass | 10 files, 15.3 KB total; largest skills/async-sqlalchemy-resource/references/test_customers.py 3.2 KB
|
| LicenceA recognised SPDX licence, with the licence text available. | Pass | MIT, by Agent Loadout; text in the repository LICENSE |
| Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. | Pass | The async-sqlalchemy-resource skill's model, router, migration and Alembic env drop into a FastAPI app on SQLAlchemy 2.1.5: five async tests pass (including a two-SELECT N+1 guard and lazy="raise"), ruff is clean, the migration upgrades, downgrades and matches the models, alembic check catches a dri
|
What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results ("tested with agent") are separate and stay untested until an agent actually runs.
Install
Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.
Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/AGENTS.md curl -fsSL --create-dirs -o CLAUDE.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/CLAUDE.md curl -fsSL --create-dirs -o .claude/skills/async-sqlalchemy-resource/SKILL.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/SKILL.md curl -fsSL --create-dirs -o .claude/skills/async-sqlalchemy-resource/references/db.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/db.py curl -fsSL --create-dirs -o .claude/skills/async-sqlalchemy-resource/references/models.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/models.py curl -fsSL --create-dirs -o .claude/skills/async-sqlalchemy-resource/references/customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/customers.py curl -fsSL --create-dirs -o .claude/skills/async-sqlalchemy-resource/references/0001_customers_orders.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/0001_customers_orders.py curl -fsSL --create-dirs -o .claude/skills/async-sqlalchemy-resource/references/alembic_env.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/alembic_env.py curl -fsSL --create-dirs -o .claude/skills/async-sqlalchemy-resource/references/test_customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/test_customers.py
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
CLAUDE.md | CLAUDE.md |
skills/async-sqlalchemy-resource/SKILL.md | .claude/skills/async-sqlalchemy-resource/SKILL.md |
skills/async-sqlalchemy-resource/references/db.py | .claude/skills/async-sqlalchemy-resource/references/db.py |
skills/async-sqlalchemy-resource/references/models.py | .claude/skills/async-sqlalchemy-resource/references/models.py |
skills/async-sqlalchemy-resource/references/customers.py | .claude/skills/async-sqlalchemy-resource/references/customers.py |
skills/async-sqlalchemy-resource/references/0001_customers_orders.py | .claude/skills/async-sqlalchemy-resource/references/0001_customers_orders.py |
skills/async-sqlalchemy-resource/references/alembic_env.py | .claude/skills/async-sqlalchemy-resource/references/alembic_env.py |
skills/async-sqlalchemy-resource/references/test_customers.py | .claude/skills/async-sqlalchemy-resource/references/test_customers.py |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.
Codex CLI reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/SKILL.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/SKILL.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/db.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/db.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/models.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/models.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/customers.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/0001_customers_orders.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/alembic_env.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/alembic_env.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/test_customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/test_customers.py
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/async-sqlalchemy-resource/SKILL.md | .agents/skills/async-sqlalchemy-resource/SKILL.md |
skills/async-sqlalchemy-resource/references/db.py | .agents/skills/async-sqlalchemy-resource/references/db.py |
skills/async-sqlalchemy-resource/references/models.py | .agents/skills/async-sqlalchemy-resource/references/models.py |
skills/async-sqlalchemy-resource/references/customers.py | .agents/skills/async-sqlalchemy-resource/references/customers.py |
skills/async-sqlalchemy-resource/references/0001_customers_orders.py | .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py |
skills/async-sqlalchemy-resource/references/alembic_env.py | .agents/skills/async-sqlalchemy-resource/references/alembic_env.py |
skills/async-sqlalchemy-resource/references/test_customers.py | .agents/skills/async-sqlalchemy-resource/references/test_customers.py |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.
Cursor reads AGENTS.md + .cursor/rules/*.mdc and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/SKILL.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/SKILL.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/db.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/db.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/models.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/models.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/customers.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/0001_customers_orders.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/alembic_env.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/alembic_env.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/test_customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/test_customers.py
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/async-sqlalchemy-resource/SKILL.md | .agents/skills/async-sqlalchemy-resource/SKILL.md |
skills/async-sqlalchemy-resource/references/db.py | .agents/skills/async-sqlalchemy-resource/references/db.py |
skills/async-sqlalchemy-resource/references/models.py | .agents/skills/async-sqlalchemy-resource/references/models.py |
skills/async-sqlalchemy-resource/references/customers.py | .agents/skills/async-sqlalchemy-resource/references/customers.py |
skills/async-sqlalchemy-resource/references/0001_customers_orders.py | .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py |
skills/async-sqlalchemy-resource/references/alembic_env.py | .agents/skills/async-sqlalchemy-resource/references/alembic_env.py |
skills/async-sqlalchemy-resource/references/test_customers.py | .agents/skills/async-sqlalchemy-resource/references/test_customers.py |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.
Gemini CLI reads GEMINI.md (imports AGENTS.md with @./AGENTS.md) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/AGENTS.md curl -fsSL --create-dirs -o GEMINI.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/GEMINI.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/SKILL.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/SKILL.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/db.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/db.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/models.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/models.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/customers.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/0001_customers_orders.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/alembic_env.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/alembic_env.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/test_customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/test_customers.py
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
GEMINI.md | GEMINI.md |
skills/async-sqlalchemy-resource/SKILL.md | .agents/skills/async-sqlalchemy-resource/SKILL.md |
skills/async-sqlalchemy-resource/references/db.py | .agents/skills/async-sqlalchemy-resource/references/db.py |
skills/async-sqlalchemy-resource/references/models.py | .agents/skills/async-sqlalchemy-resource/references/models.py |
skills/async-sqlalchemy-resource/references/customers.py | .agents/skills/async-sqlalchemy-resource/references/customers.py |
skills/async-sqlalchemy-resource/references/0001_customers_orders.py | .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py |
skills/async-sqlalchemy-resource/references/alembic_env.py | .agents/skills/async-sqlalchemy-resource/references/alembic_env.py |
skills/async-sqlalchemy-resource/references/test_customers.py | .agents/skills/async-sqlalchemy-resource/references/test_customers.py |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.
Antigravity CLI reads AGENTS.md (read natively; GEMINI.md is also read) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/SKILL.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/SKILL.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/db.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/db.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/models.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/models.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/customers.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/0001_customers_orders.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/alembic_env.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/alembic_env.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/test_customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/test_customers.py
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/async-sqlalchemy-resource/SKILL.md | .agents/skills/async-sqlalchemy-resource/SKILL.md |
skills/async-sqlalchemy-resource/references/db.py | .agents/skills/async-sqlalchemy-resource/references/db.py |
skills/async-sqlalchemy-resource/references/models.py | .agents/skills/async-sqlalchemy-resource/references/models.py |
skills/async-sqlalchemy-resource/references/customers.py | .agents/skills/async-sqlalchemy-resource/references/customers.py |
skills/async-sqlalchemy-resource/references/0001_customers_orders.py | .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py |
skills/async-sqlalchemy-resource/references/alembic_env.py | .agents/skills/async-sqlalchemy-resource/references/alembic_env.py |
skills/async-sqlalchemy-resource/references/test_customers.py | .agents/skills/async-sqlalchemy-resource/references/test_customers.py |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.
GitHub Copilot reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/SKILL.md https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/SKILL.md curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/db.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/db.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/models.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/models.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/customers.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/0001_customers_orders.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/alembic_env.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/alembic_env.py curl -fsSL --create-dirs -o .agents/skills/async-sqlalchemy-resource/references/test_customers.py https://loadout.jnw-it.com/raw/fastapi-async-sqlalchemy/1.0.0/skills/async-sqlalchemy-resource/references/test_customers.py
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/async-sqlalchemy-resource/SKILL.md | .agents/skills/async-sqlalchemy-resource/SKILL.md |
skills/async-sqlalchemy-resource/references/db.py | .agents/skills/async-sqlalchemy-resource/references/db.py |
skills/async-sqlalchemy-resource/references/models.py | .agents/skills/async-sqlalchemy-resource/references/models.py |
skills/async-sqlalchemy-resource/references/customers.py | .agents/skills/async-sqlalchemy-resource/references/customers.py |
skills/async-sqlalchemy-resource/references/0001_customers_orders.py | .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py |
skills/async-sqlalchemy-resource/references/alembic_env.py | .agents/skills/async-sqlalchemy-resource/references/alembic_env.py |
skills/async-sqlalchemy-resource/references/test_customers.py | .agents/skills/async-sqlalchemy-resource/references/test_customers.py |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.
Files
Rules AGENTS.md Rules for a FastAPI service on async SQLAlchemy 2.1 with Alembic and uv. 29 lines
# AGENTS.md: FastAPI with async SQLAlchemy
FastAPI service on SQLAlchemy 2.1's asyncio extension, with Alembic for migrations and uv for packages. SQLAlchemy 2.1
needs Python 3.11 or newer and no longer installs greenlet by default: declare `sqlalchemy[asyncio]`, or async
engines fail with an ImportError about greenlet.
## Commands
- `uv sync`; add packages with `uv add` (exact pins: `"sqlalchemy[asyncio]==2.1.5"`, `aiosqlite==0.22.1` or
`asyncpg==0.32.0`, `alembic==1.20.0`).
- Run: `uv run fastapi dev app/main.py`. Tests: `uv run pytest -q`. Lint: `uv run ruff check . && uv run ruff format --check .`.
- Migrations: `uv run alembic revision --autogenerate -m "..."`, review the file, then `uv run alembic upgrade head`
and `uv run alembic check`.
## Rules
- Async all the way: `create_async_engine`, `async_sessionmaker(engine, expire_on_commit=False)`, `AsyncSession`
via a `SessionDep` dependency. No sync `Session`, `requests` or blocking file I/O inside `async def` routes.
- One session per request. Never share an `AsyncSession` across tasks (`asyncio.gather` over one session is a bug).
- Relationships use `lazy="raise"`. Load what you return with `selectinload` or `joinedload`. A `MissingGreenlet`
error means a lazy load slipped through; fix the query, do not wrap it in `run_sync`.
- Commit explicitly in the route or service; roll back and map `IntegrityError` to 409.
- Return Pydantic response models, never ORM objects. Pydantic v2 only.
- Database URLs and passwords come from the environment (`DATABASE_URL`), never `alembic.ini` or code.
- Dispose of the engine in the app's `lifespan` shutdown.
- Never edit a migration that has run outside your machine. Add a new one.
## Tests
- Async tests through `httpx.AsyncClient` and `ASGITransport` with a throwaway database per test.
- List endpoints assert their query count. New resources: use the `async-sqlalchemy-resource` skill. Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
# CLAUDE.md
@AGENTS.md
## Claude Code notes
- Skills from this loadout live in `.claude/skills/`: `async-sqlalchemy-resource`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From the Agent Loadout "FastAPI with async SQLAlchemy 2.1 and Alembic" v1.0.0 — https://loadout.jnw-it.com/loadouts/fastapi-async-sqlalchemy/ --> Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
# GEMINI.md
@./AGENTS.md
## Gemini CLI notes
- Skills from this loadout live in `.agents/skills/`: `async-sqlalchemy-resource`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From the Agent Loadout "FastAPI with async SQLAlchemy 2.1 and Alembic" v1.0.0 — https://loadout.jnw-it.com/loadouts/fastapi-async-sqlalchemy/ --> Skill skills/async-sqlalchemy-resource/SKILL.md Model, migration, router and tests for a new resource. 60 lines
---
name: async-sqlalchemy-resource
description: Add a database-backed resource to a FastAPI app on async SQLAlchemy 2.1 (model, Alembic migration, router and async tests) without lazy-load errors or N+1 queries. Use when adding or changing tables, relationships or endpoints that read or write the database.
---
# Add an async SQLAlchemy resource
The references are a complete working set: `db.py` (engine, session factory, `SessionDep`), `models.py`,
`customers.py` (router), `0001_customers_orders.py` (migration), `alembic_env.py` (async Alembic env) and
`test_customers.py`.
## 1. Model
- Typed mappings: `Mapped[int]`, `mapped_column(...)`. `Mapped[str | None]` means nullable; plain `Mapped[str]` is
`NOT NULL`.
- Every relationship gets `lazy="raise"`. Under asyncio an implicit lazy load fails with `MissingGreenlet`, and in
sync code it is a hidden extra query. `raise` turns both into an immediate, obvious error.
- Index foreign keys you filter on (`index=True`). Use `ondelete=` on the foreign key, not ORM cascade alone.
## 2. Migration
```bash
uv run alembic revision --autogenerate -m "add orders"
```
Read the generated file before keeping it. Autogenerate misses renames (it emits drop plus add, which loses data),
some server defaults and enum changes. Then:
```bash
uv run alembic upgrade head
uv run alembic check # fails if models and migrations disagree
uv run alembic downgrade -1 && uv run alembic upgrade head # downgrade must work too
```
The env reads `DATABASE_URL` from the environment. Never write a URL with a password into `alembic.ini`.
## 3. Router
- Take the session as `session: SessionDep`. One session per request. Never store it globally or share it between
`asyncio` tasks: an `AsyncSession` is not safe for concurrent use.
- Load what the response needs in the query: `selectinload(Customer.orders)` for collections, `joinedload` for
many-to-one. Use `await session.scalars(select(...))`, `await session.get(...)` and `await session.commit()`.
- `expire_on_commit=False` on the session factory keeps attributes readable after commit.
- Map `IntegrityError` to 409 after `await session.rollback()`. Return Pydantic models
(`ConfigDict(from_attributes=True)`), never ORM objects.
- Need a relationship on an object you already have? `await session.refresh(obj, ["orders"])`, or
`await obj.awaitable_attrs.orders` on relationships that are not `lazy="raise"`.
## 4. Tests
- `httpx.AsyncClient(transport=ASGITransport(app=app))` with `pytest.mark.anyio`, so tests run on the same event
loop as the app.
- A fresh `sqlite+aiosqlite://` engine with `StaticPool` per test, and `app.dependency_overrides[get_session]`.
- Count queries with an event listener on `engine.sync_engine` and assert the number of SELECTs for list endpoints.
That is the test that catches N+1 regressions.
## Done means
pytest, `ruff check`, `ruff format --check` and `alembic check` all pass, and the migration's downgrade works. Skill file skills/async-sqlalchemy-resource/references/db.py Async engine, session factory and SessionDep. 29 lines
"""Async engine, session factory and the per-request session dependency."""
import os
from collections.abc import AsyncIterator
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncAttrs, AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite+aiosqlite:///./app.db")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
# expire_on_commit=False: attributes stay loaded after commit, so building the
# response does not trigger a lazy load (which raises under asyncio).
SessionFactory = async_sessionmaker(engine, expire_on_commit=False)
class Base(AsyncAttrs, DeclarativeBase):
pass
async def get_session() -> AsyncIterator[AsyncSession]:
async with SessionFactory() as session:
yield session
SessionDep = Annotated[AsyncSession, Depends(get_session)] Skill file skills/async-sqlalchemy-resource/references/models.py Typed models with lazy="raise" relationships. 27 lines
"""Customer and Order. Relationships use lazy="raise" so a missed eager load fails loudly."""
from datetime import datetime
from sqlalchemy import ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
class Customer(Base):
__tablename__ = "customers"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(254), unique=True)
orders: Mapped[list["Order"]] = relationship(back_populates="customer", lazy="raise")
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id", ondelete="CASCADE"), index=True)
total_pence: Mapped[int]
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
customer: Mapped[Customer] = relationship(back_populates="orders", lazy="raise") Skill file skills/async-sqlalchemy-resource/references/customers.py Router with eager loads, commits and 409/404 handling. 64 lines
"""Customers router: async session per request, eager loads, explicit commits."""
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import selectinload
from app.db import SessionDep
from app.models import Customer, Order
router = APIRouter(prefix="/customers", tags=["customers"])
class OrderOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
total_pence: int
class CustomerIn(BaseModel):
email: EmailStr
class CustomerOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
orders: list[OrderOut]
class OrderIn(BaseModel):
total_pence: int = Field(gt=0, le=10_000_000)
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_customer(body: CustomerIn, session: SessionDep) -> CustomerOut:
customer = Customer(email=body.email, orders=[])
session.add(customer)
try:
await session.commit()
except IntegrityError:
await session.rollback()
raise HTTPException(status.HTTP_409_CONFLICT, "email already registered") from None
return CustomerOut.model_validate(customer)
@router.get("")
async def list_customers(session: SessionDep, limit: int = 50) -> list[CustomerOut]:
# One query for customers and one for all their orders, however many rows.
stmt = select(Customer).options(selectinload(Customer.orders)).order_by(Customer.id).limit(min(limit, 200))
customers = (await session.scalars(stmt)).all()
return [CustomerOut.model_validate(c) for c in customers]
@router.post("/{customer_id}/orders", status_code=status.HTTP_201_CREATED)
async def add_order(customer_id: int, body: OrderIn, session: SessionDep) -> OrderOut:
if await session.get(Customer, customer_id) is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "customer not found")
order = Order(customer_id=customer_id, total_pence=body.total_pence)
session.add(order)
await session.commit()
return OrderOut.model_validate(order) Skill file skills/async-sqlalchemy-resource/references/0001_customers_orders.py Reviewed Alembic migration with a working downgrade. 41 lines
"""customers and orders
Revision ID: 0001
Revises:
Create Date: 2026-10-17
"""
import sqlalchemy as sa
from alembic import op
revision = "0001"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"customers",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("email", sa.String(length=254), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
)
op.create_table(
"orders",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("customer_id", sa.Integer(), nullable=False),
sa.Column("total_pence", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
sa.ForeignKeyConstraint(["customer_id"], ["customers.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_orders_customer_id", "orders", ["customer_id"])
def downgrade() -> None:
op.drop_index("ix_orders_customer_id", table_name="orders")
op.drop_table("orders")
op.drop_table("customers") Skill file skills/async-sqlalchemy-resource/references/alembic_env.py Async Alembic env.py reading DATABASE_URL. 45 lines
"""Alembic env.py for an async engine (from `alembic init -t async`), wired to the app's models.
The URL comes from DATABASE_URL, never from alembic.ini, so no credentials are committed.
"""
import asyncio
import os
from alembic import context
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import create_async_engine
import app.models # noqa: F401 (registers every model on Base.metadata)
from app.db import Base
target_metadata = Base.metadata
URL = os.environ.get("DATABASE_URL", "sqlite+aiosqlite:///./app.db")
def run_migrations_offline() -> None:
context.configure(url=URL, target_metadata=target_metadata, literal_binds=True, render_as_batch=True)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
# render_as_batch lets ALTER-style changes work on SQLite; harmless elsewhere.
context.configure(connection=connection, target_metadata=target_metadata, render_as_batch=True)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online() -> None:
engine = create_async_engine(URL, poolclass=pool.NullPool)
async with engine.connect() as connection:
await connection.run_sync(do_run_migrations)
await engine.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online()) Skill file skills/async-sqlalchemy-resource/references/test_customers.py Async tests with a query-count N+1 guard. 88 lines
"""Async API tests: httpx.AsyncClient over ASGITransport, a fresh in-memory database
per test, and a query counter that catches N+1 loads."""
from collections.abc import AsyncIterator
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import event, select
from sqlalchemy.exc import InvalidRequestError
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from app.db import Base, get_session
from app.main import app
from app.models import Customer
pytestmark = pytest.mark.anyio
@pytest.fixture
def anyio_backend() -> str:
return "asyncio"
@pytest.fixture
async def db() -> AsyncIterator[dict]:
engine = create_async_engine("sqlite+aiosqlite://", poolclass=StaticPool)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
factory = async_sessionmaker(engine, expire_on_commit=False)
queries: list[str] = []
event.listen(engine.sync_engine, "before_cursor_execute", lambda *a: queries.append(a[2]))
async def override() -> AsyncIterator:
async with factory() as session:
yield session
app.dependency_overrides[get_session] = override
yield {"factory": factory, "queries": queries}
app.dependency_overrides.clear()
await engine.dispose()
@pytest.fixture
async def client(db: dict) -> AsyncIterator[AsyncClient]:
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
async def test_create_and_list_without_n_plus_one(client: AsyncClient, db: dict) -> None:
for i in range(5):
r = await client.post("/customers", json={"email": f"c{i}@example.com"})
assert r.status_code == 201
await client.post(f"/customers/{r.json()['id']}/orders", json={"total_pence": 1000 + i})
db["queries"].clear()
r = await client.get("/customers")
assert r.status_code == 200
assert [len(c["orders"]) for c in r.json()] == [1, 1, 1, 1, 1]
selects = [q for q in db["queries"] if q.lstrip().upper().startswith("SELECT")]
assert len(selects) == 2, selects
async def test_duplicate_email_is_409(client: AsyncClient) -> None:
assert (await client.post("/customers", json={"email": "a@example.com"})).status_code == 201
r = await client.post("/customers", json={"email": "a@example.com"})
assert r.status_code == 409
async def test_order_for_missing_customer_is_404(client: AsyncClient) -> None:
r = await client.post("/customers/999/orders", json={"total_pence": 500})
assert r.status_code == 404
async def test_bad_total_is_422(client: AsyncClient) -> None:
r = await client.post("/customers", json={"email": "b@example.com"})
bad = await client.post(f"/customers/{r.json()['id']}/orders", json={"total_pence": 0})
assert bad.status_code == 422
async def test_lazy_load_raises_instead_of_hiding_a_query(client: AsyncClient, db: dict) -> None:
await client.post("/customers", json={"email": "lazy@example.com"})
async with db["factory"]() as session:
customer = (await session.scalars(select(Customer))).one()
with pytest.raises(InvalidRequestError):
_ = customer.orders
await session.refresh(customer, ["orders"])
assert customer.orders == [] All files: source zip · Licence MIT · By Agent Loadout · First listed
Credits
Public work that inspired this loadout. The wording here is our own; follow the links for the originals.
Installing and testing
How do I install FastAPI with async SQLAlchemy 2.1 and Alembic in Claude Code?
From your project root, put AGENTS.md, CLAUDE.md, skills/async-sqlalchemy-resource/SKILL.md at .claude/skills/async-sqlalchemy-resource/SKILL.md, skills/async-sqlalchemy-resource/references/db.py at .claude/skills/async-sqlalchemy-resource/references/db.py, skills/async-sqlalchemy-resource/references/models.py at .claude/skills/async-sqlalchemy-resource/references/models.py, skills/async-sqlalchemy-resource/references/customers.py at .claude/skills/async-sqlalchemy-resource/references/customers.py, skills/async-sqlalchemy-resource/references/0001_customers_orders.py at .claude/skills/async-sqlalchemy-resource/references/0001_customers_orders.py, skills/async-sqlalchemy-resource/references/alembic_env.py at .claude/skills/async-sqlalchemy-resource/references/alembic_env.py, skills/async-sqlalchemy-resource/references/test_customers.py at .claude/skills/async-sqlalchemy-resource/references/test_customers.py. The "Claude Code" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/fastapi-async-sqlalchemy/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI with async SQLAlchemy 2.1 and Alembic in Codex CLI?
From your project root, put AGENTS.md, skills/async-sqlalchemy-resource/SKILL.md at .agents/skills/async-sqlalchemy-resource/SKILL.md, skills/async-sqlalchemy-resource/references/db.py at .agents/skills/async-sqlalchemy-resource/references/db.py, skills/async-sqlalchemy-resource/references/models.py at .agents/skills/async-sqlalchemy-resource/references/models.py, skills/async-sqlalchemy-resource/references/customers.py at .agents/skills/async-sqlalchemy-resource/references/customers.py, skills/async-sqlalchemy-resource/references/0001_customers_orders.py at .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py, skills/async-sqlalchemy-resource/references/alembic_env.py at .agents/skills/async-sqlalchemy-resource/references/alembic_env.py, skills/async-sqlalchemy-resource/references/test_customers.py at .agents/skills/async-sqlalchemy-resource/references/test_customers.py. The "Codex CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/fastapi-async-sqlalchemy/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI with async SQLAlchemy 2.1 and Alembic in Cursor?
From your project root, put AGENTS.md, skills/async-sqlalchemy-resource/SKILL.md at .agents/skills/async-sqlalchemy-resource/SKILL.md, skills/async-sqlalchemy-resource/references/db.py at .agents/skills/async-sqlalchemy-resource/references/db.py, skills/async-sqlalchemy-resource/references/models.py at .agents/skills/async-sqlalchemy-resource/references/models.py, skills/async-sqlalchemy-resource/references/customers.py at .agents/skills/async-sqlalchemy-resource/references/customers.py, skills/async-sqlalchemy-resource/references/0001_customers_orders.py at .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py, skills/async-sqlalchemy-resource/references/alembic_env.py at .agents/skills/async-sqlalchemy-resource/references/alembic_env.py, skills/async-sqlalchemy-resource/references/test_customers.py at .agents/skills/async-sqlalchemy-resource/references/test_customers.py. The "Cursor" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/fastapi-async-sqlalchemy/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI with async SQLAlchemy 2.1 and Alembic in Gemini CLI?
From your project root, put AGENTS.md, GEMINI.md, skills/async-sqlalchemy-resource/SKILL.md at .agents/skills/async-sqlalchemy-resource/SKILL.md, skills/async-sqlalchemy-resource/references/db.py at .agents/skills/async-sqlalchemy-resource/references/db.py, skills/async-sqlalchemy-resource/references/models.py at .agents/skills/async-sqlalchemy-resource/references/models.py, skills/async-sqlalchemy-resource/references/customers.py at .agents/skills/async-sqlalchemy-resource/references/customers.py, skills/async-sqlalchemy-resource/references/0001_customers_orders.py at .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py, skills/async-sqlalchemy-resource/references/alembic_env.py at .agents/skills/async-sqlalchemy-resource/references/alembic_env.py, skills/async-sqlalchemy-resource/references/test_customers.py at .agents/skills/async-sqlalchemy-resource/references/test_customers.py. The "Gemini CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/fastapi-async-sqlalchemy/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI with async SQLAlchemy 2.1 and Alembic in Antigravity CLI?
From your project root, put AGENTS.md, skills/async-sqlalchemy-resource/SKILL.md at .agents/skills/async-sqlalchemy-resource/SKILL.md, skills/async-sqlalchemy-resource/references/db.py at .agents/skills/async-sqlalchemy-resource/references/db.py, skills/async-sqlalchemy-resource/references/models.py at .agents/skills/async-sqlalchemy-resource/references/models.py, skills/async-sqlalchemy-resource/references/customers.py at .agents/skills/async-sqlalchemy-resource/references/customers.py, skills/async-sqlalchemy-resource/references/0001_customers_orders.py at .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py, skills/async-sqlalchemy-resource/references/alembic_env.py at .agents/skills/async-sqlalchemy-resource/references/alembic_env.py, skills/async-sqlalchemy-resource/references/test_customers.py at .agents/skills/async-sqlalchemy-resource/references/test_customers.py. The "Antigravity CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/fastapi-async-sqlalchemy/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI with async SQLAlchemy 2.1 and Alembic in GitHub Copilot?
From your project root, put AGENTS.md, skills/async-sqlalchemy-resource/SKILL.md at .agents/skills/async-sqlalchemy-resource/SKILL.md, skills/async-sqlalchemy-resource/references/db.py at .agents/skills/async-sqlalchemy-resource/references/db.py, skills/async-sqlalchemy-resource/references/models.py at .agents/skills/async-sqlalchemy-resource/references/models.py, skills/async-sqlalchemy-resource/references/customers.py at .agents/skills/async-sqlalchemy-resource/references/customers.py, skills/async-sqlalchemy-resource/references/0001_customers_orders.py at .agents/skills/async-sqlalchemy-resource/references/0001_customers_orders.py, skills/async-sqlalchemy-resource/references/alembic_env.py at .agents/skills/async-sqlalchemy-resource/references/alembic_env.py, skills/async-sqlalchemy-resource/references/test_customers.py at .agents/skills/async-sqlalchemy-resource/references/test_customers.py. The "GitHub Copilot" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/fastapi-async-sqlalchemy/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
Has FastAPI with async SQLAlchemy 2.1 and Alembic been tested?
It passed the Level 1 automated install checks on 2026-10-17: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.
More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.