A production-grade Personal Finance Manager REST API built with FastAPI, SQLAlchemy 2.0 async, and PostgreSQL. Demonstrates modern Python backend engineering: Clean Architecture, async/await, JWT authentication, and comprehensive testing.
Built as a portfolio project applicable to fintech, SaaS, and any data-driven Python backend.
Follows Clean Architecture (Hexagonal) with 4 layers:
┌──────────────────────────────────────────────────────────────────┐
│ presentation/ FastAPI routers, request/response schemas │
├──────────────────────────────────────────────────────────────────┤
│ application/ Services (use cases), Pydantic schemas │
├──────────────────────────────────────────────────────────────────┤
│ infrastructure/ SQLAlchemy ORM models, async repositories │
├──────────────────────────────────────────────────────────────────┤
│ domain/ Pure Python dataclasses, domain exceptions │
│ (no framework dependencies) │
└──────────────────────────────────────────────────────────────────┘
| Feature | Details |
|---|---|
| JWT Auth | Access token (30 min) + Refresh token (7 days) — python-jose |
| Password security | bcrypt hashing — passlib |
| Async everything | SQLAlchemy 2.0 async + asyncpg — no blocking I/O |
| Accounts | CRUD — checking, savings, investment, credit with balance tracking |
| Transactions | CRUD — income/expense/transfer with category, tags, pagination |
| Analytics | Spending by category + monthly income vs expenses |
| Validation | Pydantic v2 — password strength, email, decimal precision |
| Rate limiting | 60 req/min per IP via slowapi |
| CORS | Configurable allowed origins |
| Background tasks | Welcome email via FastAPI BackgroundTasks |
| DB migrations | Alembic async migrations |
| Structured logging | structlog with request logging middleware |
| OpenAPI docs | Auto-generated Swagger UI + ReDoc |
| Tests | Unit (Mockito-style mocks) + integration (httpx AsyncClient + SQLite) |
- Python 3.11 — dataclasses, match statements,
X | Ytype hints - FastAPI 0.111 — async, dependency injection, OpenAPI
- SQLAlchemy 2.0 — async ORM (
mapped_column,Mappedtypes) - asyncpg — high-performance async PostgreSQL driver
- Alembic — database migrations (async-compatible)
- Pydantic v2 — request/response validation, settings
- python-jose — JWT encoding/decoding
- passlib + bcrypt — password hashing
- slowapi — FastAPI rate limiting
- structlog — structured JSON logging
- pytest + httpx — async test client
- Docker / Docker Compose — one-command local setup
- Python 3.11+
- Docker & Docker Compose
git clone https://github.com/M-TOUITI/fastapi-finance-api.git
cd fastapi-finance-api
docker-compose up -d
# API: http://localhost:8000/api/v1/docs
# pgAdmin: http://localhost:5050 (admin@demo.com / admin)# 1. Install dependencies
pip install -r requirements.txt
# 2. Copy and configure environment
cp .env.example .env
# 3. Start PostgreSQL
docker-compose up -d postgres
# 4. Run migrations
make migrate
# 5. Start dev server (with auto-reload)
make dev
# → http://localhost:8000/api/v1/docs# All tests with coverage
make test
# Unit tests only (no DB needed)
make test-unit
# Integration tests
make test-integrationAll endpoints are documented at /api/v1/docs (Swagger UI).
# Register
POST /api/v1/auth/register
{
"email": "user@example.com",
"password": "Password123",
"full_name": "John Doe"
}
# Login → get tokens
POST /api/v1/auth/login
{
"email": "user@example.com",
"password": "Password123"
}
# Response: { "access_token": "...", "refresh_token": "...", "token_type": "bearer" }
# Refresh access token
POST /api/v1/auth/refresh
{ "refresh_token": "..." }# Create account
POST /api/v1/accounts
Authorization: Bearer <token>
{
"name": "Main Checking",
"account_type": "checking", # checking | savings | investment | credit
"currency": "EUR",
"initial_balance": "1000.00"
}
# List accounts
GET /api/v1/accounts
# Get / Update / Delete
GET /api/v1/accounts/{id}
PATCH /api/v1/accounts/{id}
DELETE /api/v1/accounts/{id}# Create transaction
POST /api/v1/transactions
{
"account_id": "uuid",
"transaction_type": "expense", # income | expense | transfer
"amount": "85.50",
"description": "Grocery shopping",
"category": "food",
"transaction_date": "2025-06-15T14:00:00",
"tags": ["weekly", "organic"]
}
# List with filters + pagination
GET /api/v1/transactions
?account_id=uuid
&category=food
&transaction_type=expense
&date_from=2025-06-01
&date_to=2025-06-30
&page=1
&page_size=20# Spending by category (pie chart data)
GET /api/v1/analytics/spending-by-category
?date_from=2025-06-01&date_to=2025-06-30
# Response:
{
"spending_by_category": [
{"category": "food", "total": 350.0, "count": 8, "percentage": 35.0},
{"category": "housing", "total": 1200.0, "count": 1, "percentage": 65.0}
],
"total_spending": 1550.0
}
# Monthly summary (bar chart data)
GET /api/v1/analytics/monthly-summary?year=2025
# Response:
{
"year": 2025,
"total_income": 36000.0,
"total_expenses": 28000.0,
"net_savings": 8000.0,
"savings_rate": 22.2,
"monthly_totals": [
{"month": 1, "month_name": "January", "income": 3000.0, "expenses": 2300.0, "net": 700.0},
...
]
}fastapi-finance-api/
├── app/
│ ├── main.py # FastAPI app factory, middleware, routers
│ ├── core/
│ │ ├── config.py # pydantic-settings — env vars
│ │ ├── security.py # JWT + bcrypt
│ │ └── exceptions.py # exception handlers
│ ├── domain/
│ │ ├── models/ # Pure Python dataclasses (no ORM)
│ │ └── exceptions.py # Domain exceptions
│ ├── infrastructure/
│ │ ├── database/
│ │ │ ├── models.py # SQLAlchemy 2.0 ORM models
│ │ │ └── session.py # Async session + get_db dependency
│ │ └── repositories/ # Async repository classes
│ ├── application/
│ │ ├── services/ # Business logic / use cases
│ │ └── schemas/ # Pydantic v2 request/response schemas
│ └── presentation/
│ └── api/v1/
│ ├── dependencies.py # FastAPI DI (repos, services, current_user)
│ └── routers/ # auth, accounts, transactions, analytics
├── tests/
│ ├── conftest.py # Fixtures (client, db_session, auth_headers)
│ ├── unit/services/ # Service tests with mocks
│ └── integration/ # Full API tests with httpx AsyncClient
├── alembic/ # DB migrations
├── docker-compose.yml
├── Dockerfile
├── Makefile
└── requirements.txt
Why async SQLAlchemy 2.0? Async I/O allows the API to handle many concurrent requests without threads. With asyncpg, PostgreSQL queries are truly non-blocking — critical for high-throughput finance APIs where many users query dashboards simultaneously.
Why Clean Architecture? Each layer depends only inward. The domain and application layers have zero framework dependencies — services can be unit-tested without spinning up FastAPI or a database, using simple Python mocks.
Why Annotated for dependency injection?
Annotated[SomeType, Depends(get_something)] keeps router signatures clean and type-safe. It also makes the dependencies explicit without cluttering function bodies.
Why soft-delete for accounts?
Setting is_active=False preserves referential integrity — transactions linked to the account remain intact for audit/reporting. Hard-deleting an account would cascade-delete its transactions, losing financial history.
MIT