Week 12 of 16

Build: Prompt Vault REST API

Pydantic models, automatic validation, proper HTTP status codes — the real production-grade API for the DVP Prompt Vault.

Day 58 75 minutes Build

Day 58 of 80

Pydantic Models: Data Contracts

What Pydantic Does

A Pydantic model is a class that describes what data should look like — its fields, their types, and any custom rules. FastAPI uses these models to automatically validate incoming requests before they reach your code.

Think of it as a contract: "If you want to create a prompt, the request body must have these fields, and they must be these types." If the request doesn't match, FastAPI returns a clear 422 error — you never write the validation logic yourself.

You'll define two models today:

Build: api.py

This is the full production API. It connects to your existing SQLite database via the Database class you built in earlier weeks. Create this file in your DVP Prompt Vault project root.

api.py — imports and models python
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel, field_validator
from typing import Optional
from database import Database

app = FastAPI(
    title="DVP Prompt Vault API",
    description="REST API for managing AI video generation prompts",
    version="1.0.0"
)

db = Database()

# ─── Pydantic Models ───────────────────────────────────────────

class PromptCreate(BaseModel):
    """Shape of data required to create a new prompt."""
    platform: str
    shot: str
    prompt_text: str

    @field_validator("platform")
    @classmethod
    def platform_must_be_valid(cls, v):
        allowed = ["Kling", "Runway", "Veo", "Pika", "Sora"]
        if v not in allowed:
            raise ValueError(f"Platform must be one of: {', '.join(allowed)}")
        return v

    @field_validator("shot", "prompt_text")
    @classmethod
    def must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError("Field cannot be empty or whitespace")
        return v.strip()


class PromptResponse(BaseModel):
    """Shape of data returned by the API for a prompt."""
    id: int
    platform: str
    shot: str
    prompt_text: str
    created_at: Optional[str] = None
The @field_validator decorator runs your custom check after Pydantic's basic type check passes. If your validator raises a ValueError, Pydantic converts it into a clean 422 response with the error message you wrote.
api.py — routes python
# ─── Routes ────────────────────────────────────────────────────

@app.get("/prompts", response_model=list[PromptResponse])
def get_prompts(platform: Optional[str] = None):
    """Return all prompts, with optional platform filter."""
    try:
        if platform:
            results = db.get_prompts_by_platform(platform)
        else:
            results = db.get_all_prompts()
        return results
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/prompts/{prompt_id}", response_model=PromptResponse)
def get_prompt(prompt_id: int):
    """Return a single prompt by ID."""
    prompt = db.get_prompt(prompt_id)
    if not prompt:
        raise HTTPException(status_code=404, detail=f"Prompt {prompt_id} not found")
    return prompt


@app.post("/prompts", response_model=PromptResponse, status_code=201)
def create_prompt(prompt: PromptCreate):
    """Create a new prompt. Returns 201 Created on success."""
    try:
        new_id = db.add_prompt(
            platform=prompt.platform,
            shot=prompt.shot,
            prompt_text=prompt.prompt_text
        )
        return db.get_prompt(new_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.delete("/prompts/{prompt_id}", status_code=204)
def delete_prompt(prompt_id: int):
    """Delete a prompt. Returns 204 No Content on success."""
    prompt = db.get_prompt(prompt_id)
    if not prompt:
        raise HTTPException(status_code=404, detail=f"Prompt {prompt_id} not found")
    db.delete_prompt(prompt_id)
    return None  # 204 = No Content, intentionally empty response


@app.get("/stats")
def get_stats():
    """Return summary statistics about the prompt vault."""
    all_prompts = db.get_all_prompts()
    platforms = {}
    for p in all_prompts:
        platform = p["platform"]
        platforms[platform] = platforms.get(platform, 0) + 1
    return {
        "total_prompts": len(all_prompts),
        "by_platform": platforms,
        "platforms_count": len(platforms)
    }
HTTPException returns a proper JSON error response with the correct HTTP status code. 404 = resource not found. 422 = validation error (FastAPI generates these automatically). 500 = server error. Always use HTTPException instead of returning error dicts manually.
HTTP Status Codes That Matter

Every HTTP response has a status code. These are the ones your API uses today:

What response_model Does

When you add response_model=PromptResponse to a route decorator, FastAPI does two things:

  1. Documents the response — The /docs UI shows exactly what fields the endpoint returns, including their types.
  2. Filters the output — Even if your database returns extra fields (like internal flags or hashes), FastAPI will only include the fields defined in PromptResponse. This is a security feature — you can't accidentally expose data you didn't intend to.

Test All Endpoints in /docs

uvicorn api:app --reload

Open http://localhost:8000/docs and run through each endpoint:

  1. GET /stats — run this first to see how many prompts exist
  2. GET /prompts — returns all prompts
  3. GET /prompts?platform=Kling — filter by platform
  4. GET /prompts/{id} — try a real ID, then try a fake one (should get 404)
  5. POST /prompts — create a new prompt with valid data
  6. POST /prompts — try sending an invalid platform like "Instagram" (should get 422)
  7. DELETE /prompts/{id} — delete the one you just created
If You Don't Have a Database Class Yet

Replace the db = Database() lines with the in-memory prompts list from Day 57. The goal today is understanding the routing and validation patterns — you can swap in the real database once you've confirmed the API structure works correctly.

End of Day Checklist

Tomorrow — Day 59: Connect the Frontend

You'll build a simple HTML page that uses fetch() to call your FastAPI backend. This closes the loop: Python API serving JSON to a JavaScript frontend — the same pattern used in every modern web app.