feat(01-01): Backend FastAPI skeleton + Docker build config
- FastAPI app with SPAStaticFiles catch-all (root_path NOT in constructor)
- config.py with DATA_PATH, PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH
- Startup lifespan creates data directories automatically
- Health endpoint: GET /api/health -> {"status": "ok"}
- Dockerfile multi-stage: node:22-slim builds React, python:3.12-slim serves API+SPA
- --root-path /postgenerator set in Uvicorn CMD only (avoids Pitfall #4)
- docker-compose.yml: lab-postgenerator-app, proxy_net, named volume for data persistence
- requirements.txt with pinned versions: fastapi[standard]==0.135.1, anthropic==0.84.0
This commit is contained in:
78
backend/main.py
Normal file
78
backend/main.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""PostGenerator FastAPI application.
|
||||
|
||||
IMPORTANT: root_path is intentionally NOT set in the FastAPI() constructor.
|
||||
It is passed only via Uvicorn's --root-path flag at runtime to avoid the
|
||||
double-path bug (Pitfall #4).
|
||||
"""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from backend.config import CAMPAIGNS_PATH, CONFIG_PATH, OUTPUTS_PATH, PROMPTS_PATH
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPA static files with catch-all fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SPAStaticFiles(StaticFiles):
|
||||
"""Serve a React SPA: return index.html for any 404 so the client-side
|
||||
router handles unknown paths instead of returning a 404 from the server."""
|
||||
|
||||
async def get_response(self, path: str, scope):
|
||||
try:
|
||||
return await super().get_response(path, scope)
|
||||
except Exception:
|
||||
# Fall back to index.html for client-side routing
|
||||
return await super().get_response("index.html", scope)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Startup / shutdown lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Create required data directories on startup if they do not exist."""
|
||||
for directory in (PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
yield
|
||||
# Nothing to clean up on shutdown
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# CRITICAL: Do NOT pass root_path here — use Uvicorn --root-path instead.
|
||||
app = FastAPI(
|
||||
title="PostGenerator",
|
||||
description="Instagram carousel post generator powered by Claude",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
# root_path is intentionally absent — set via Uvicorn --root-path at runtime
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API routes (must be registered BEFORE the SPA catch-all mount)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health() -> dict:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPA catch-all mount (MUST be last — catches everything not matched above)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_STATIC_DIR = Path(__file__).parent.parent / "static"
|
||||
|
||||
if _STATIC_DIR.exists():
|
||||
app.mount("/", SPAStaticFiles(directory=str(_STATIC_DIR), html=True), name="spa")
|
||||
Reference in New Issue
Block a user