Servir paginas estaticas con FastAPI

Hasta ahora aprendimos a crear endpoints con GET que devuelven datos en JSON. El siguiente paso es aprender a servir páginas HTML, CSS y JS desde FastAPI, de modo que puedas ver tus APIs funcionando dentro de una página web.

Objetivos de la sección

  • Entender qué significa contenido estático (HTML, CSS, JS, imágenes).
  • Servir archivos estáticos con FastAPI usando StaticFiles.
  • Crear una página HTML que use fetch en JavaScript para consumir los endpoints /chiste y /chistes de la API de chistes.
  • Ver cómo el frontend (HTML) se comunica con el backend (FastAPI).

Paso 1: API de chistes (JSON) + página en la raíz

En main.py definimos dos endpoints de API y configuramos la raíz para devolver el index.html.

from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pathlib import Path
import random

app = FastAPI(title="API de Chistes")

BASE_DIR = Path(__file__).resolve().parent
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")

chistes = [
    "Por qué los programadores confunden Halloween y Navidad? Porque OCT 31 == DEC 25.",
    "¡Camarero! Ese filete tiene muchos nervios. Pues normal, es la primera vez que se lo comen.",
    "¿Cómo se despiden los químicos? ¡Ácido un placer.",
    "Estás obsesionado con la comida, de verdad. ¿Qué dices croquetamente?",
    "¿Cuál es el café más peligroso del mundo? El ex-preso."
]

# Raíz -> devuelve la página HTML
@app.get("/", response_class=FileResponse)
def index():
    return FileResponse(BASE_DIR / "static" / "index.html")

# API -> todos los chistes
@app.get("/chistes")
def obtener_chistes():
    return {"chistes": chistes}

# API -> chiste aleatorio
@app.get("/chiste")
def obtener_chiste():
    return {"chiste": random.choice(chistes)}

Paso 2: Montar carpeta estática

Ya quedó montada en el código anterior con:

app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")

Esto hace que cualquier archivo dentro de static/ se pueda acceder como http://127.0.0.1:8000/static/...

Paso 3: Página HTML (cliente)

Crear static/index.html con este contenido:

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <title>Chistes con FastAPI</title>
  <link rel="stylesheet" href="/static/style.css">
</head>
<body>
  <h1>API de Chistes  Cliente HTML</h1>

  <section>
    <h2>Todos los chistes</h2>
    <div id="lista-chistes">Cargando...</div>
  </section>

  <section>
    <h2>Chiste aleatorio</h2>
    <button id="btn-aleatorio">Obtener chiste</button>
    <div id="chiste-aleatorio"></div>
  </section>

  <script src="/static/app.js"></script>
</body>
</html>

Paso 4: CSS básico

Archivo static/style.css:

body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: darkblue; }
section { margin-bottom: 20px; }
#lista-chistes { border: 1px solid #ccc; padding: 10px; }
#chiste-aleatorio { margin-top: 10px; font-style: italic; }
button { padding: 5px 10px; cursor: pointer; }

Paso 5: JavaScript con fetch (usando /chistes y /chiste)

Archivo static/app.js:

async function cargarChistes() {
  const res = await fetch("/chistes");
  const data = await res.json();
  const div = document.getElementById("lista-chistes");
  div.innerHTML = "";
  (data.chistes || []).forEach((chiste, idx) => {
    const p = document.createElement("p");
    p.textContent = `${idx + 1}. ${chiste}`;
    div.appendChild(p);
  });
}

async function cargarChisteAleatorio() {
  const res = await fetch("/chiste");
  const data = await res.json();
  document.getElementById("chiste-aleatorio").textContent = data.chiste ?? "";
}

document.getElementById("btn-aleatorio").addEventListener("click", cargarChisteAleatorio);

cargarChistes();

Nuestra aplicación queda con los siguientes archivos:

archivos estáticos y aplicación Python

Si accedemos a la raiz del sitio, se nos retorna la página index.html

Página HTML de chistes consumiendo la API y pagina estática

Listo. Al abrir http://127.0.0.1:8000/ se carga index.html, que a su vez hace fetch a /chistes y /chiste para mostrar los datos.