A production-grade FastAPI starter, built around modern Python 3.13 idioms and the current best-of-breed toolchain.
Batteries included: async SQLAlchemy 2.0, Pydantic v2, structured logging, RFC 7807 errors, layered architecture, full test setup, multi-stage Docker, GitHub Actions, pre-commit, and
uvfor dependency management.
- Features
- Tech stack
- Project layout
- Quick start
- Configuration
- Development workflow
- Database migrations
- Testing
- Code quality
- Docker
- Production deployment
- Architecture
- Adding a new resource
- API conventions
- License
- FastAPI 0.115+ with the latest
Annotated[..., Depends(...)]dependency idioms. - Async SQLAlchemy 2.0 ORM with typed
Mapped[...]columns andasyncpgdriver. - Pydantic v2 for schemas and
pydantic-settingsfor environment-validated configuration. - Layered architecture —
api → services → repositories → models— so business logic stays out of HTTP handlers. - Versioned API under
/api/v1, ready to grow into/v2without breaking clients. - RFC 7807 Problem Details error responses through a global exception hierarchy.
- Structured logging with
structlog, request IDs propagated viacontextvars, JSON output in production. - Generic
BaseRepositoryfor CRUD plumbing, plus domain-specific repositories for richer queries. - Alembic wired for async migrations and the
src/layout, with auto-formatting hooks. - Pytest + httpx + ASGI lifespan test harness against an in-memory SQLite database.
uvfor dependency resolution, locking, and a fast venv-less workflow.- Ruff (lint + format) and mypy --strict enforced locally and in CI.
- Pre-commit hooks that match CI — no surprise failures on push.
- Multi-stage Dockerfile with non-root user and healthcheck; docker compose with Postgres healthcheck.
- GitHub Actions CI running lint, type check, tests, and a Docker build.
| Concern | Choice |
|---|---|
| Language | Python 3.13 |
| Web framework | FastAPI ≥ 0.115 |
| ASGI server | Uvicorn (standard) |
| ORM | SQLAlchemy 2.0 (async) |
| Database driver | asyncpg |
| Migrations | Alembic (async env) |
| Validation / settings | Pydantic v2 + pydantic-settings |
| Logging | structlog |
| Package manager | uv (replaces pip / poetry / pipenv) |
| Lint + format | Ruff |
| Type checker | mypy (strict mode) |
| Tests | pytest, pytest-asyncio, httpx |
| Container | Multi-stage Docker image on Python 3.13 |
fastapi-boilerplate/
├── .github/workflows/ci.yml # Lint, type check, test, Docker build
├── alembic/
│ ├── env.py # Async migration environment, src-aware
│ ├── script.py.mako # Modern Python 3.13 migration template
│ └── versions/ # Generated revision files
├── src/
│ └── app/
│ ├── main.py # FastAPI app factory + ASGI export
│ ├── api/
│ │ ├── deps.py # Reusable Annotated[Depends(...)] aliases
│ │ ├── router.py # Top-level /api router
│ │ └── v1/
│ │ ├── router.py # v1 sub-router aggregator
│ │ └── endpoints/ # health.py, users.py, …
│ ├── core/
│ │ ├── config.py # pydantic-settings Settings class
│ │ ├── logging.py # structlog configuration
│ │ ├── exceptions.py # AppException hierarchy
│ │ ├── exception_handlers.py # FastAPI handlers → ProblemDetail
│ │ ├── middleware.py # Request-id / access-log middleware
│ │ └── lifespan.py # Startup/shutdown lifecycle
│ ├── db/
│ │ ├── base.py # DeclarativeBase + naming convention
│ │ ├── mixins.py # TimestampMixin
│ │ └── session.py # Async engine + sessionmaker + get_session
│ ├── models/ # SQLAlchemy ORM models
│ ├── repositories/ # Data access (CRUD + queries)
│ ├── schemas/ # Pydantic v2 request/response models
│ └── services/ # Business logic
├── tests/
│ ├── conftest.py # AsyncClient + ephemeral DB fixtures
│ ├── unit/ # Fast, isolated tests
│ └── integration/ # End-to-end tests against the ASGI app
├── .env.example # Documented environment variables
├── .pre-commit-config.yaml
├── .python-version # 3.13
├── Dockerfile # Multi-stage build with uv
├── docker-compose.yml # API + Postgres with healthchecks
├── Makefile # Common developer commands
├── alembic.ini
├── pyproject.toml # Single source of truth for tooling
└── README.md
- Docker (for the bundled Postgres) — everything else is installed by
make setup. - Optional: a system Python ≥ 3.11 to bootstrap
uv(any modern distro qualifies).
git clone <your-repo-url>
cd fastapi-boilerplate
make setup # installs uv + Python 3.13, creates .env, starts Postgres, installs deps, runs migrations
make dev # auto-reload server on http://localhost:8000make setup is idempotent — safe to re-run any time. Under the hood it composes these granular targets, each of which you can call on its own:
| Target | What it does |
|---|---|
setup-uv |
Installs uv if missing, then pins Python 3.13 via uv python install 3.13. |
setup-env |
Copies .env.example → .env if .env is absent. |
install |
uv sync --all-extras — resolves and installs everything into .venv. |
setup-db |
Starts the Postgres service via docker compose up -d db. |
wait-db |
Polls pg_isready until the container is accepting connections. |
migrate |
alembic upgrade head. |
reset |
Destructive. Wipes the Postgres volume and re-runs setup. |
doctor |
Prints versions of uv, Python, Docker, and Make — useful for bug reports. |
git clone <your-repo-url>
cd fastapi-boilerplate
# 1. uv + Python
curl -LsSf https://astral.sh/uv/install.sh | sh
uv python install 3.13
# 2. Dependencies
uv sync --all-extras
# 3. Environment
cp .env.example .env
$EDITOR .env
# 4. Postgres (or point .env at an existing instance)
docker compose up -d db
# 5. Migrations + run
uv run alembic upgrade head
uv run uvicorn app.main:app --reloadThe API is now live at http://localhost:8000:
- Swagger UI → http://localhost:8000/docs
- ReDoc → http://localhost:8000/redoc
- OpenAPI → http://localhost:8000/api/openapi.json
- Health → http://localhost:8000/api/v1/health
docker compose up --buildThis starts Postgres and the API with healthchecks. Migrations run automatically against the configured DB the first time you make migrate inside the container, or you can bake the call into a startup script.
All settings are validated at startup by Settings. See .env.example for the documented list. Highlights:
| Variable | Default | Purpose |
|---|---|---|
ENVIRONMENT |
local |
Switches docs/reload defaults |
DEBUG |
false |
FastAPI debug mode |
LOG_LEVEL / LOG_JSON |
INFO / false |
Structlog level + JSON renderer toggle |
API_PREFIX |
/api |
Mounts the versioned router prefix |
CORS_ORIGINS |
(empty) | Comma-separated list, or * |
DB_* |
local Postgres defaults | Async DSN, pool sizing, timeouts |
SECRET_KEY |
change me | For future auth / signing |
REQUEST_ID_HEADER |
X-Request-ID |
Header copied into structured logs |
local/development→/docsand/redocenabled, full SQL echo opt-in.production→ docs disabled, JSON logs recommended, generic 500 messages.test→ used by the pytest fixtures; auto-skips DB connectivity check at startup.
make install # uv sync --all-extras
make dev # uvicorn with --reload
make check # ruff format + ruff lint + mypy
make test # pytest
make test-cov # pytest with coverageuv run pre-commit install
uv run pre-commit run --all-filesHooks run the same ruff, ruff format, and mypy that CI runs, so green locally ⇒ green in CI.
Alembic is preconfigured for async PostgreSQL and the src/ layout.
make makemigration MSG="add posts table" # autogenerate from models
make migrate # apply migrations
make downgrade # roll back one revision
make db-revision # show current headMigration scripts are auto-formatted with ruff via the post_write_hooks in alembic.ini. Migration files in alembic/versions/ are not committed by default — set up your own policy for your team.
Tests run against an in-memory SQLite database via aiosqlite, so the suite is fast and hermetic.
make test
make test-cov
uv run pytest tests/unit -m unit # unit tests only
uv run pytest tests/integration -m integrationThe client fixture builds the FastAPI app with dependency_overrides[get_session] pointing at the test session, and drives it through httpx.AsyncClient + asgi-lifespan — no real network, full lifespan events.
All tooling is configured in pyproject.toml:
- Ruff runs a broad rule set (
E,W,F,I,B,C4,UP,N,ASYNC,S,SIM,RUF,PL,PT,RET,PTH,TCH, …) plus formatting. - mypy runs in strict mode with the Pydantic plugin enabled.
- pytest is configured with strict markers, strict config, and tracebacks trimmed for readability.
CI fails on any of: format drift, lint findings, type errors, or test failures.
The included Dockerfile is a multi-stage build:
- builder — uses the official
uvimage to resolve dependencies into a.venvdirectory. - runtime —
python:3.13-slim-bookworm, non-root user, healthcheck against/api/v1/health.
The accompanying docker-compose.yml brings up the API + a Postgres 17 service with proper healthcheck clauses and depends_on: condition: service_healthy.
make docker-build
make docker-up
make docker-logs
make docker-downA non-exhaustive checklist:
-
ENVIRONMENT=production -
LOG_JSON=trueand ship logs to your aggregator of choice -
DEBUG=false,DB_ECHO=false - Strong
SECRET_KEY(32+ random bytes) - Explicit
CORS_ORIGINSallowlist (no*in production) - Run behind a reverse proxy / TLS terminator (Caddy, nginx, ALB, …)
- Multiple workers (
API_WORKERS) and a process supervisor (gunicorn withuvicorn.workers.UvicornWorker, or justuvicorn --workers N) - Health probes wired to
/api/v1/health(liveness) and/api/v1/health/ready(readiness) - Database migrations applied as part of the deploy (
alembic upgrade head)
┌──────────────────────────────────────────────┐
│ HTTP / FastAPI router │
│ api/v1/endpoints/*.py (thin, declarative) │
└──────────────────────┬───────────────────────┘
│ Annotated[Depends(...)]
▼
┌──────────────────┐
│ Services │ ← business rules, invariants
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Repositories │ ← SQLAlchemy calls live here only
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Models (ORM) │
└──────────────────┘
- Endpoints stay thin — parse input, call a service, return a Pydantic model.
- Services own the business logic, raise
AppExceptionsubclasses, and never touch SQLAlchemy directly. - Repositories are the only place that builds SQL statements. Subclass
BaseRepositoryfor richer queries. - Models declare schema; Schemas declare the wire format. Never leak ORM types past the service boundary.
Cross-cutting:
core/middleware.pyattaches anX-Request-IDto every request, logs latency, and exposes it on responses.core/exception_handlers.pytranslates every exception flavour (HTTP, validation, integrity, app-domain) into a consistent JSON envelope.core/lifespan.pyverifies DB connectivity at startup and disposes the pool on shutdown.
- Model →
src/app/models/<resource>.py(inheritBase, TimestampMixin, export frommodels/__init__.py). - Schema →
src/app/schemas/<resource>.py(Create,Update,Publicflavours). - Repository → subclass
BaseRepository[Model]insrc/app/repositories/<resource>.py. - Service →
src/app/services/<resource>.py(raiseNotFoundError/ConflictErroras appropriate). - Dependencies → add
Annotated[..., Depends(...)]aliases inapi/deps.py. - Endpoints →
src/app/api/v1/endpoints/<resource>.py; wire intoapi/v1/router.py. - Migration →
make makemigration MSG="add <resource>"thenmake migrate. - Tests → add a
tests/integration/test_<resource>.py.
- Versioning:
/api/v1/.... Breaking changes ship under a new major version. - Pagination:
?page=1&size=20viaPageParams; responses use thePage[T]envelope withtotal,pages,has_next,has_prev. - Identifiers: UUIDs (preferred for external surfaces) — no exposed sequence IDs.
- Errors: RFC 7807-style payloads —
{type, title, status, code, detail, instance, errors?}. - Request IDs: clients may send
X-Request-ID; if absent, the server generates one and echoes it back. - Partial updates:
PATCHwithUpdateschemas (all fields optional).
Example error response:
{
"type": "about:blank#conflict",
"title": "Conflict",
"status": 409,
"code": "conflict",
"detail": "Username already taken.",
"instance": "http://localhost:8000/api/v1/users",
"errors": null
}MIT — see LICENSE.