Creación de endpoints API internos
En las aplicaciones web modernas, es común separar la lógica de presentación de la lógica de datos creando endpoints API que devuelvan información en formato JSON. Estos endpoints internos permiten que las páginas HTML consuman datos de manera dinámica sin recargar toda la página.
Los endpoints API internos son rutas que devuelven datos estructurados en JSON, diseñadas específicamente para ser consumidas por el frontend de la misma aplicación. A diferencia de las rutas que renderizan templates HTML, estos endpoints se centran únicamente en proporcionar datos.
Diferencias entre rutas HTML y API
La principal diferencia radica en el tipo de respuesta que generan:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
# Ruta HTML tradicional
@app.get("/usuarios", response_class=HTMLResponse)
async def pagina_usuarios(request: Request):
usuarios = [
{"id": 1, "nombre": "Ana", "email": "ana@ejemplo.com"},
{"id": 2, "nombre": "Carlos", "email": "carlos@ejemplo.com"}
]
return templates.TemplateResponse("usuarios.html", {
"request": request,
"usuarios": usuarios
})
# Endpoint API interno
@app.get("/api/usuarios")
async def obtener_usuarios():
usuarios = [
{"id": 1, "nombre": "Ana", "email": "ana@ejemplo.com"},
{"id": 2, "nombre": "Carlos", "email": "carlos@ejemplo.com"}
]
return usuarios
El endpoint API devuelve directamente los datos en formato JSON, mientras que la ruta HTML renderiza un template con esos datos.
Organización de rutas API
Es recomendable agrupar las rutas API bajo un prefijo común para mantener una estructura clara:
from fastapi import APIRouter
# Router específico para endpoints API
api_router = APIRouter(prefix="/api", tags=["API"])
@api_router.get("/productos")
async def listar_productos():
productos = [
{"id": 1, "nombre": "Laptop", "precio": 899.99},
{"id": 2, "nombre": "Mouse", "precio": 29.99},
{"id": 3, "nombre": "Teclado", "precio": 79.99}
]
return {"productos": productos}
@api_router.get("/productos/{producto_id}")
async def obtener_producto(producto_id: int):
# Simulamos búsqueda de producto
productos = {
1: {"id": 1, "nombre": "Laptop", "precio": 899.99, "stock": 15},
2: {"id": 2, "nombre": "Mouse", "precio": 29.99, "stock": 50},
3: {"id": 3, "nombre": "Teclado", "precio": 79.99, "stock": 25}
}
producto = productos.get(producto_id)
if not producto:
return {"error": "Producto no encontrado"}
return {"producto": producto}
# Registrar el router en la aplicación principal
app.include_router(api_router)
Endpoints con diferentes métodos HTTP
Los endpoints API internos pueden manejar diferentes operaciones usando los métodos HTTP apropiados:
# Lista de tareas en memoria para el ejemplo
tareas = [
{"id": 1, "titulo": "Aprender FastAPI", "completada": False},
{"id": 2, "titulo": "Crear API interna", "completada": True}
]
@api_router.get("/tareas")
async def listar_tareas():
return {"tareas": tareas}
@api_router.post("/tareas")
async def crear_tarea(titulo: str):
nueva_tarea = {
"id": len(tareas) + 1,
"titulo": titulo,
"completada": False
}
tareas.append(nueva_tarea)
return {"mensaje": "Tarea creada", "tarea": nueva_tarea}
@api_router.put("/tareas/{tarea_id}")
async def actualizar_tarea(tarea_id: int, completada: bool = None):
for tarea in tareas:
if tarea["id"] == tarea_id:
if completada is not None:
tarea["completada"] = completada
return {"mensaje": "Tarea actualizada", "tarea": tarea}
return {"error": "Tarea no encontrada"}
@api_router.delete("/tareas/{tarea_id}")
async def eliminar_tarea(tarea_id: int):
for i, tarea in enumerate(tareas):
if tarea["id"] == tarea_id:
tarea_eliminada = tareas.pop(i)
return {"mensaje": "Tarea eliminada", "tarea": tarea_eliminada}
return {"error": "Tarea no encontrada"}
Estructuración de respuestas JSON
Las respuestas consistentes facilitan el consumo desde el frontend. Es buena práctica mantener una estructura predecible:
from typing import List, Dict, Any
@api_router.get("/estadisticas")
async def obtener_estadisticas():
# Estructura de respuesta consistente
return {
"success": True,
"data": {
"total_usuarios": 150,
"usuarios_activos": 98,
"nuevos_registros": 12
},
"timestamp": "2025-01-08T10:30:00Z"
}
@api_router.get("/buscar")
async def buscar_elementos(query: str):
# Simulamos búsqueda
resultados = [
{"id": 1, "tipo": "usuario", "nombre": "Ana López"},
{"id": 2, "tipo": "producto", "nombre": "Laptop Gaming"}
]
return {
"success": True,
"data": {
"query": query,
"resultados": resultados,
"total": len(resultados)
}
}
Manejo de errores en endpoints API
Los errores estructurados proporcionan información clara para el frontend:
from fastapi import HTTPException
@api_router.get("/usuario/{usuario_id}")
async def obtener_usuario(usuario_id: int):
# Simulamos base de datos de usuarios
usuarios_db = {
1: {"id": 1, "nombre": "Ana", "activo": True},
2: {"id": 2, "nombre": "Carlos", "activo": False}
}
if usuario_id not in usuarios_db:
return {
"success": False,
"error": {
"code": "USER_NOT_FOUND",
"message": "El usuario solicitado no existe"
}
}
usuario = usuarios_db[usuario_id]
if not usuario["activo"]:
return {
"success": False,
"error": {
"code": "USER_INACTIVE",
"message": "El usuario está desactivado"
}
}
return {
"success": True,
"data": {"usuario": usuario}
}
Endpoints con parámetros de consulta
Los parámetros de consulta permiten filtrar y paginar los resultados:
from typing import Optional
@api_router.get("/articulos")
async def listar_articulos(
categoria: Optional[str] = None,
limite: int = 10,
pagina: int = 1
):
# Simulamos datos de artículos
todos_articulos = [
{"id": 1, "titulo": "Introducción a FastAPI", "categoria": "programacion"},
{"id": 2, "titulo": "Recetas de cocina", "categoria": "gastronomia"},
{"id": 3, "titulo": "Desarrollo web moderno", "categoria": "programacion"},
{"id": 4, "titulo": "Técnicas de fotografía", "categoria": "arte"}
]
# Filtrar por categoría si se especifica
articulos = todos_articulos
if categoria:
articulos = [a for a in articulos if a["categoria"] == categoria]
# Calcular paginación
inicio = (pagina - 1) * limite
fin = inicio + limite
articulos_pagina = articulos[inicio:fin]
return {
"success": True,
"data": {
"articulos": articulos_pagina,
"pagina_actual": pagina,
"total_articulos": len(articulos),
"total_paginas": (len(articulos) + limite - 1) // limite
}
}
Ejemplo completo de aplicación
Aquí tienes un ejemplo que integra rutas HTML y API trabajando juntas:
from fastapi import FastAPI, Request, APIRouter
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# Configuración de archivos estáticos y templates
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
# Router para rutas HTML
html_router = APIRouter()
# Router para endpoints API
api_router = APIRouter(prefix="/api", tags=["API"])
# Datos en memoria para el ejemplo
noticias = [
{"id": 1, "titulo": "FastAPI 0.100 disponible", "resumen": "Nueva versión estable"},
{"id": 2, "titulo": "Python 3.13 lanzado", "resumen": "Mejoras en rendimiento"}
]
# Ruta HTML que renderiza la página principal
@html_router.get("/", response_class=HTMLResponse)
async def pagina_principal(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
# Endpoint API que proporciona datos para la página
@api_router.get("/noticias")
async def obtener_noticias():
return {
"success": True,
"data": {"noticias": noticias}
}
@api_router.post("/noticias")
async def crear_noticia(titulo: str, resumen: str):
nueva_noticia = {
"id": len(noticias) + 1,
"titulo": titulo,
"resumen": resumen
}
noticias.append(nueva_noticia)
return {
"success": True,
"data": {"noticia": nueva_noticia}
}
# Registrar routers
app.include_router(html_router)
app.include_router(api_router)
Los endpoints API internos actúan como el puente de datos entre tu aplicación FastAPI y las páginas HTML, proporcionando una arquitectura limpia y escalable donde la lógica de presentación y la lógica de datos están claramente separadas.
Consumo con JavaScript fetch()
Una vez que tenemos nuestros endpoints API internos configurados, el siguiente paso es consumirlos desde las plantillas HTML usando JavaScript. La API fetch()
proporciona una interfaz moderna y flexible para realizar peticiones HTTP asíncronas desde el navegador.
Configuración básica en templates Jinja2
Para integrar JavaScript en nuestras plantillas Jinja2, necesitamos estructurar el HTML de manera que separe la presentación de la lógica:
<!-- templates/productos.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lista de Productos</title>
</head>
<body>
<div class="container">
<h1>Productos Disponibles</h1>
<button id="cargar-productos">Cargar Productos</button>
<div id="lista-productos">
<!-- Los productos se cargarán aquí dinámicamente -->
</div>
</div>
<script>
// JavaScript se incluye al final del body para asegurar que el DOM esté cargado
document.addEventListener('DOMContentLoaded', function() {
const botonCargar = document.getElementById('cargar-productos');
const contenedorProductos = document.getElementById('lista-productos');
botonCargar.addEventListener('click', cargarProductos);
});
async function cargarProductos() {
// Implementaremos esta función a continuación
}
</script>
</body>
</html>
Sintaxis moderna de fetch() con async/await
La sintaxis moderna de fetch() utiliza async/await para manejar las peticiones asíncronas de manera más legible:
async function cargarProductos() {
try {
// Mostrar indicador de carga
const contenedor = document.getElementById('lista-productos');
contenedor.innerHTML = '<p>Cargando productos...</p>';
// Realizar petición al endpoint API interno
const response = await fetch('/api/productos');
// Verificar si la respuesta es exitosa
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
// Convertir respuesta a JSON
const data = await response.json();
// Procesar y mostrar los datos
mostrarProductos(data.productos);
} catch (error) {
console.error('Error al cargar productos:', error);
document.getElementById('lista-productos').innerHTML =
'<p style="color: red;">Error al cargar los productos. Inténtalo de nuevo.</p>';
}
}
function mostrarProductos(productos) {
const contenedor = document.getElementById('lista-productos');
if (productos.length === 0) {
contenedor.innerHTML = '<p>No hay productos disponibles.</p>';
return;
}
const html = productos.map(producto => `
<div class="producto-item" data-id="${producto.id}">
<h3>${producto.nombre}</h3>
<p>Precio: $${producto.precio}</p>
<button onclick="verDetalle(${producto.id})">Ver Detalle</button>
</div>
`).join('');
contenedor.innerHTML = html;
}
Peticiones POST con datos de formulario
Para enviar datos al servidor, necesitamos configurar la petición con el método POST y los headers apropiados:
async function crearProducto(event) {
event.preventDefault(); // Prevenir envío tradicional del formulario
const formulario = event.target;
const formData = new FormData(formulario);
try {
const response = await fetch('/api/productos', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
nombre: formData.get('nombre'),
precio: formData.get('precio'),
categoria: formData.get('categoria')
})
});
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const resultado = await response.json();
if (resultado.success) {
mostrarMensaje('Producto creado exitosamente', 'success');
formulario.reset();
cargarProductos(); // Recargar la lista
} else {
mostrarMensaje(resultado.error.message, 'error');
}
} catch (error) {
console.error('Error al crear producto:', error);
mostrarMensaje('Error al crear el producto', 'error');
}
}
function mostrarMensaje(texto, tipo) {
const mensaje = document.createElement('div');
mensaje.className = `mensaje ${tipo}`;
mensaje.textContent = texto;
document.body.appendChild(mensaje);
// Remover mensaje después de 3 segundos
setTimeout(() => {
mensaje.remove();
}, 3000);
}
Manejo de respuestas JSON estructuradas
Cuando nuestros endpoints devuelven respuestas estructuradas, debemos procesarlas adecuadamente:
async function buscarElementos(query) {
try {
const response = await fetch(`/api/buscar?query=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.success) {
const resultados = data.data.resultados;
const total = data.data.total;
mostrarResultadosBusqueda(resultados, total, query);
} else {
mostrarError(data.error.message);
}
} catch (error) {
console.error('Error en búsqueda:', error);
mostrarError('Error al realizar la búsqueda');
}
}
function mostrarResultadosBusqueda(resultados, total, query) {
const contenedor = document.getElementById('resultados-busqueda');
let html = `<h3>Resultados para "${query}" (${total} encontrados)</h3>`;
if (resultados.length === 0) {
html += '<p>No se encontraron resultados.</p>';
} else {
html += '<ul class="lista-resultados">';
resultados.forEach(resultado => {
html += `
<li class="resultado-item">
<span class="tipo">[${resultado.tipo}]</span>
<span class="nombre">${resultado.nombre}</span>
</li>
`;
});
html += '</ul>';
}
contenedor.innerHTML = html;
}
Actualización en tiempo real con fetch()
Para crear interfaces dinámicas e interactivas, podemos combinar eventos del DOM con peticiones fetch():
// Sistema de actualización de estado de tareas
async function alternarEstadoTarea(tareaId, nuevoEstado) {
try {
const response = await fetch(`/api/tareas/${tareaId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
completada: nuevoEstado
})
});
const resultado = await response.json();
if (resultado.success) {
// Actualizar la interfaz inmediatamente
actualizarInterfazTarea(tareaId, resultado.data.tarea);
} else {
throw new Error(resultado.error.message);
}
} catch (error) {
console.error('Error al actualizar tarea:', error);
// Revertir el cambio visual si hubo error
revertirCambioTarea(tareaId);
}
}
function actualizarInterfazTarea(tareaId, tarea) {
const elementoTarea = document.querySelector(`[data-tarea-id="${tareaId}"]`);
const checkbox = elementoTarea.querySelector('.tarea-checkbox');
const titulo = elementoTarea.querySelector('.tarea-titulo');
checkbox.checked = tarea.completada;
titulo.classList.toggle('completada', tarea.completada);
}
// Configurar event listeners para checkboxes de tareas
document.addEventListener('change', function(event) {
if (event.target.classList.contains('tarea-checkbox')) {
const tareaId = parseInt(event.target.dataset.tareaId);
const nuevoEstado = event.target.checked;
alternarEstadoTarea(tareaId, nuevoEstado);
}
});
Integración con plantillas Jinja2 y datos dinámicos
Podemos combinar datos del servidor con la funcionalidad de fetch() para crear experiencias híbridas:
<!-- templates/dashboard.html -->
<div class="dashboard">
<h1>Dashboard - {{ usuario.nombre }}</h1>
<div class="estadisticas" id="estadisticas-container">
<!-- Las estadísticas se cargarán dinámicamente -->
</div>
<div class="acciones-rapidas">
<button onclick="actualizarEstadisticas()">Actualizar</button>
<button onclick="exportarDatos()">Exportar</button>
</div>
</div>
<script>
// Datos del servidor disponibles en JavaScript
const usuarioActual = {
id: {{ usuario.id }},
nombre: "{{ usuario.nombre }}",
rol: "{{ usuario.rol }}"
};
// Cargar estadísticas al iniciar la página
document.addEventListener('DOMContentLoaded', function() {
actualizarEstadisticas();
// Actualizar automáticamente cada 30 segundos
setInterval(actualizarEstadisticas, 30000);
});
async function actualizarEstadisticas() {
try {
const response = await fetch('/api/estadisticas');
const data = await response.json();
if (data.success) {
mostrarEstadisticas(data.data);
}
} catch (error) {
console.error('Error al actualizar estadísticas:', error);
}
}
function mostrarEstadisticas(stats) {
const container = document.getElementById('estadisticas-container');
container.innerHTML = `
<div class="stat-card">
<h3>Total Usuarios</h3>
<p class="stat-number">${stats.total_usuarios}</p>
</div>
<div class="stat-card">
<h3>Usuarios Activos</h3>
<p class="stat-number">${stats.usuarios_activos}</p>
</div>
<div class="stat-card">
<h3>Nuevos Registros</h3>
<p class="stat-number">${stats.nuevos_registros}</p>
</div>
`;
}
</script>
Manejo de errores y estados de carga
Un manejo robusto de errores mejora significativamente la experiencia del usuario:
class APIClient {
constructor(baseURL = '/api') {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new APIError(
errorData.error?.message || `Error HTTP: ${response.status}`,
response.status,
errorData.error?.code
);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
throw error;
}
throw new APIError('Error de conexión', 0, 'CONNECTION_ERROR');
}
}
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}
class APIError extends Error {
constructor(message, status, code) {
super(message);
this.status = status;
this.code = code;
this.name = 'APIError';
}
}
// Uso del cliente API
const api = new APIClient();
async function cargarDatosConManejo() {
const loadingElement = document.getElementById('loading');
const errorElement = document.getElementById('error');
const contentElement = document.getElementById('content');
try {
// Mostrar estado de carga
loadingElement.style.display = 'block';
errorElement.style.display = 'none';
const data = await api.get('/productos');
if (data.success) {
renderizarContenido(data.data);
contentElement.style.display = 'block';
}
} catch (error) {
console.error('Error:', error);
errorElement.textContent = error.message;
errorElement.style.display = 'block';
} finally {
loadingElement.style.display = 'none';
}
}
La integración de fetch() con plantillas Jinja2 permite crear aplicaciones web dinámicas que combinan la renderización del lado del servidor con la interactividad del lado del cliente, ofreciendo una experiencia de usuario fluida y responsiva.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en FastAPI
Documentación oficial de FastAPI
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
- Entender la diferencia entre rutas HTML y endpoints API internos en FastAPI.
- Crear y organizar endpoints API con diferentes métodos HTTP y manejo de errores.
- Consumir endpoints API desde plantillas Jinja2 utilizando JavaScript fetch() con async/await.
- Implementar manejo de respuestas JSON estructuradas y estados de carga en el frontend.
- Integrar datos dinámicos del servidor con la lógica cliente para interfaces interactivas y actualizaciones en tiempo real.