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
from fastapi import Depends, FastAPI, Response
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
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()
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})
# 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
+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"