A modular FastAPI backend for user authentication and todo management. The project is structured as a modulith and follows the main ideas of Domain-Driven Design, Clean Architecture, and CQRS:
- Each business module owns its presentation, application, domain, and infrastructure code.
- FastAPI routers stay at the delivery edge.
- Application handlers coordinate use cases.
- Domain entities and repository contracts define business concepts.
- Infrastructure repositories adapt SQLAlchemy models to domain entities.
The API is currently versioned under /api/v1.
- Features
- Tech Stack
- Project Structure
- Architecture
- Request Flow
- API Routes
- Prerequisites
- Environment Variables
- Local Setup
- Running the Application
- Database and Migrations
- Testing and Quality Checks
- Makefile Commands
- Docker Notes
- Development Guide
- Troubleshooting
- Security TODO
- Known Notes
- User registration.
- User login with JWT access and refresh tokens.
- Refresh token rotation and logout/token revocation.
- Authentication middleware that validates bearer tokens.
- RBAC authorization with Casbin-backed roles and permissions.
- Current-user lookup from authenticated request state.
- Todo creation.
- Cursor-paginated todo listing by authenticated user.
- Todo update with ownership checks.
- Todo delete with ownership checks.
- Role and permission management APIs.
- Redis-backed rate limiting.
- Request ID, structured logging, audit logging, and security headers.
- Request size limiting and idempotency support.
- Health, liveness, and readiness endpoints.
- API route grouping under
/api/v1. - Async SQLAlchemy persistence.
- Alembic database migrations.
- Database seeders for authorization data and optional users.
- Pytest regression tests.
- Ruff linting.
- Python
>=3.14 - FastAPI
- Uvicorn
- SQLAlchemy async ORM
- Asyncpg PostgreSQL driver
- PostgreSQL
- Alembic
- Pydantic and Pydantic Settings
- Python JOSE for JWT handling
- Passlib for password hashing
- Casbin for authorization policies
- Redis and fastapi-limiter for rate limiting and token revocation
- Pytest
- Ruff
- Poetry
.
├── alembic/ # Alembic migration environment and versions
├── scripts/ # Helper shell scripts
├── src/
│ ├── main.py # FastAPI application entrypoint
│ ├── core/
│ │ ├── bootstrap/ # Application bootstrap helpers
│ │ ├── authorization/ # RBAC permissions, Casbin services, auth models
│ │ ├── config/ # Runtime settings
│ │ ├── database/ # PostgreSQL and Redis connection setup
│ │ ├── dependency/ # Shared FastAPI dependencies
│ │ ├── exceptions/ # Exception registration and handlers
│ │ ├── middleware/ # Auth, audit, security, logging, request middleware
│ │ ├── routers/ # API router composition
│ │ ├── schemas/ # Shared response schemas
│ │ ├── security/ # JWT, password, revocation, audit helpers
│ │ ├── seed/ # Database seeding orchestration
│ │ ├── utils/ # Cursor pagination helpers
│ │ └── lifespan.py # FastAPI lifespan hook
│ ├── modules/
│ │ ├── authorization/
│ │ │ ├── domain/ # Role and permission entities
│ │ │ └── presenter/ # Role and permission routers/schemas
│ │ ├── user/
│ │ │ ├── application/ # User commands, queries, handlers
│ │ │ ├── domain/ # User entity, exceptions, repository port
│ │ │ ├── infrastructure/ # SQLAlchemy model/repository/services
│ │ │ └── presentation/ # FastAPI router, dependencies, schemas
│ │ └── todo/
│ │ ├── application/ # Todo commands, queries, handlers
│ │ ├── domain/ # Todo entity, exceptions, repository port
│ │ ├── infrastructure/ # SQLAlchemy model/repository
│ │ └── presentation/ # FastAPI router and dependencies
│ └── shared/
│ ├── database/ # Shared SQLAlchemy base and mixins
│ └── exceptions/ # Cross-cutting exceptions
├── tests/ # Pytest tests
├── pyproject.toml # Project metadata and dependencies
├── poetry.lock # Poetry lock file
├── alembic.ini # Alembic configuration
├── Dockerfile # Docker image definition
└── docker-compose.yml # Local API, PostgreSQL, and Redis services
This is a single deployable application with module boundaries inside the codebase. The user, todo, and authorization modules are independent feature areas under src/modules.
The intended dependency direction is:
presentation -> application -> domain
infrastructure -> domain
The domain layer should not depend on FastAPI, SQLAlchemy, or external infrastructure. Infrastructure implements domain repository contracts.
Each module follows this shape:
domain: entities, domain exceptions, repository interfaces.application: commands, queries, and handlers that coordinate use cases.infrastructure: SQLAlchemy models and repository implementations.presentation: FastAPI routers, request/response schemas, and dependency wiring.
The code separates commands and queries at the application naming level:
- Commands mutate state, for example register user, login user, create todo, update todo.
- Queries read state, for example user detail and todo listing.
Some flows are still pragmatic and can be made stricter over time by introducing dedicated read repositories or read DTOs.
POST /api/v1/auth/register
-> user_router.register
-> RegisterUserCommand
-> RegisterUserCommandHandler
-> UserRepository port
-> SQLAlchemyUserRepository
-> users table
POST /api/v1/auth/login
-> user_router.login
-> LoginUserCommand
-> LoginUserCommandHandler
-> SQLAlchemyUserRepository
-> JWT access token
Request with Authorization: Bearer <token>
-> AuthenticationMiddleware validates JWT
-> request.state.user_id is set
-> route dependency checks role permission
-> todo handler executes use case
-> TodoRepository port
-> SQLAlchemyTodoRepository
-> todos table
Base API prefix:
/api/v1
Current routes:
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
GET /api/v1/auth/me
POST /api/v1/auth/logout
POST /api/v1/todos/
GET /api/v1/todos/?cursor=<cursor>&limit=10
PATCH /api/v1/todos/{todo_id}
DELETE /api/v1/todos/{todo_id}
POST /api/v1/roles/
GET /api/v1/roles/?cursor=<cursor>&limit=10
GET /api/v1/roles/{role_id}
PATCH /api/v1/roles/{role_id}
DELETE /api/v1/roles/{role_id}
POST /api/v1/roles/{role_id}/permissions/{permission_id}
DELETE /api/v1/roles/{role_id}/permissions/{permission_id}
POST /api/v1/permissions/
GET /api/v1/permissions/?cursor=<cursor>&limit=10
GET /api/v1/permissions/{permission_id}
PATCH /api/v1/permissions/{permission_id}
DELETE /api/v1/permissions/{permission_id}
GET /health
GET /live
GET /ready
Public routes:
/health/live/ready/docs/redoc/openapi.json/api/v1/auth/login/api/v1/auth/register
Protected routes require:
Authorization: Bearer <access_token>
Swagger UI, ReDoc, and OpenAPI JSON are disabled when APP_ENV=production.
- Python
3.14or compatible with the project constraint. - Poetry.
- PostgreSQL, either local or via Docker.
- Make, if using the generated
Makefile.
Create a .env file from .env.example:
cp .env.example .envExpected values:
APP_NAME=Todo Modulith API
APP_ENV=production
POSTGRES_USER=postgres
POSTGRES_PASSWORD=
POSTGRES_DB=todo_db
REDIS_PASSWORD=
DATABASE_URL=
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=10
DATABASE_POOL_TIMEOUT=30
DATABASE_POOL_RECYCLE=3600
REDIS_URL=
SECRET_KEY=
MAX_REQUEST_SIZE_MB=5242880
ALGORITHM=HS256
JWT_ISSUER=todo-modulith-api
JWT_AUDIENCE=todo-modulith-client
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=10080
RATE_LIMIT=100/minute
CORS_ALLOW_ORIGINS=http://localhost:3000
CORS_ALLOW_METHODS=*
CORS_ALLOW_HEADERS=*
SECURITY_CONTENT_SECURITY_POLICY=default-src 'self'; frame-ancestors 'none'
IDEMPOTENCY_TTL_SECONDS=86400
ACCOUNT_LOCKOUT_MAX_ATTEMPTS=5
ACCOUNT_LOCKOUT_WINDOW_MINUTES=15
ACCOUNT_LOCKOUT_DURATION_MINUTES=15
LOG_FORMAT=json
SEED_ADMIN_EMAIL=
SEED_ADMIN_PASSWORD=
SEED_ADMIN_USERNAME=admin
SEED_ADMIN_FULLNAME=System Administrator
SEED_DEVELOPMENT_USERS_PASSWORD=For local development without Docker, point DATABASE_URL at your local PostgreSQL host, for example:
DATABASE_URL=postgresql+asyncpg://postgres@localhost:5432/todo_dbInstall dependencies:
poetry installActivate the virtual environment if desired:
poetry shellOr run commands through Poetry:
poetry run pytest -qThis repository also has a local .venv, so the Makefile uses .venv/bin/... where practical.
Run the API locally:
poetry run uvicorn src.main:app --reload --host 0.0.0.0 --port 8000Or with Make:
make runOpen:
http://localhost:8000/docs
Health check:
http://localhost:8000/health
Operational checks:
http://localhost:8000/live
http://localhost:8000/ready
Alembic is configured in:
alembic.inialembic/env.pyalembic/versions/
Apply migrations:
poetry run alembic upgrade headCreate a new migration with autogenerate:
poetry run alembic revision --autogenerate -m "describe change"Rollback one migration:
poetry run alembic downgrade -1With Make:
make migrate
make seed
make revision name="add todo due date"
make downgradeImportant: migration autogeneration depends on importing all SQLAlchemy models in alembic/env.py, so new module models must be imported there or through a central model registry.
Seed baseline authorization data after applying migrations:
make seedThe seeder is idempotent. It creates default authorization resources, the default admin and user roles, default permissions, role-permission links, and matching Casbin policies without duplicating existing records.
To seed an initial admin user, set these environment variables before running make seed:
SEED_ADMIN_EMAIL=admin@example.com
SEED_ADMIN_PASSWORD=
SEED_ADMIN_USERNAME=admin
SEED_ADMIN_FULLNAME=System AdministratorIf SEED_ADMIN_EMAIL or SEED_ADMIN_PASSWORD is empty, user seeding is skipped. Existing users are not modified.
When APP_ENV=development, the seeder can also create demo users with different roles. Set a shared development password before running make seed:
SEED_DEVELOPMENT_USERS_PASSWORD=Development demo accounts:
user@example.comwith theuserrolemanager@example.comwith themanagerroleviewer@example.comwith theviewerrole
These users are skipped outside development and are not updated if they already exist.
Run tests:
make testRun lint:
make lintRun the full local check:
make checkCurrent check set:
pytest -qruff check src tests scripts- import check for
src.main
Dependency scanning is available separately:
make security-scanmake help
make install
make run
make test
make lint
make import-check
make security-scan
make check
make migrate
make seed
make downgrade
make revision name="migration message"
make db-up
make db-down
make db-logs
make cleanBefore starting Docker Compose, set non-empty POSTGRES_PASSWORD, REDIS_PASSWORD, and SECRET_KEY in .env. Compose intentionally fails fast when database or Redis passwords are missing.
Run API, PostgreSQL, and Redis services:
make db-upStop services:
make db-downFollow service logs:
make db-logs- Add a command or query in the module application layer.
- Add a handler in the application layer.
- Keep business rules in the domain entity when they are true invariants.
- Depend on domain repository interfaces, not SQLAlchemy directly.
- Add or extend infrastructure repositories only in the infrastructure layer.
- Wire the handler in presentation dependencies.
- Expose the use case from the FastAPI router.
- Add focused tests.
Use the same structure:
src/modules/<module_name>/
├── application/
├── domain/
├── infrastructure/
└── presentation/
Then register its router in:
src/core/routers/api/v1.py
-
Create the SQLAlchemy model in the module infrastructure layer.
-
Ensure the model imports into Alembic metadata discovery.
-
Generate a migration:
make revision name="add new table" -
Review the generated migration before applying it.
-
Apply:
make migrate
The tests add both the repository root and src to sys.path through tests/conftest.py. If new tests import modules inconsistently, prefer absolute src... imports.
Check DATABASE_URL.
For Docker Compose, the database hostname is usually:
db
For local execution against a host PostgreSQL instance, it is usually:
localhost
Make sure the model is imported by alembic/env.py or by something that is imported there before target_metadata = Base.metadata.
Protected routes require:
Authorization: Bearer <token>
The token must contain a sub claim with a valid user id.
Legend: Implemented means code exists in the repository. Partial means code exists but still needs a fix, test, or production hardening.
| Category | Recommended | Current Status | Notes |
|---|---|---|---|
| JWT Authentication | Required | Implemented | AuthenticationMiddleware validates bearer tokens for non-public routes. |
| Refresh Token Rotation | Required | Implemented | Refresh flow revokes the old refresh token and persists a new token. |
| RBAC + Permissions | Required | Implemented | Casbin-backed role and permission checks are wired through route dependencies. |
| Rate Limiting (Redis-backed) | Required | Implemented | Redis-backed limiter reads the configured RATE_LIMIT value. |
| Security Headers Middleware | Required | Implemented | Adds X-Content-Type-Options, X-Frame-Options, CSP frame-ancestors, Referrer-Policy, and Permissions-Policy. |
| CORS Configuration | Required | Implemented | CORS origins, methods, and headers are environment-driven through settings. |
| Request ID Middleware | Required | Implemented | Generates or propagates X-Request-ID and stores it on request state. |
| Audit Logging | Required | Implemented | Adds global endpoint audit logging, domain audit events, and separate persisted error traces. |
| Structured Logging | Required | Implemented | Logs request ID, method, path, status, latency, and user context when available. |
| Global Exception Handling | Required | Implemented | Domain exceptions are registered explicitly and Exception is used only as the fallback handler. |
| Input Validation | Required | Implemented | Pydantic schemas and application validation functions are used across user and todo flows. |
| Password Hashing (Argon2 or bcrypt) | Required | Implemented | User auth service uses bcrypt hashing. |
| Account Lockout | Required | Implemented | Tracks failed logins and temporarily locks accounts after configured thresholds. |
| Token Revocation | Required | Implemented | Refresh tokens are revoked on rotation/logout, and access tokens are denylisted in Redis until expiry. |
| OpenAPI Authentication | Required | Implemented | Swagger OAuth2 auth is configured, and docs/OpenAPI endpoints are disabled when APP_ENV=production. |
| Health Check Endpoint | Required | Implemented | /health endpoint returns service health. |
| Readiness/Liveness Endpoints | Required | Implemented | Adds /live and /ready operational endpoints. |
| Request Size Limiting | Required | Implemented | LimitRequestSizeMiddleware rejects oversized write requests. |
| Idempotency Support (for applicable POST endpoints) | Optional but valuable | Implemented | Supports Idempotency-Key replay caching for POST responses. |
| Database Migrations | Required | Implemented | Alembic is configured with migration commands in the README and Makefile. |
| Dependency Injection | Required | Implemented | FastAPI dependencies wire repositories, handlers, auth, authorization, and database sessions. |
| Configuration via Environment Variables | Required | Implemented | Pydantic settings read .env and reject the default secret key in production. |
- Fix and verify rate limit configuration wiring.
- Add security headers middleware.
- Add request ID middleware.
- Add structured request logging.
- Add audit logging for sensitive actions.
- Add account lockout or equivalent failed-login protection.
- Disable or authenticate
/docs,/redoc, and/openapi.jsonin production. - Add readiness and liveness endpoints.
- Add production config validation for secrets and unsafe defaults.
- Harden CORS through environment-driven allowed origins, methods, and headers.
- Review exception responses to avoid leaking token parsing details or internal exception messages.
- Add automated tests for request size limits, rate limiting, auth failures, authorization failures, CORS, security headers, and request IDs.
- Add dependency vulnerability scanning to local or CI checks, for example
pip-auditor an equivalent Poetry-compatible scanner.
src/core/lifespan.pystill callsBase.metadata.create_all; with Alembic in place, production environments normally rely on migrations instead.- The project has a Pydantic v2 deprecation warning for class-based settings config.
- The current architecture is clean enough for a learning modulith, but some flows can be made stricter by moving remaining business orchestration out of routers and into application handlers.