Batteries-included FastAPI starter with PostgreSQL, Redis, async SQLAlchemy, Alembic, structured logging, rate limiting, and full Docker setup. No features pre-built — just a clean, production-ready foundation to build on.
| Layer | Technology |
|---|---|
| Framework | FastAPI 0.115 + Python 3.12 |
| Database | PostgreSQL 16 via asyncpg + SQLAlchemy 2 async |
| Migrations | Alembic (async engine, autogenerate) |
| Cache / Rate limit | Redis 7 via redis-py async |
| Logging | structlog (JSON in prod, colored in dev) |
| Testing | pytest-asyncio + httpx ASGI client |
| CI | GitHub Actions |
fastapi-template/
├── app/
│ ├── api/
│ │ ├── deps.py # Shared FastAPI dependencies (DBDep)
│ │ └── v1/
│ │ ├── router.py # Aggregates all v1 route modules
│ │ └── routes/
│ │ └── health.py # GET /health
│ ├── core/
│ │ ├── settings.py # All config via Pydantic BaseSettings + .env
│ │ ├── logging.py # structlog setup, JSON or console renderer
│ │ └── exceptions.py # Domain exceptions + FastAPI exception handlers
│ ├── db/
│ │ ├── base.py # Imports Base + all models (required for Alembic autogenerate)
│ │ └── session.py # Async engine, sessionmaker, get_db dependency
│ ├── models/
│ │ └── base.py # Declarative base with UUID PK + timestamps
│ ├── schemas/
│ │ ├── base.py # BaseSchema (from_attributes=True) + TimestampedSchema
│ │ └── common.py # HealthResponse, MessageResponse
│ ├── services/ # Business logic layer (empty — add your services here)
│ ├── repositories/
│ │ └── base.py # Generic CRUD: get, get_multi, create, update, delete
│ ├── middleware/
│ │ ├── logging.py # Per-request structured logging + X-Request-ID header
│ │ └── rate_limit.py # Redis token-bucket rate limiter per IP
│ ├── utils/
│ │ └── pagination.py # PaginationMeta dataclass with offset/pages helpers
│ └── main.py # App factory, lifespan (Redis connect), middleware stack
├── alembic/
│ ├── env.py # Async Alembic runner; reads DATABASE_URL from settings
│ └── versions/ # Auto-generated migration files go here
├── tests/
│ ├── conftest.py # Async fixtures: engine, DB session, ASGI test client
│ └── api/v1/
│ └── test_health.py # Health endpoint smoke test
├── .github/workflows/ci.yml # GitHub Actions: migrate + test on push/PR
├── .env.example # Template for .env — all variables documented
├── docker-compose.yml # Dev stack (api + db + redis) with hot reload
├── docker-compose.prod.yml # Prod overrides (no volume mounts, more workers)
├── Dockerfile # Multi-stage build (builder → runtime, non-root user)
├── alembic.ini
├── requirements.txt
└── pyproject.toml # pytest, coverage, ruff, mypy config
cp .env.example .envOpen .env and set the required values:
| Variable | How to set it |
|---|---|
POSTGRES_USER |
Your choice, e.g. appuser |
POSTGRES_PASSWORD |
A strong password |
POSTGRES_DB |
Your choice, e.g. myapp |
Everything else has a working default for local development.
CORS_ORIGINS=http://localhost:3000,https://yourfrontend.com
Set EMAILS_ENABLED=true and fill the SMTP_* variables. Leave all blank to disable — nothing will break.
Fill S3_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY. Set S3_ENDPOINT_URL for MinIO or Cloudflare R2. Leave blank to disable.
# Start all services (api + postgres + redis)
docker compose up --build
# In a separate terminal — create initial migration (first time only)
docker compose exec api alembic revision --autogenerate -m "initial"
docker compose exec api alembic upgrade head
# API is available at:
# http://localhost:8000
# http://localhost:8000/docs (Swagger — disabled in production)
# http://localhost:8000/api/v1/healthpython -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
# Make sure PostgreSQL and Redis are running locally, then:
alembic upgrade head
uvicorn app.main:app --reload# Generate a new migration after changing a model
alembic revision --autogenerate -m "describe what changed"
# Apply all pending migrations
alembic upgrade head
# Roll back one migration
alembic downgrade -1
# Show migration history
alembic history --verboseRule: never call Base.metadata.create_all() outside of tests. All schema changes go through Alembic.
The pattern is the same for every new domain entity:
- Model →
app/models/your_entity.py(inherit fromBase) - Register → add
from app.models.your_entity import YourEntitytoapp/db/base.py - Migration →
alembic revision --autogenerate -m "add your_entity" - Schema →
app/schemas/your_entity.py - Repository →
app/repositories/your_entity_repo.py(extendBaseRepository) - Service →
app/services/your_entity_service.py - Routes →
app/api/v1/routes/your_entity.py - Register routes → add
router.include_router(your_entity.router)inapp/api/v1/router.py
# All tests
pytest
# With coverage report
pytest --cov=app --cov-report=term-missing
# Single file
pytest tests/api/v1/test_health.py -vTests use a rollback-per-test fixture so the DB is always clean. Set TEST_DATABASE_URL in your environment to point at a separate test database in CI.
-
ENVIRONMENT=production -
DEBUG=false -
LOG_JSON=true -
DATABASE_ECHO=false -
ALLOWED_HOSTSset to your actual domain(s) -
CORS_ORIGINSset to your actual frontend URL(s) -
REDIS_PASSWORDset -
.envis not committed to version control (check.gitignore) -
docker-compose.prod.ymlused (removes exposed DB/Redis ports, disables volume mounts) - Migrations applied:
alembic upgrade head - Swagger UI disabled (automatic when
ENVIRONMENT=production) - HTTPS terminated at reverse proxy (nginx/Caddy/ALB) — app itself does not handle TLS
See .env.example for the full list with descriptions. All variables are loaded via app/core/settings.py (Pydantic BaseSettings) — start there to understand defaults and validation rules.
MIT