Week 12 of 16

Build: Connect the Frontend

Python API meets JavaScript fetch() — close the loop between your FastAPI backend and a live HTML interface.

Day 59 60 minutes Build

Day 59 of 80

Two Ways to Connect

Option 1 vs Option 2

Option 1: Update Flask's app.py — Your existing Flask app can make requests to the FastAPI API using the requests library. Flask handles the HTML; FastAPI handles the data. This is a microservices pattern — two separate servers running at different ports.

Option 2: Pure HTML + JavaScript fetch() — A plain HTML file that talks directly to the FastAPI backend using browser-native fetch(). No Flask required. The browser is the frontend; FastAPI is the backend. This is the most common pattern for modern web apps.

Today you'll build Option 2. Marc already knows JavaScript, so fetch() is familiar territory — you're just learning to point it at a Python API.

The Connection

Here's the full data flow:

Python (FastAPI) → JSON response → JavaScript fetch() → DOM update → HTML visible to user

FastAPI doesn't know or care what the frontend looks like. It just returns JSON. JavaScript doesn't know or care that the backend is Python. It just receives JSON. This separation is what makes REST APIs powerful — you can replace the frontend or the backend independently.

CORS: You'll Need This

When a browser page at one origin (e.g., file://index.html) calls an API at another origin (localhost:8000), the browser blocks it by default. This is CORS — Cross-Origin Resource Sharing. Add this to api.py before you test the frontend:

api.py — add CORS middleware python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(title="DVP Prompt Vault API")

# Allow any origin in development — tighten this in production
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)
In production, replace ["*"] with your actual frontend domain: ["https://dynamicvibe.net"]. For local development, wildcard is fine.

Build: index_api.html

Create this file in your project folder. It's a standalone HTML file — no server needed to serve it, just open it in your browser while api.py is running.

index_api.html — HTML structure html
<!-- index_api.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>DVP Prompt Vault</title>
  <style>
    body { font-family: var(--font-body, 'Hanken Grotesk', system-ui, sans-serif); max-width: 800px; margin: 40px auto; padding: 0 20px; }
    .prompt-card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; margin: 12px 0; }
    .platform-badge { background: #6366f1; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
    button { background: #6366f1; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; }
    input, textarea { width: 100%; padding: 8px; margin: 4px 0 12px; border: 1px solid #e2e8f0; border-radius: 6px; }
  </style>
</head>
<body>
  <h1>DVP Prompt Vault</h1>

  <section id="add-prompt">
    <h2>Add Prompt</h2>
    <input id="platform" placeholder="Platform (Kling, Runway, Veo...)">
    <input id="shot" placeholder="Shot description">
    <textarea id="prompt-text" rows="3" placeholder="Full prompt text"></textarea>
    <button onclick="addPrompt()">Add Prompt</button>
  </section>

  <section id="prompts-list">
    <h2>All Prompts <span id="count"></span></h2>
    <div id="prompts">Loading...</div>
  </section>

  <script src="app.js"></script>
</body>
</html>
The HTML is just structure — no data baked in. All the data comes from the API dynamically. This means the page always shows the current state of your database.
app.js — fetch functions javascript
const API_BASE = 'http://localhost:8000';

// Fetch all prompts from the API and display them
async function loadPrompts() {
    const response = await fetch(`${API_BASE}/prompts`);
    const prompts = await response.json();

    document.getElementById('count').textContent = `(${prompts.length})`;
    displayPrompts(prompts);
}

function displayPrompts(prompts) {
    const container = document.getElementById('prompts');
    if (prompts.length === 0) {
        container.innerHTML = '<p>No prompts yet.</p>';
        return;
    }
    container.innerHTML = prompts.map(p => `
        <div class="prompt-card">
            <span class="platform-badge">${p.platform}</span>
            <strong> ${p.shot}</strong>
            <p>${p.prompt_text}</p>
            <button onclick="deletePrompt(${p.id})">Delete</button>
        </div>
    `).join('');
}

// Create a new prompt via POST /prompts
async function addPrompt() {
    const platform = document.getElementById('platform').value;
    const shot = document.getElementById('shot').value;
    const promptText = document.getElementById('prompt-text').value;

    const response = await fetch(`${API_BASE}/prompts`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
            platform,
            shot,
            prompt_text: promptText
        })
    });

    if (response.ok) {
        // Clear form fields
        document.getElementById('platform').value = '';
        document.getElementById('shot').value = '';
        document.getElementById('prompt-text').value = '';
        loadPrompts(); // refresh the list
    } else {
        const error = await response.json();
        alert(`Error: ${error.detail}`);
    }
}

// Delete a prompt via DELETE /prompts/{id}
async function deletePrompt(id) {
    const response = await fetch(`${API_BASE}/prompts/${id}`, {
        method: 'DELETE'
    });
    if (response.ok) {
        loadPrompts();
    }
}

// Load prompts when the page opens
loadPrompts();
When addPrompt() fails (e.g., invalid platform name), response.ok is false. The error.detail is the message you put in HTTPException back in api.py. Your Python validation error flows cleanly to the JavaScript error handler.

Stretch Goal: /generate Endpoint

If you finish early, add a /generate endpoint to api.py that calls Claude and returns a suggested prompt. This sets up exactly what you'll build in Week 13 with async.

api.py — /generate stretch goal python
import anthropic
from dotenv import load_dotenv

load_dotenv()
claude = anthropic.Anthropic()

class GenerateRequest(BaseModel):
    shot: str
    platform: str = "Kling"

@app.post("/generate")
def generate_prompt(data: GenerateRequest):
    """Ask Claude to generate a video prompt for a given shot and platform."""
    message = claude.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=200,
        messages=[{
            "role": "user",
            "content": f"Write a {data.platform} AI video generation prompt for: {data.shot}. Under 75 words."
        }]
    )
    return {
        "shot": data.shot,
        "platform": data.platform,
        "generated_prompt": message.content[0].text
    }
This is synchronous — it waits for Claude before responding. In Week 13 you'll convert this to async def and use AsyncAnthropic() to generate for multiple platforms in parallel.

End of Day Checklist

Tomorrow — Day 60: Week 12 Review

You'll consolidate everything from this week — FastAPI routing, Pydantic validation, HTTP methods, and the frontend connection — before Week 13 takes your API async.