Copy this file into the root of your project as AGENTS.md.
AGENTS.md
Project
Async REST API built with FastAPI. PostgreSQL via SQLAlchemy 2.0 (async engine). Alembic for migrations. Auth via JWT. Background jobs via ARQ (Redis-backed). Containerised with Docker.
Commands
uv sync— install dependencies (uses uv, not pip)uv run uvicorn app.main:app --reload— dev server on http://localhost:8000uv run pytest— run testsuv run pytest -k <pattern>— run a subsetuv run ruff check .— lintuv run ruff format .— formatuv run mypy app/— type checkuv run alembic revision --autogenerate -m "<msg>"— create migrationuv run alembic upgrade head— apply migrations
Always run ruff check, mypy, and pytest before considering work done.
Code style
- Python 3.12+. Use modern syntax:
list[str]notList[str],str | NonenotOptional[str]. - Type-annotate every function signature. No untyped
**kwargs. - Use
pydanticv2 models for all request/response bodies. - Module structure mirrors domain:
app/users/,app/orders/, etc. Each module hasrouter.py,service.py,schemas.py,models.py. - Never import from
tests/in application code. - Logging via
structlog. Noprint().
Stack rules
Database
- All DB access through repository classes in
app/<module>/repository.py. Routers do not touch sessions directly. - Use the
async_sessiondependency for FastAPI handlers. - Every model has a
created_atandupdated_at(UTC, timezone-aware). - Migrations are reviewed before running on production. Never
--autogeneratewithout inspecting the diff. - Use
select()(2.0 style), not the legacy query API.
API design
- URLs are plural nouns:
/users,/orders/{id}. No verbs in paths. - 2xx for success, 4xx for client error, 5xx for server error. Map
IntegrityErrorto 409. - Pagination: cursor-based, never offset, for any list endpoint.
- Errors return
{ "detail": "...", "code": "snake_case_code" }.
Auth
- JWT in
Authorization: Bearer <token>header. Refresh tokens stored in DB with rotation. - Roles checked via
Depends(require_role("admin")). - Passwords hashed with
argon2.
Testing
- pytest with
pytest-asynciomode = "auto". - Use
httpx.AsyncClientagainst the app, not the real network. - One DB transaction per test, rolled back via fixture.
- Aim for ≥80% coverage on
services/andrepositories/. Routers covered by integration tests.
Before editing
- Read the relevant module's
schemas.pyandmodels.py. - Check
app/core/exceptions.pyfor the right exception to raise. - Look at
tests/conftest.pyfor available fixtures before creating new ones.
Constraints
- No synchronous DB calls. Everything is
async/await. - No global mutable state outside of the app factory.
- No bare
except:. Always catch a specific exception. - Do not add Celery — ARQ is the chosen background-jobs library.