A production-ready FastAPI template implementing Domain-Driven Design (DDD) and Clean Architecture principles with Dependency Injection, featuring authentication, database migrations, and Docker deployment.
- Clean Architecture: Organized by contexts following DDD principles (NOT strict DDD)
- Dependency Injection: Using
dependency-injectorfor better testability and modularity - Authentication: API Key middleware with SHA-256 hashing (keys never stored in plain text)
- REST API: User management endpoints (
/api/v1/auth/users) with cursor-based pagination - CLI Commands: Typer-based administrative CLI for user and API key management
- Domain Events: Synchronous in-process event bus with subscriber pattern
- Rate Limiting: Sliding window rate limiter middleware (per-IP, configurable)
- Distributed Cache: Redis-backed cache (
RedisCacheClient) with graceful degradation;InMemoryCacheClientused as the test double - Deep Health Check:
/healthendpoint verifies database and Redis connectivity with latency reporting - Database Management:
- PostgreSQL 18 with async SQLAlchemy
- Alembic for database migrations
- Connection pooling and health checks
- Docker Support: Complete Docker Compose setup for development and production
- Code Quality:
- Ruff for linting and formatting (configured with extensive rule sets)
- Pre-commit hooks
- Type hints targeting Python 3.14
- Structured Logging: Loguru-based wide events β one canonical, queryable log line per request with request-id correlation (JSON in production)
- Settings Management: Pydantic Settings with environment-based configuration
- Production Ready:
- Multi-stage Docker builds
- Non-root user execution
- Hot reload in development
- Python 3.14+
- uv - Fast Python package installer (used locally and in Docker)
- Docker & Docker Compose (for containerized deployment)
- PostgreSQL 18+ (handled by Docker Compose)
- Redis 8+ (handled by Docker Compose)
fastapi-template/
βββ src/
β βββ main.py # FastAPI application entry point
β βββ settings.py # Application settings & configuration
β βββ container.py # Dependency injection container
β βββ contexts/ # DDD contexts
β βββ auth/ # Authentication context
β β βββ domain/ # Domain models & repositories
β β βββ application/ # Use cases
β β βββ infrastructure/ # Persistence, HTTP middleware, CLI
β βββ shared/ # Shared kernel
β βββ domain/
β βββ infrastructure/ # Database, cache, logging
βββ migrations/ # Alembic database migrations
βββ scripts/ # Utility scripts (init_db, etc.)
βββ secrets/ # Environment variables (.env)
βββ docker-compose.yaml # Docker services definition
βββ Dockerfile # Multi-stage Docker build
βββ tests/ # Test suite
β βββ support/ # Shared fixtures (DB, containers, factories)
β βββ contexts/ # Tests per bounded context (unit, integration, e2e)
βββ Makefile # Development commands
βββ pyproject.toml # Project dependencies & config
git clone https://github.com/p0llopez/fastapi-template.git
cd fastapi-templateCreate a .env file in the secrets/ directory (used by the app and Docker Compose):
cp secrets/.env.example secrets/.env# Build and start all services
make build && make start
# Or manually:
docker compose up --buildThe API will be available at http://localhost:8080
# Install dependencies with uv
make install
# Or manually:
uv sync
# Run database migrations (requires DATABASE_URL)
make migration-upgrade
# Start the application
uv run uvicorn src.main:app --reloadThe project includes a comprehensive Makefile for common tasks:
make start # Start all services
make stop # Stop all services
make build # Build Docker images
make restart # Restart all services
make logs # Follow application logs
make shell # Open bash in app container
make db-shell # Open psql in database containerIf your environment file lives in secrets/.env, run:
ENV_FILE=secrets/.env make db-shellmake migration-create # Create auto-generated migration
make migration-create-empty # Create empty migration template
make migration-upgrade # Apply all pending migrations
make migration-downgrade # Rollback last migration
make migration-history # Show migration history
make migration-current # Show current migration version
make migration-show # Show current head revision
make migration-sql # Generate SQL for upgrade headmake test # Run all tests (Docker)
make test-unit # Run unit tests (Docker)
make test-unit-local # Run unit tests locally (fast, no Docker)
make test-integration # Run integration tests (Docker, requires DB)
make test-e2e # Run e2e tests (Docker, requires DB)
make test-cov # Run tests with coverage (Docker)make fmt # Format code with ruff
make lint # Run linter with auto-fix
make all # Run install, format, lint, and test
make clean # Remove __pycache__ and .pyc files# Make sure services are running first
make start
# Show CLI help
make cli
# Run CLI commands (pass args explicitly):
make cli args="auth list-users"
make cli args="auth create-user --username admin --email admin@example.com"
make cli args="auth create-api-key --user-id <uuid>"
make cli args="auth deactivate-api-key --user-id <uuid> --api-key <key>"
# For local development (without Docker):
uv run cli auth create-user --username admin --email admin@example.com
uv run cli auth list-usersThe project is organized into contexts (bounded contexts in DDD terminology):
- Auth Context: Handles authentication, users, and API keys
- Shared Context: Common infrastructure (database, logging, caching)
Each context follows a layered architecture:
- Domain Layer: Business logic, entities, and repository interfaces
- Application Layer: Use cases and business workflows
- Infrastructure Layer: Database models, external services, and implementations
The application uses dependency-injector for managing dependencies:
ApplicationContainer: Root container- Context-specific containers (e.g.,
AuthContainer,SharedContainer) - Providers for repositories, use cases, and services
To add a new feature (e.g., a "Products" context):
-
Create the context structure:
src/contexts/products/ βββ domain/ β βββ aggregates.py # Product entity β βββ repositories.py # Repository interface βββ application/ β βββ use_cases/ # Business logic βββ infrastructure/ βββ container.py # DI container βββ persistence/ # Database models -
Register in the main container (
src/container.py) -
Create database models and migrations
-
Implement use cases and endpoints
The template includes an API Key authentication system and user management:
- User Management: Create and manage users via REST API or CLI (passwords are bcrypt-hashed)
- API Keys: SHA-256 hashed β plain key shown only once at creation, never stored
- HTTP: All routes require
X-API-Keyheader unless explicitly marked@public
| Method | Route | Public | Description |
|---|---|---|---|
POST |
/api/v1/auth/users |
Yes | Create user |
GET |
/api/v1/auth/users |
No | List users (cursor-paginated) |
GET |
/api/v1/auth/users/{id} |
No | Get user by ID |
DELETE |
/api/v1/auth/users/{id} |
No | Delete user |
GET |
/health |
Yes | Deep health check (DB + Redis) |
API key management is CLI-only for security:
make cli args="auth create-user"
make cli args="auth list-users"
make cli args="auth create-api-key --user-id <uuid>"
make cli args="auth deactivate-api-key --user-id <uuid> --api-key <key>"- Global dependency: all routes require the
X-API-Keyheader. - Public routes: decorate handlers with
@publicto bypass auth. POST /api/v1/auth/usersis public (needed to create the first user).GET /healthis public (needed for load balancers/orchestrators).
## ποΈ Database
### Migrations
The project uses Alembic for database migrations:
```bash
# Create a new migration after modifying models
make migration-create
# Apply migrations
make migration-upgrade
# Rollback last migration
make migration-downgrade
Run the initialization script to set up the database and apply migrations:
docker compose exec app python -m scripts.init_dbThe Dockerfile uses a multi-stage build for optimized images:
- Base: Python 3.14 slim image
- Builder: Installs dependencies using uv
- Runner: Final image with only runtime dependencies
- Non-root user execution for security
- Compiled bytecode for faster startup
- Health checks for container orchestration
- Volume mounts for hot reload in development
Control build behavior:
# Build with dev dependencies
docker compose build --build-arg INSTALL_DEV=true
# Production build
docker compose build --build-arg INSTALL_DEV=falseContainer names are derived from the Compose project (the worktree directory), so multiple git worktrees can run their own stacks side by side. To avoid host-port collisions, override the host ports in each worktree's secrets/.env:
# e.g. in a second worktree
APP_HOST_PORT=8081
DB_HOST_PORT=5433
REDIS_HOST_PORT=6380Internal service URLs (DATABASE_URL, REDIS_URL) point at the Compose service names and are unaffected by these host-port overrides.
Configuration is managed through src/settings.py using Pydantic Settings:
from src.settings import settings
# Access configuration
if settings.is_production:
# Production-specific code
pass
# Database URL
db_url = settings.database_url
# Feature flags
log_level = settings.log_level- Application:
environment,log_level,log_format(console|json, defaultconsole; setjsonin staging/production for structured wide-event logs) - Security:
secret_key,allowed_origins - Rate Limiting:
rate_limit_requests(default: 100),rate_limit_window_seconds(default: 60),rate_limit_exclude_paths(default:["/health"]) - Database:
database_url - Redis:
redis_url(default:redis://localhost:6379/0)
The template uses structured logging (Loguru) built around wide events: every HTTP request emits one canonical log line carrying the full context of what happened β meant to be queried, not grepped.
Controlled by LOG_FORMAT (env var / settings.log_format):
console(default) β human-readable, colorized; for local development.jsonβ one flat JSON object per line; for staging/production (setLOG_FORMAT=json).
LOG_LEVEL controls verbosity (default INFO). Outside development, Loguru's diagnose is disabled so tracebacks never leak local variable values.
The request middleware emits one line per request automatically β you wire nothing. Each event includes request_id, method, path, route, status_code, duration_ms, outcome, client_ip, user_agent, and (once authenticated) user_id and api_key_id. In development it also adds RAM deltas (ram_start_mb, ram_end_mb, ram_delta_mb). The level scales with the outcome: 5xx β ERROR, 4xx β WARNING, else INFO. The line is emitted even when the request raises.
Example (LOG_FORMAT=json):
{"timestamp": "2026-06-06T14:41:57+02:00", "level": "INFO", "logger": "src.contexts.shared.infrastructure.logger.middleware", "message": "GET /api/v1/auth/users -> 200 (3.41ms)", "request_id": "0b9c1f2e-...", "method": "GET", "path": "/api/v1/auth/users", "route": "/api/v1/auth/users", "status_code": 200, "duration_ms": 3.41, "outcome": "ok", "client_ip": "127.0.0.1", "user_agent": "curl/8.4.0", "user_id": "a1b2...", "api_key_id": "c3d4..."}Any logger.* call made while handling a request automatically inherits that request's context (request_id, identity) β no plumbing needed, including in the domain/application layers:
from loguru import logger
logger.info("charge captured") # emitted with request_id + user_id already attachedThe request id is read from an inbound X-Request-ID header (or generated if absent) and echoed back in the response X-Request-ID header, so you can correlate client, logs, and downstream services.
To enrich the wide event with extra fields, call bind_context(...). It lives in the infrastructure layer, so call it from infrastructure code (HTTP middleware, dependencies, event subscribers) β the same way the auth dependency attaches user_id/api_key_id:
from src.contexts.shared.infrastructure.logger import bind_context
bind_context(tenant_id=str(tenant_id), plan="premium")Whatever you bind lands on the request's canonical line and on every log emitted afterwards in that request. Never log secrets β the raw X-API-Key is deliberately never bound or stored; only user_id/api_key_id are.
The project ships a committed agent configuration so every clone inherits it:
AGENTS.mdβ canonical instructions, read by all AI coding agents.CLAUDE.mdre-exports it for Claude Code (@AGENTS.md)..claude/rules/β path-scoped guidance (architecture, coding principles, naming, testing, migrations, git) that loads automatically when editing matching files..claude/settings.jsonβ shared permissions (denies destructive git ops and readingsecrets/.env) plus hooks that auto-runruffformat + lint on edit. Machine-specific overrides go in the gitignored.claude/settings.local.json..worktreeincludeβ copiessecrets/into new worktrees created withclaude --worktree <name>.
Planned improvements for future development:
- JWT Authentication β Token-based auth for user sessions alongside API keys
- Prometheus Metrics β
/metricsendpoint for application observability - OpenTelemetry Tracing β Distributed tracing for request flows
- Distributed Rate Limiting β Redis-backed rate limiter for multi-instance deployments
- Event Persistence β Event sourcing / domain event storage for audit trails
- Roles & Permissions (RBAC) β Role-based access control for fine-grained authorization
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.
Pol LΓ³pez Cano
- Email: pol.lopez.cano@gmail.com
- GitHub: @p0llopez
- FastAPI for the excellent web framework
- Astral's uv for blazing-fast package management
- The Python community for amazing tools and libraries
Happy Coding! π