Rutas HTML con HTMLResponse y datos dinámicos

Intermedio
FastAPI
FastAPI
Actualizado: 15/09/2025

Configuración de Jinja2Templates y rutas básicas

La integración de Jinja2Templates con FastAPI permite crear aplicaciones web dinámicas que combinan la flexibilidad de los templates HTML con la velocidad de FastAPI. Esta configuración establece la base para renderizar páginas web completas desde nuestras rutas.

Configuración del motor de templates

El primer paso consiste en configurar el objeto Jinja2Templates que actuará como nuestro motor de renderizado. Esta configuración debe realizarse una sola vez en nuestra aplicación:

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

app = FastAPI()

# Configuración del motor de templates
templates = Jinja2Templates(directory="templates")

La instancia templates se convierte en nuestro punto de acceso para renderizar cualquier archivo HTML ubicado en el directorio especificado. FastAPI buscará automáticamente los archivos .html en esta carpeta.

Estructura de directorios recomendada

Para mantener una organización clara, la estructura de proyecto debe separar templates y archivos estáticos:

proyecto/
├── main.py
├── templates/
│   ├── index.html
│   ├── about.html
│   └── base.html
└── static/
    ├── css/
    ├── js/
    └── images/

Esta estructura facilita el mantenimiento y permite que FastAPI localice rápidamente los recursos necesarios.

Creación de rutas con templates

Las rutas que renderizan templates siguen un patrón específico que incluye el objeto Request como parámetro obligatorio:

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    return templates.TemplateResponse(
        name="index.html",
        context={"request": request}
    )

El parámetro request es esencial porque Jinja2Templates lo necesita para construir URLs correctas y acceder a información de la petición HTTP.

Uso de HTMLResponse

Aunque no es obligatorio, especificar HTMLResponse como clase de respuesta mejora la documentación automática y garantiza que el navegador interprete correctamente el contenido:

@app.get("/about", response_class=HTMLResponse)
async def about_page(request: Request):
    return templates.TemplateResponse(
        name="about.html",
        context={"request": request, "title": "Acerca de nosotros"}
    )

La inclusión de response_class=HTMLResponse indica explícitamente que la ruta devuelve contenido HTML, lo cual aparecerá correctamente documentado en la interfaz automática de FastAPI.

Patrón básico para múltiples rutas

Un enfoque escalable para organizar múltiples rutas de templates sigue este patrón consistente:

@app.get("/services", response_class=HTMLResponse)
async def services(request: Request):
    return templates.TemplateResponse("services.html", {"request": request})

@app.get("/contact", response_class=HTMLResponse)
async def contact(request: Request):
    return templates.TemplateResponse("contact.html", {"request": request})

@app.get("/portfolio", response_class=HTMLResponse)
async def portfolio(request: Request):
    return templates.TemplateResponse("portfolio.html", {"request": request})

Este patrón estandariza la creación de rutas y facilita el mantenimiento del código a medida que la aplicación crece.

Template básico de ejemplo

Un template HTML mínimo para trabajar con FastAPI debe incluir la estructura básica de HTML5:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title | default('Mi aplicación FastAPI') }}</title>
</head>
<body>
    <header>
        <h1>Bienvenido a FastAPI</h1>
        <nav>
            <a href="/">Inicio</a>
            <a href="/about">Acerca de</a>
            <a href="/contact">Contacto</a>
        </nav>
    </header>
    
    <main>
        <h2>Contenido principal</h2>
        <p>Esta página está renderizada con Jinja2Templates.</p>
    </main>
</body>
</html>

La sintaxis {{ title | default('Mi aplicación FastAPI') }} utiliza filtros de Jinja2 para proporcionar valores por defecto cuando una variable no está definida en el contexto.

Configuración con subdirectorios

Para proyectos más complejos, es posible organizar templates en subdirectorios y referenciarlos usando rutas relativas:

# Estructura: templates/pages/home.html
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request):
    return templates.TemplateResponse(
        "pages/dashboard.html",
        {"request": request}
    )

Esta aproximación permite categorizar templates por funcionalidad y mantener una estructura de proyecto más organizada.

Datos dinámicos en plantillas HTML

La verdadera potencia de los templates Jinja2 surge cuando combinamos datos dinámicos provenientes de nuestras rutas FastAPI. Esta integración permite crear páginas web que respondan en tiempo real a la información de la aplicación.

Pasando variables al contexto

El diccionario context es el mecanismo principal para enviar datos desde FastAPI hacia nuestros templates. Cualquier variable incluida en este diccionario estará disponible en el template HTML:

from datetime import datetime
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/usuario/{nombre}", response_class=HTMLResponse)
async def perfil_usuario(request: Request, nombre: str):
    # Datos dinámicos que se envían al template
    datos_usuario = {
        "request": request,
        "nombre": nombre.capitalize(),
        "fecha_actual": datetime.now(),
        "edad": 25,
        "activo": True
    }
    
    return templates.TemplateResponse("perfil.html", datos_usuario)

Cada clave del diccionario se convierte en una variable accesible dentro del template usando la sintaxis {{ nombre_variable }}.

Sintaxis básica para mostrar datos

Los templates Jinja2 utilizan una sintaxis específica para diferentes tipos de operaciones. Las dobles llaves {{ }} permiten mostrar valores de variables:

<!DOCTYPE html>
<html lang="es">
<head>
    <title>Perfil de {{ nombre }}</title>
</head>
<body>
    <h1>Bienvenido, {{ nombre }}</h1>
    <p>Tu edad es: {{ edad }} años</p>
    <p>Fecha actual: {{ fecha_actual.strftime('%d/%m/%Y') }}</p>
    
    {% if activo %}
        <span class="badge-active">Usuario activo</span>
    {% else %}
        <span class="badge-inactive">Usuario inactivo</span>
    {% endif %}
</body>
</html>

La combinación de {{ }} para mostrar datos y {% %} para lógica de control proporciona flexibilidad completa en el renderizado.

Trabajando con listas y bucles

Los datos de tipo lista son extremadamente comunes en aplicaciones web. Jinja2 proporciona la directiva {% for %} para iterar sobre colecciones:

@app.get("/productos", response_class=HTMLResponse)
async def lista_productos(request: Request):
    productos = [
        {"nombre": "Laptop", "precio": 899.99, "stock": 15},
        {"nombre": "Mouse", "precio": 29.99, "stock": 50},
        {"nombre": "Teclado", "precio": 79.99, "stock": 0},
        {"nombre": "Monitor", "precio": 249.99, "stock": 8}
    ]
    
    return templates.TemplateResponse(
        "productos.html", 
        {"request": request, "productos": productos, "total": len(productos)}
    )

El template correspondiente puede procesar cada elemento de la lista individualmente:

<div class="productos-container">
    <h2>Catálogo de productos ({{ total }} artículos)</h2>
    
    {% for producto in productos %}
        <div class="producto-card">
            <h3>{{ producto.nombre }}</h3>
            <p class="precio">${{ producto.precio }}</p>
            
            {% if producto.stock > 0 %}
                <p class="stock disponible">En stock: {{ producto.stock }} unidades</p>
                <button class="btn-comprar">Agregar al carrito</button>
            {% else %}
                <p class="stock agotado">Producto agotado</p>
                <button class="btn-disabled" disabled>No disponible</button>
            {% endif %}
        </div>
    {% endfor %}
</div>

Parámetros de consulta y datos dinámicos

Los query parameters permiten crear páginas altamente personalizables que respondan a la entrada del usuario:

@app.get("/buscar", response_class=HTMLResponse)
async def buscar_articulos(request: Request, q: str = "", categoria: str = "todas"):
    # Simulamos una búsqueda en base de datos
    articulos_encontrados = [
        {"titulo": f"Artículo sobre {q}", "categoria": categoria, "fecha": "2025-01-15"},
        {"titulo": f"Guía de {q}", "categoria": categoria, "fecha": "2025-01-10"}
    ] if q else []
    
    return templates.TemplateResponse("busqueda.html", {
        "request": request,
        "termino_busqueda": q,
        "categoria_seleccionada": categoria,
        "resultados": articulos_encontrados,
        "hay_resultados": len(articulos_encontrados) > 0
    })

El template puede adaptar su contenido según los parámetros recibidos:

<div class="busqueda-container">
    <h2>Resultados de búsqueda</h2>
    
    {% if termino_busqueda %}
        <p>Buscando: <strong>{{ termino_busqueda }}</strong> en {{ categoria_seleccionada }}</p>
        
        {% if hay_resultados %}
            <div class="resultados">
                {% for articulo in resultados %}
                    <article class="resultado-item">
                        <h3>{{ articulo.titulo }}</h3>
                        <small>Categoría: {{ articulo.categoria }} | {{ articulo.fecha }}</small>
                    </article>
                {% endfor %}
            </div>
        {% else %}
            <div class="sin-resultados">
                <p>No se encontraron resultados para "{{ termino_busqueda }}"</p>
                <p>Intenta con términos diferentes o explora otras categorías.</p>
            </div>
        {% endif %}
    {% else %}
        <p>Introduce un término de búsqueda para comenzar.</p>
    {% endif %}
</div>

Filtros y formateo de datos

Jinja2 incluye filtros integrados que permiten transformar datos directamente en el template sin modificar la lógica de negocio:

@app.get("/estadisticas", response_class=HTMLResponse)
async def estadisticas(request: Request):
    datos_estadisticas = {
        "request": request,
        "ventas_mes": [1250.75, 890.50, 2100.25, 750.00],
        "fecha_reporte": datetime.now(),
        "porcentaje_crecimiento": 0.1547,
        "descripcion": "   reporte mensual de ventas   ",
        "productos_populares": ["laptop", "mouse", "teclado"]
    }
    
    return templates.TemplateResponse("estadisticas.html", datos_estadisticas)

Los filtros se aplican usando la sintaxis de tubería |:

<div class="estadisticas">
    <h2>{{ descripcion | trim | title }}</h2>
    <p>Generado el: {{ fecha_reporte.strftime('%d de %B, %Y') }}</p>
    
    <div class="metricas">
        <p>Ventas totales: ${{ ventas_mes | sum | round(2) }}</p>
        <p>Promedio mensual: ${{ (ventas_mes | sum / ventas_mes | length) | round(2) }}</p>
        <p>Crecimiento: {{ (porcentaje_crecimiento * 100) | round(1) }}%</p>
    </div>
    
    <div class="productos-top">
        <h3>Productos más populares:</h3>
        <ul>
            {% for producto in productos_populares %}
                <li>{{ loop.index }}. {{ producto | title }}</li>
            {% endfor %}
        </ul>
    </div>
</div>

Variables de contexto avanzadas

Para aplicaciones complejas, es útil estructurar el contexto de manera que proporcione acceso a múltiples niveles de información:

@app.get("/dashboard/{usuario_id}", response_class=HTMLResponse)
async def dashboard_usuario(request: Request, usuario_id: int):
    # Simulamos datos complejos de diferentes fuentes
    contexto_dashboard = {
        "request": request,
        "usuario": {
            "id": usuario_id,
            "nombre": "Ana García",
            "rol": "administrador",
            "ultimo_acceso": datetime.now().strftime('%d/%m/%Y %H:%M')
        },
        "notificaciones": [
            {"tipo": "info", "mensaje": "Nuevo pedido recibido", "urgente": False},
            {"tipo": "warning", "mensaje": "Stock bajo en 3 productos", "urgente": True},
            {"tipo": "success", "mensaje": "Backup completado", "urgente": False}
        ],
        "configuracion": {
            "tema_oscuro": True,
            "notificaciones_email": True,
            "idioma": "es"
        }
    }
    
    return templates.TemplateResponse("dashboard.html", contexto_dashboard)

El template puede acceder a propiedades anidadas usando la notación de punto:

<div class="dashboard">
    <header class="dashboard-header">
        <h1>Dashboard - {{ usuario.nombre }}</h1>
        <span class="rol">{{ usuario.rol | title }}</span>
        <small>Último acceso: {{ usuario.ultimo_acceso }}</small>
    </header>
    
    <section class="notificaciones">
        <h2>Notificaciones</h2>
        {% for notif in notificaciones %}
            <div class="notificacion {{ notif.tipo }} {{ 'urgente' if notif.urgente }}">
                {{ notif.mensaje }}
            </div>
        {% endfor %}
    </section>
    
    <section class="configuracion">
        <h3>Configuración actual</h3>
        <ul>
            <li>Tema: {{ "Oscuro" if configuracion.tema_oscuro else "Claro" }}</li>
            <li>Email: {{ "Activado" if configuracion.notificaciones_email else "Desactivado" }}</li>
            <li>Idioma: {{ configuracion.idioma | upper }}</li>
        </ul>
    </section>
</div>

Esta aproximación separa claramente la lógica de datos en Python de la presentación en HTML, manteniendo el código organizado y fácil de mantener.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en FastAPI

Documentación oficial de FastAPI
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, FastAPI es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de FastAPI

Explora más contenido relacionado con FastAPI y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Configurar Jinja2Templates para renderizar templates HTML en FastAPI.
  • Crear rutas que devuelvan respuestas HTML usando HTMLResponse.
  • Pasar datos dinámicos desde FastAPI a los templates para personalizar el contenido.
  • Utilizar sintaxis y filtros de Jinja2 para mostrar y formatear datos en plantillas.
  • Organizar templates en subdirectorios y manejar contextos complejos con variables anidadas.