feat(slice-4): backend serves the built SPA — the deployed nginx-proxies-all topology (launch-app §2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 23:32:22 -07:00
parent a1c5544694
commit 9f4295b77e
2 changed files with 33 additions and 1 deletions
+9 -1
View File
@@ -17,6 +17,7 @@ from typing import Any
import psycopg import psycopg
from fastapi import Depends, FastAPI, Response from fastapi import Depends, FastAPI, Response
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
from app.domains import accounts, storefronts from app.domains import accounts, storefronts
@@ -89,7 +90,7 @@ def _set_session_cookie(response: Response, account: accounts.Account) -> None:
) )
def create_app(database_url: str | None = None) -> FastAPI: def create_app(database_url: str | None = None, static_dir: str | Path | None = None) -> FastAPI:
_ensure_app_logging() _ensure_app_logging()
dsn = database_url or config.database_url() dsn = database_url or config.database_url()
@@ -202,6 +203,13 @@ def create_app(database_url: str | None = None) -> FastAPI:
) )
return JSONResponse(status_code=201, content={"id": sf.id, "name": sf.name}) return JSONResponse(status_code=201, content={"id": sf.id, "name": sf.name})
# Deployed topology (launch-app SPEC §2): nginx proxies everything here, so the
# backend serves the built SPA. Mounted LAST so /healthz and /api/* win. In dev the
# dist dir doesn't exist (Vite serves the frontend) and the mount is skipped.
spa_dir = Path(static_dir) if static_dir is not None else _REPO_ROOT / "frontend" / "dist"
if (spa_dir / "index.html").is_file():
app.mount("/", StaticFiles(directory=spa_dir, html=True), name="spa")
return app return app
+24
View File
@@ -0,0 +1,24 @@
"""Deployed topology (launch-app SPEC §2): nginx proxies EVERYTHING to the backend, so
the backend must serve the built SPA (frontend/dist) itself. Dev is unaffected — Vite
serves the frontend and the default dist dir simply doesn't exist."""
from fastapi.testclient import TestClient
from app.main import create_app
def test_spa_served_when_dist_present(fresh_db_url, tmp_path):
(tmp_path / "index.html").write_text("<!doctype html><title>ecomm spa</title>")
with TestClient(create_app(database_url=fresh_db_url, static_dir=tmp_path)) as client:
root = client.get("/")
assert root.status_code == 200
assert "ecomm spa" in root.text
# API + health still win over the mount
assert client.get("/healthz").json()["status"] == "ok"
assert client.get("/api/auth/me").status_code == 401
def test_no_mount_when_dist_absent(fresh_db_url, tmp_path):
# An empty/missing dist (the dev case) must not 500 the app — / just 404s.
with TestClient(create_app(database_url=fresh_db_url, static_dir=tmp_path / "nope")) as client:
assert client.get("/").status_code == 404
assert client.get("/healthz").json()["status"] == "ok"