Files: - STACK.md - FEATURES.md - ARCHITECTURE.md - PITFALLS.md - SUMMARY.md Key findings: - Stack: FastAPI 0.135.1 + React 19 + Vite 7 + Tailwind v4, single-container deploy - Architecture: FastAPI serves React SPA via catch-all, file-based storage (Docker volume), LLMService with retry/backoff - Critical pitfall: All 9 pitfalls map to Phase 1 — Italian prompts, Canva field constants, UTF-8 BOM, root_path config, per-item bulk isolation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
17 KiB
Stack Research
Domain: Python content automation backend + React SPA frontend (bulk social media content generation) Researched: 2026-03-07 Confidence: HIGH (all versions verified against PyPI and npm registries as of March 2026)
Recommended Stack
Core Technologies
| Technology | Version | Purpose | Why Recommended |
|---|---|---|---|
| Python | 3.12+ | Runtime | LTS, full async support, matches Docker slim images |
| FastAPI | 0.135.1 | API framework + static file serving | Native async, Pydantic v2 integration, StaticFiles for SPA, OpenAPI auto-docs. Current standard for Python REST APIs. |
| Uvicorn | 0.41.0 | ASGI server | Production-grade async server for FastAPI. "Standard" install includes uvicorn + extras. |
| Pydantic | 2.12.5 | Data validation + settings | FastAPI depends on it. v2 is 5-50x faster than v1. Required for request/response schemas. |
| anthropic (SDK) | 0.84.0 | Claude API client | Official Anthropic Python SDK. Supports sync/async, streaming via client.messages.stream(). |
| React | 19.2.4 | Frontend SPA | Latest stable. New concurrent features, improved hooks. Ecosystem standard. |
| Vite | 7.x (latest stable) | Frontend build tool | Faster than Webpack/CRA. Native ES modules in dev. First-party React plugin. Single-command build for production. |
| Tailwind CSS | 4.2.1 | Utility CSS | v4 is major rewrite: 5x faster builds, no tailwind.config.js required, native @tailwindcss/vite plugin. |
Supporting Libraries
Python Backend
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| python-dotenv | 1.2.2 | Environment variable loading | Load .env in local dev. In Docker, use compose environment: directly. |
| python-multipart | 0.0.22 | Form/file upload parsing | Required by FastAPI for UploadFile and Form parameters. |
| httpx | 0.28.1 | Async HTTP client | Calling Unsplash API. Native async, HTTP/2 support, replaces requests for async code. |
| aiofiles | 24.x | Async file I/O | Reading/writing prompt files and generated CSVs without blocking the event loop. |
CSV generation: Use Python's built-in csv module (stdlib). No external dependency needed. This is the correct choice for structured CSV output to Canva. Pandas/Polars are overkill — the project generates structured rows, not analyzes datasets.
Prompt file management: Use Python's built-in pathlib.Path + json/yaml stdlib. No external library needed for file-based prompt storage.
React Frontend
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| @tailwindcss/vite | 4.x | Tailwind v4 Vite plugin | Required for Tailwind v4 integration. Replaces PostCSS config. |
| shadcn/ui | latest CLI | UI component collection | Copy-owned components built on Radix UI + Tailwind. Use for forms, tables, modals, progress indicators. Not an npm dep — CLI copies source. |
| @radix-ui/react-* | latest | Accessible primitives | Pulled in automatically by shadcn/ui. Headless, accessible. |
| react-router-dom | 7.x | Client-side routing | SPA navigation between generation wizard, history, settings pages. |
| @tanstack/react-query | 5.x | Server state + async data | Handles API call lifecycle (loading, error, cache). Essential for polling long-running generation jobs. |
| lucide-react | latest | Icons | shadcn/ui default icon set. Consistent with component library. |
Development Tools
| Tool | Purpose | Notes |
|---|---|---|
| Docker | Container runtime | Multi-stage build: Node stage builds React, Python stage serves everything |
| docker compose | Local dev + VPS deploy | Single docker-compose.yml for both environments with env overrides |
| Vite dev proxy | Dev CORS bypass | vite.config.ts proxy /api to localhost:8000 during development |
| TypeScript | Frontend type safety | Enabled by default in Vite React template. Catches API contract errors at compile time. |
| ESLint + prettier | Code quality | Vite template includes eslint. Add prettier for formatting. |
Installation
Backend
# Create and activate venv
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
# Core
pip install fastapi[standard]==0.135.1
pip install anthropic==0.84.0
pip install httpx==0.28.1
pip install python-dotenv==1.2.2
pip install aiofiles
# python-multipart is included in fastapi[standard]
# pydantic is included in fastapi[standard]
# uvicorn is included in fastapi[standard]
# Generate requirements.txt
pip freeze > requirements.txt
Note: fastapi[standard] installs FastAPI + uvicorn[standard] + python-multipart + pydantic + email-validator. This is the recommended install per FastAPI docs as of v0.100+. Do NOT install fastapi-slim — it was deprecated.
Frontend
# Create Vite + React + TypeScript project
npm create vite@latest frontend -- --template react-ts
cd frontend
# Install Tailwind v4 with Vite plugin
npm install tailwindcss @tailwindcss/vite
# Install routing and data fetching
npm install react-router-dom @tanstack/react-query
# Install shadcn/ui (interactive CLI)
npx shadcn@latest init
# Add components as needed
npx shadcn@latest add button input table progress select
# Dev dependencies
npm install -D typescript @types/react @types/react-dom
Tailwind v4 Configuration
In vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
In src/index.css (no tailwind.config.js needed in v4):
@import "tailwindcss";
Architecture: Single-Container Deploy
FastAPI serves both the API and the built React SPA. This eliminates CORS configuration and simplifies Docker deployment on the VPS.
Pattern:
# main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import os
app = FastAPI()
# API routes first
app.include_router(api_router, prefix="/api")
# SPA catch-all: serve index.html for any non-API route
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
static_dir = "static"
file_path = os.path.join(static_dir, full_path)
if os.path.isfile(file_path):
return FileResponse(file_path)
return FileResponse(os.path.join(static_dir, "index.html"))
# Mount static files (CSS, JS, assets)
app.mount("/assets", StaticFiles(directory="static/assets"), name="assets")
Multi-stage Dockerfile:
# Stage 1: Build React frontend
FROM node:22-slim AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build
# Stage 2: Python runtime
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
# Copy built frontend into FastAPI static directory
COPY --from=frontend-builder /app/frontend/dist ./static
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
Claude API Integration
Recommended model: claude-sonnet-4-5 for content generation (best cost/quality ratio for structured text output at volume).
Sync pattern (for bulk generation — simpler, no streaming needed for batch jobs):
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2000,
messages=[
{"role": "user", "content": prompt}
]
)
content = response.content[0].text
Streaming pattern (for real-time UI feedback during generation):
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
) as stream:
for text in stream.text_stream:
yield text # SSE to frontend
Async pattern (for FastAPI async endpoints):
async_client = anthropic.AsyncAnthropic()
async def generate_content(prompt: str) -> str:
response = await async_client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
Unsplash API Integration
Rate limits:
- Demo (free) tier: 50 requests/hour
- Production (after manual approval): 5,000 requests/hour
Implication for bulk generation: With 50 req/hour limit and bulk carousel generation, implement:
- Local image cache (save URLs/download images locally after first fetch)
- Deduplication (don't re-fetch same search term if cached)
- Fallback to cached results when limit approached
Python integration via httpx (no dedicated library needed — Unsplash REST API is simple):
import httpx
async def search_unsplash(query: str, count: int = 1) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.unsplash.com/search/photos",
params={"query": query, "per_page": count},
headers={"Authorization": f"Client-ID {UNSPLASH_ACCESS_KEY}"}
)
response.raise_for_status()
return response.json()["results"]
Why not python-unsplash library: python-unsplash (v1.2.5, last updated Nov 2023) and pyunsplash (v2020) are both poorly maintained. The Unsplash REST API is simple enough that httpx direct calls are cleaner, more maintainable, and don't add a stale dependency.
CSV Generation for Canva Bulk Create
Use Python's stdlib csv module — no additional dependency:
import csv
import io
def generate_canva_csv(rows: list[dict], fields: list[str]) -> str:
"""Generate CSV string formatted for Canva Bulk Create."""
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
return output.getvalue()
FastAPI endpoint for CSV download:
from fastapi.responses import StreamingResponse
@app.get("/api/export/{job_id}/csv")
async def export_csv(job_id: str):
csv_content = get_job_csv(job_id) # retrieve generated CSV
return StreamingResponse(
io.StringIO(csv_content),
media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename=carousels_{job_id}.csv"}
)
File-Based Prompt Management
No database. Prompt templates stored as files:
backend/
prompts/
frameworks/
persuasion_nurturing.txt
schwartz_awareness.txt
niches/
tech_saas.txt
finance.txt
system_prompt.txt
Access pattern:
from pathlib import Path
PROMPTS_DIR = Path(__file__).parent / "prompts"
def load_prompt(category: str, name: str) -> str:
prompt_path = PROMPTS_DIR / category / f"{name}.txt"
return prompt_path.read_text(encoding="utf-8")
def list_prompts(category: str) -> list[str]:
return [p.stem for p in (PROMPTS_DIR / category).glob("*.txt")]
Advantages over DB: Zero setup, version-controlled with git, easy to edit, diff-friendly. Appropriate for this scale (tens of prompt files, not millions of records).
Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|---|---|---|
| FastAPI | Django REST Framework | When you need Django ORM, admin panel, or large team with Django expertise |
| FastAPI | Flask | Flask is simpler but lacks native async, auto OpenAPI docs, and Pydantic integration |
| React + Vite | Next.js | When you need SSR, SEO, or ISR. Overkill for an internal B2B tool served on a subpath. |
| Tailwind v4 | Tailwind v3 | Only if targeting environments where v4's new CSS syntax causes issues. v4 is stable as of 2025. |
| shadcn/ui | MUI / Chakra UI | When you need a full pre-styled design system without customization. shadcn is better for owned components. |
| stdlib csv | pandas | When doing data analysis, not just writing structured rows. pandas adds ~30MB to container. |
| httpx direct | python-unsplash | Only if Unsplash API complexity grows significantly (pagination, upload, etc.) |
| Single container | Separate nginx + Python | When frontend and backend scale independently, or need different update cadences. Overkill for this project. |
What NOT to Use
| Avoid | Why | Use Instead |
|---|---|---|
fastapi-slim |
Deprecated as of recent FastAPI versions. Missing extras. | fastapi[standard] |
create-react-app |
Unmaintained since 2023. No Vite, slow builds, outdated webpack. | npm create vite@latest |
requests library |
Sync-only. Blocks FastAPI async event loop when called from async endpoints. | httpx with AsyncClient |
openai library for Claude |
Wrong SDK. Anthropic has official anthropic SDK with better type safety and streaming helpers. |
anthropic SDK |
| Tailwind v3 PostCSS config | v3 config is still valid but v4 drops tailwind.config.js requirement and is significantly faster. |
Tailwind v4 + @tailwindcss/vite |
python-unsplash / pyunsplash |
Both stale (2020-2023), no async support, thin wrappers adding maintenance risk. | httpx direct calls |
uvicorn without [standard] |
Base uvicorn lacks websocket support and other extensions. | uvicorn[standard] (included in fastapi[standard]) |
| Separate nginx container | Adds operational complexity without benefit for single-service subpath deploy. | FastAPI serves static files directly. |
Stack Patterns by Variant
For development (local):
- Run FastAPI with
uvicorn main:app --reloadon port 8000 - Run Vite dev server on port 5173 with proxy:
vite.config.ts→proxy: { '/api': 'http://localhost:8000' } - No Docker needed locally
For VPS subpath deployment (lab.mlhub.it/postgenerator/):
- React must be built with
base: '/postgenerator/'invite.config.ts - FastAPI must handle requests prefixed with
/postgenerator/(via reverse proxy or app root_path) - Set
root_path="/postgenerator"in uvicorn or via--root-path /postgeneratorflag - lab-router nginx handles the subpath routing
For bulk generation (many carousels in one job):
- Use FastAPI
BackgroundTasksfor async job processing - Store job state in a JSON file (file-based, no DB)
- Frontend polls
/api/jobs/{id}/statuswith react-query - Streaming SSE optional for real-time token display
Version Compatibility
| Package | Compatible With | Notes |
|---|---|---|
| fastapi==0.135.1 | pydantic>=2.0, starlette>=0.46.0 | v0.100+ requires pydantic v2. pydantic v1 not supported. |
| anthropic==0.84.0 | Python>=3.8 | Fully supports async via AsyncAnthropic(). |
| tailwindcss==4.2.1 | @tailwindcss/vite==4.x | v4 requires the Vite plugin, NOT PostCSS plugin (different from v3). |
| react==19.2.4 | react-router-dom>=7, @tanstack/react-query>=5 | React 19 has breaking changes from 18. Verify shadcn/ui component compatibility. |
| shadcn/ui | react>=18, tailwindcss>=3 | shadcn/ui officially supports Tailwind v4 since early 2025. Use npx shadcn@latest init. |
| python-multipart==0.0.22 | Python>=3.10 | Required for FastAPI file uploads. Requires py3.10+. |
Sources
- PyPI - fastapi — v0.135.1 verified March 1, 2026 (HIGH confidence)
- PyPI - anthropic — v0.84.0 verified February 25, 2026 (HIGH confidence)
- PyPI - uvicorn — v0.41.0 verified February 16, 2026 (HIGH confidence)
- PyPI - pydantic — v2.12.5 verified (HIGH confidence)
- PyPI - python-dotenv — v1.2.2 verified March 1, 2026 (HIGH confidence)
- PyPI - httpx — v0.28.1 verified December 6, 2024 (HIGH confidence)
- PyPI - python-multipart — v0.0.22 verified January 25, 2026 (HIGH confidence)
- npm - react — v19.2.4 (HIGH confidence, per WebSearch)
- npm - tailwindcss — v4.2.1 verified ~February 2026 (HIGH confidence)
- npm - vite — v7.x confirmed stable 2026 (MEDIUM confidence — WebSearch shows 7.3.1, v8 beta in progress)
- Anthropic Streaming Docs — Streaming patterns verified directly (HIGH confidence)
- Unsplash API Docs — Rate limits 50 req/hr demo, 5000 prod (HIGH confidence)
- FastAPI Static Files Docs — SPA serving pattern (HIGH confidence)
- Tailwind CSS v4 Announcement — v4 features and migration (HIGH confidence)
Stack research for: PostGenerator — Instagram carousel automation (Python + FastAPI + React + Claude API) Researched: 2026-03-07