Architecture decision ADR-012

Replace Express/TypeScript API with Python FastAPI

Accepted · 2026-05-20

Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.

Date: 2026-05-20
Status: Accepted
Feature: FEAT-016
Deciders: Daniel Meza


Context and Problem Statement

apps/api is a Node.js / Express / TypeScript service. apps/ingestion is a Python / FastAPI service. Running both runtimes in the monorepo requires maintaining two package managers (pnpm for Node, uv/pip for Python), two test toolchains (Vitest/supertest for the API, pytest for ingestion), and two Railway build environments — without any functional benefit that demands both.

The Express API is in an early TDD stub state: every route group exists but the repository layer is unimplemented (StubProductRepository throws new Error('not implemented')). No production traffic has reached it. This makes the refactor low-risk: there is no live behaviour to preserve beyond the HTTP contract already captured in docs/specs/openapi.yaml.


Decision Drivers


Considered Options

  1. Keep Express/TypeScript. Continue implementing the TS API. Benefit: no migration cost. Cost: permanent dual-runtime overhead; test toolchain split between Vitest and pytest.
  2. Replace with Python FastAPI (chosen). One-time refactor cost; unified runtime afterward.
  3. Rewrite in a different framework (Django, Flask, etc.). FastAPI is chosen over Django because it is async-first, produces OpenAPI natively, and matches the ingestion service pattern. Flask is synchronous by default, making async DB operations more complex.

Decision Outcome

Option 2: Replace apps/api with Python FastAPI.

What changes

Aspect Before (TS) After (Python)
Language TypeScript Python 3.12
Framework Express 4 FastAPI
Test framework Vitest + supertest pytest + httpx
ORM / DB layer node-postgres (raw SQL) SQLAlchemy 2.x async + asyncpg
Migration runner node-pg-migrate Alembic (wrapping existing SQL files)
Package manager pnpm (workspace member) pip / pyproject.toml
Railway builder Nixpacks (Node) Nixpacks (Python auto-detect)

What is preserved

Aspect Status
HTTP contract — all /api/v1/ paths Unchanged
Request/response shapes Unchanged
Status codes and error envelope Unchanged
docs/specs/openapi.yaml Reconciled but not structurally changed
packages/db/migrations/0001_init.sql Unchanged
DB schema (all tables, types, indexes) Unchanged
apps/web and apps/ingestion Untouched
packages/db, packages/types Untouched (TS packages remain for web + legacy)
Railway environment structure Unchanged (single production, no new services)

Migration strategy for Alembic

Alembic is configured in apps/api/alembic/. A single Python migration file (alembic/versions/0001_init.py) uses op.execute() to run the full text of packages/db/migrations/0001_init.sql verbatim. This satisfies the requirement to keep the SQL source unchanged while giving Alembic its version-tracking table (alembic_version).

The previous node-pg-migrate migration command (tracked in pgmigrations) is replaced by:

railway run --service api --environment production \
  alembic upgrade head

Note: On a production database that has already had 0001_init applied via node-pg-migrate, run alembic stamp head to mark the migration as applied without re-executing the DDL. New databases use alembic upgrade head normally.

Contract-drift guard

apps/api/tests/test_contract_drift.py loads the FastAPI app, fetches /openapi.json, and diffs it against docs/specs/openapi.yaml. Any structural divergence fails the test. This replaces the Zod schema validation in the TS contract tests as the primary contract guard.

Re-migration cost

If the team later chooses to return to TypeScript for the API service, the porting cost is equivalent to this refactor (≈ a sprint of work). The HTTP contract, DB schema, and OpenAPI spec are language-agnostic and require no changes in either direction.


Consequences

Positive

Negative / Trade-offs


References