Async FastAPI starter you can clone and bend into a test task in an afternoon. CRUD over a single User, but the wiring is the real point: clean layers, dependency injection, migrations, tests that actually run, and a Docker setup that comes up with one command.
Nothing fancy. Just the boring parts done right, so you don't redo them every time.
FastAPI and Uvicorn up front. SQLAlchemy 2.0 async with asyncpg underneath, Alembic for migrations, Pydantic v2 (and pydantic-settings) for schemas and config. Tests run on Pytest with pytest-asyncio and httpx. Postgres lives in Docker; the suite uses in-memory SQLite so it needs nothing running.
app/
├── api/
│ ├── deps.py # DI wiring: session → repository → service
│ └── v1/
│ ├── router.py
│ └── routes/
│ └── users.py # thin handlers, no logic
├── core/
│ ├── config.py # pydantic-settings, validated at startup
│ ├── database.py # async engine, session factory, Base
│ └── exceptions.py # domain errors
├── models/ # SQLAlchemy models
├── schemas/ # Pydantic request/response
├── repositories/ # data access, the only place that talks to the session
├── services/ # business logic, raises domain errors
└── main.py # app, router include, exception handlers
tests/
├── conftest.py # test DB + httpx client fixtures
└── test_users.py
alembic/ # env + versions
The flow is one direction: route → service → repository → DB. A handler never touches the session, a service never knows about HTTP. Swap Postgres for anything else and only database.py cares.
cp .env.example .env
docker compose up --buildPostgres comes up, the app waits for it to be healthy, migrations apply, then the server starts. Open http://localhost:8000/docs.
You'll need a Postgres running somewhere and a .env pointing at it.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
cp .env.example .env # edit DATABASE_URL if needed
alembic upgrade head
uvicorn app.main:app --reloadThe app talks to whatever DATABASE_URL points at, nothing else to change. The driver has to be async:
postgresql+asyncpg://user:pass@host:5432/dbname # Postgres
sqlite+aiosqlite:///./local.db # SQLite
Swap the URL in .env, run alembic upgrade head, done. Docker Compose already wires the app to its Postgres service. Tests are the one exception: they always spin up in-memory SQLite regardless of your .env, so the suite stays fast and self-contained.
No database needed. The suite spins up an in-memory SQLite per test and overrides the session dependency, so pytest just works.
pip install -e ".[dev]"
pytestAfter you change a model:
alembic revision --autogenerate -m "add something"
alembic upgrade headThe initial migration (0001) creates the users table, so a fresh database is one upgrade head away.
GET /api/v1/users list, skip/limit paging
POST /api/v1/users create, 409 on duplicate email
GET /api/v1/users/{id} fetch one, 404 if missing
PATCH /api/v1/users/{id} partial update
DELETE /api/v1/users/{id} delete, 204
GET /health liveness
Full schema lives at /docs once the app is up.
Copy models/user.py, schemas/user.py, repositories/user_repository.py, services/user_service.py, routes/users.py and rename. Each file is small and follows the same shape, so a new entity is mostly find-and-replace plus whatever logic the task actually wants. Add the model import to alembic/env.py, autogenerate, done.