Skip to content

Repository files navigation

FastAPI Modulith Todo API

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.

Table of Contents

Features

  • 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.

Tech Stack

  • 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

Project Structure

.
├── 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

Architecture

Modulith

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.

Clean Architecture Direction

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.

DDD Layers

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.

CQRS

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.

Request Flow

Register User

POST /api/v1/auth/register
  -> user_router.register
  -> RegisterUserCommand
  -> RegisterUserCommandHandler
  -> UserRepository port
  -> SQLAlchemyUserRepository
  -> users table

Login User

POST /api/v1/auth/login
  -> user_router.login
  -> LoginUserCommand
  -> LoginUserCommandHandler
  -> SQLAlchemyUserRepository
  -> JWT access token

Authenticated Todo Request

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

API Routes

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.

Prerequisites

  • Python 3.14 or compatible with the project constraint.
  • Poetry.
  • PostgreSQL, either local or via Docker.
  • Make, if using the generated Makefile.

Environment Variables

Create a .env file from .env.example:

cp .env.example .env

Expected 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_db

Local Setup

Install dependencies:

poetry install

Activate the virtual environment if desired:

poetry shell

Or run commands through Poetry:

poetry run pytest -q

This repository also has a local .venv, so the Makefile uses .venv/bin/... where practical.

Running the Application

Run the API locally:

poetry run uvicorn src.main:app --reload --host 0.0.0.0 --port 8000

Or with Make:

make run

Open:

http://localhost:8000/docs

Health check:

http://localhost:8000/health

Operational checks:

http://localhost:8000/live
http://localhost:8000/ready

Database and Migrations

Alembic is configured in:

  • alembic.ini
  • alembic/env.py
  • alembic/versions/

Apply migrations:

poetry run alembic upgrade head

Create a new migration with autogenerate:

poetry run alembic revision --autogenerate -m "describe change"

Rollback one migration:

poetry run alembic downgrade -1

With Make:

make migrate
make seed
make revision name="add todo due date"
make downgrade

Important: 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 seed

The 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 Administrator

If 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.com with the user role
  • manager@example.com with the manager role
  • viewer@example.com with the viewer role

These users are skipped outside development and are not updated if they already exist.

Testing and Quality Checks

Run tests:

make test

Run lint:

make lint

Run the full local check:

make check

Current check set:

  • pytest -q
  • ruff check src tests scripts
  • import check for src.main

Dependency scanning is available separately:

make security-scan

Makefile Commands

make 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 clean

Docker Notes

Before 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-up

Stop services:

make db-down

Follow service logs:

make db-logs

Development Guide

Adding a New Use Case

  1. Add a command or query in the module application layer.
  2. Add a handler in the application layer.
  3. Keep business rules in the domain entity when they are true invariants.
  4. Depend on domain repository interfaces, not SQLAlchemy directly.
  5. Add or extend infrastructure repositories only in the infrastructure layer.
  6. Wire the handler in presentation dependencies.
  7. Expose the use case from the FastAPI router.
  8. Add focused tests.

Adding a New Module

Use the same structure:

src/modules/<module_name>/
├── application/
├── domain/
├── infrastructure/
└── presentation/

Then register its router in:

src/core/routers/api/v1.py

Adding a New Table

  1. Create the SQLAlchemy model in the module infrastructure layer.

  2. Ensure the model imports into Alembic metadata discovery.

  3. Generate a migration:

    make revision name="add new table"
  4. Review the generated migration before applying it.

  5. Apply:

    make migrate

Troubleshooting

Import errors in tests

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.

Database connection errors

Check DATABASE_URL.

For Docker Compose, the database hostname is usually:

db

For local execution against a host PostgreSQL instance, it is usually:

localhost

Alembic does not detect model changes

Make sure the model is imported by alembic/env.py or by something that is imported there before target_metadata = Base.metadata.

Authentication failures

Protected routes require:

Authorization: Bearer <token>

The token must contain a sub claim with a valid user id.

Security TODO

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.

Next Implementation Checklist

  • 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.json in 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-audit or an equivalent Poetry-compatible scanner.

Known Notes

  • src/core/lifespan.py still calls Base.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.

About

A modular FastAPI backend with JWT auth, RBAC, PostgreSQL, Redis, and CQRS patterns.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages