Skip to main content
Fintech Risk Monitor is a server-rendered web application. There is no separate frontend build step, no API client, and no SPA framework. The browser talks directly to FastAPI; HTMX handles dynamic updates by swapping HTML fragments returned from the same routes.

Tech stack

LayerTechnologyRole
Web frameworkFastAPIAsync-capable Python router, automatic request validation via Pydantic, built-in OpenAPI docs at /docs
ORMSQLAlchemyMaps Python classes to SQL tables; used directly in route handlers with no service layer
DatabaseSQLiteZero-configuration embedded database, no separate server process. File: risk_monitor.db
TemplatesJinja2Server-side HTML rendering; pairs naturally with HTMX’s partial-swap model
UI interactivityHTMXInteractive UI via HTML attributes — filtering, sorting, pagination, and risk evaluation all work without writing JavaScript
ContainerDockerSingle-container deployment using python:3.12-slim, port 8000, uvicorn entrypoint

Request flow

Every request follows the same path:
Browser → FastAPI router → SQLAlchemy query → Jinja2 template → HTML response
For a direct page load (e.g. navigating to /businesses), FastAPI renders and returns a full HTML page. For an HTMX-triggered interaction (e.g. filtering the business list), FastAPI detects the HX-Request header and returns only the HTML fragment needed to update part of the page — no full reload required.
Full page load:   GET /businesses              → index.html (full page)
HTMX interaction: GET /businesses?industry=crypto  → partials/business_rows.html (fragment)
The route handler checks the header and branches accordingly:
if request.headers.get("HX-Request"):
    return templates.TemplateResponse("partials/business_rows.html", ctx)
return templates.TemplateResponse("index.html", ctx)
This keeps rendering logic in one place. The same query runs for both cases; only the template changes.

HTMX partials pattern

HTMX drives all dynamic interactions in the UI:
  • Filtering — submitting the search form sends a GET with query parameters; the response swaps in updated table rows
  • Sorting — clicking a column header triggers the same route with ?sort= and ?order= parameters
  • Pagination — page links include ?page= and update only the table region
  • Risk evaluationPOST /businesses/{id}/evaluate returns a partials/risk_display.html fragment that replaces the score display in place
Routes that serve partials still work as full pages when navigated to directly, because the template branch falls through to the full-page render when the HX-Request header is absent.

Design decisions

Single routes file — with five business endpoints plus a test runner, splitting into multiple router modules would add indirection without value. Each route has a clear docstring and app/routes.py stays under 250 lines. No service layer — routes query SQLAlchemy directly. Two models and one evaluation function don’t justify an extra abstraction layer. If the app grows, extracting services is a straightforward refactor. Factors stored as JSON text — SQLite has no native JSON column type. The factors column stores json.dumps(...) in a Text column and is parsed on read with json.loads(). In a production PostgreSQL setup this would be a native jsonb column.
The simulated risk engine uses deterministic industry and country modifiers plus controlled randomness. This makes results testable (statistical tests verify high-risk inputs trend higher) while still producing varied scores on each evaluation run.

Container

The app ships as a single Docker container:
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
On startup, main.py creates all database tables (Base.metadata.create_all) and seeds 50 sample businesses if the database is empty.

Explore further

Risk engine

How the simulated scoring algorithm computes risk scores from industry and country inputs

Data model

The two SQLAlchemy models, their columns, relationships, and the JSON text pattern for factors

Build docs developers (and LLMs) love