Sistema de mensajes flash con sessions
Los mensajes flash son notificaciones temporales que aparecen en la interfaz web después de que el usuario realiza una acción, como enviar un formulario o completar una operación. En FastAPI, implementamos este sistema utilizando sessions para almacenar mensajes que se muestran una sola vez y luego se eliminan automáticamente.
Configuración del middleware de sesiones
Para trabajar con sessions en FastAPI, necesitamos configurar el SessionMiddleware de Starlette. Este middleware nos permite almacenar datos temporalmente en la sesión del usuario.
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
# Configurar el middleware de sesiones con una clave secreta
app.add_middleware(
SessionMiddleware,
secret_key="tu-clave-secreta-muy-segura-aqui"
)
La clave secreta debe ser única y segura en aplicaciones de producción. Este middleware permite que FastAPI mantenga datos en la sesión del usuario entre diferentes peticiones.
Implementación básica de mensajes flash
Creamos funciones auxiliares para añadir y obtener mensajes flash desde la sesión:
from fastapi import Request
from typing import List, Dict, Any
def add_flash_message(request: Request, message: str, category: str = "info"):
"""Añade un mensaje flash a la sesión del usuario"""
if "flash_messages" not in request.session:
request.session["flash_messages"] = []
request.session["flash_messages"].append({
"message": message,
"category": category
})
def get_flash_messages(request: Request) -> List[Dict[str, Any]]:
"""Obtiene y elimina los mensajes flash de la sesión"""
messages = request.session.pop("flash_messages", [])
return messages
Estas funciones nos permiten almacenar mensajes en la sesión y recuperarlos una sola vez, siguiendo el patrón típico de los mensajes flash.
Integración con endpoints FastAPI
Los mensajes flash se integran naturalmente en nuestros endpoints. Aquí un ejemplo completo de un formulario que añade mensajes según el resultado:
from fastapi import Form, HTTPException
from fastapi.responses import RedirectResponse
@app.post("/contact")
async def process_contact_form(
request: Request,
name: str = Form(...),
email: str = Form(...),
message: str = Form(...)
):
try:
# Simular procesamiento del formulario
if len(message) < 10:
add_flash_message(
request,
"El mensaje debe tener al menos 10 caracteres",
"error"
)
return RedirectResponse(url="/contact", status_code=303)
# Procesar el formulario exitosamente
add_flash_message(
request,
f"¡Gracias {name}! Tu mensaje ha sido enviado correctamente",
"success"
)
return RedirectResponse(url="/contact", status_code=303)
except Exception as e:
add_flash_message(
request,
"Ha ocurrido un error inesperado",
"error"
)
return RedirectResponse(url="/contact", status_code=303)
El patrón Post-Redirect-Get es fundamental aquí: después de procesar el formulario, redirigimos al usuario para evitar reenvíos accidentales.
Mostrar mensajes en templates Jinja2
Para mostrar los mensajes flash en nuestros templates, necesitamos pasarlos desde el endpoint y renderizarlos en el HTML:
@app.get("/contact")
async def show_contact_form(request: Request):
# Obtener mensajes flash de la sesión
flash_messages = get_flash_messages(request)
return templates.TemplateResponse(
"contact.html",
{
"request": request,
"flash_messages": flash_messages
}
)
En el template HTML, creamos una sección para mostrar los mensajes:
<!-- contact.html -->
<!DOCTYPE html>
<html>
<head>
<title>Contacto</title>
</head>
<body>
<!-- Mostrar mensajes flash -->
{% if flash_messages %}
<div class="flash-messages">
{% for flash in flash_messages %}
<div class="alert alert-{{ flash.category }}">
{{ flash.message }}
</div>
{% endfor %}
</div>
{% endif %}
<h1>Formulario de contacto</h1>
<form method="post" action="/contact">
<div>
<label>Nombre:</label>
<input type="text" name="name" required>
</div>
<div>
<label>Email:</label>
<input type="email" name="email" required>
</div>
<div>
<label>Mensaje:</label>
<textarea name="message" required></textarea>
</div>
<button type="submit">Enviar</button>
</form>
</body>
</html>
Categorías de mensajes
Un sistema robusto de mensajes flash maneja diferentes categorías para distintos tipos de notificaciones:
class FlashCategory:
SUCCESS = "success"
INFO = "info"
WARNING = "warning"
ERROR = "error"
def add_success_message(request: Request, message: str):
"""Añade un mensaje de éxito"""
add_flash_message(request, message, FlashCategory.SUCCESS)
def add_error_message(request: Request, message: str):
"""Añade un mensaje de error"""
add_flash_message(request, message, FlashCategory.ERROR)
def add_warning_message(request: Request, message: str):
"""Añade un mensaje de advertencia"""
add_flash_message(request, message, FlashCategory.WARNING)
Estas funciones especializadas hacen el código más legible y mantenible.
Ejemplo práctico completo
Aquí un ejemplo funcional que muestra el sistema completo:
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="mi-clave-secreta")
templates = Jinja2Templates(directory="templates")
# Lista en memoria para simular datos
users = []
@app.get("/users")
async def list_users(request: Request):
flash_messages = get_flash_messages(request)
return templates.TemplateResponse(
"users.html",
{
"request": request,
"users": users,
"flash_messages": flash_messages
}
)
@app.post("/users")
async def create_user(
request: Request,
username: str = Form(...),
email: str = Form(...)
):
# Validar datos
if len(username) < 3:
add_error_message(request, "El nombre de usuario debe tener al menos 3 caracteres")
return RedirectResponse(url="/users", status_code=303)
# Verificar usuario único
if any(user["username"] == username for user in users):
add_warning_message(request, f"El usuario '{username}' ya existe")
return RedirectResponse(url="/users", status_code=303)
# Crear usuario
users.append({"username": username, "email": email})
add_success_message(request, f"Usuario '{username}' creado exitosamente")
return RedirectResponse(url="/users", status_code=303)
Este sistema proporciona feedback inmediato al usuario sobre el resultado de sus acciones, mejorando significativamente la experiencia de usuario en aplicaciones web con FastAPI.
Alertas Bootstrap y confirmaciones visuales
Bootstrap proporciona un sistema completo de componentes visuales que podemos integrar perfectamente con nuestros mensajes flash. Estos componentes no solo mejoran la apariencia, sino que también ofrecen funcionalidades avanzadas como auto-dismissal, animaciones y modales de confirmación.
Integración con componentes Bootstrap
Los componentes de alerta de Bootstrap se integran naturalmente con nuestro sistema de mensajes flash mediante el mapeo de categorías a clases CSS específicas:
# Mapeo de categorías a clases Bootstrap
BOOTSTRAP_ALERT_CLASSES = {
"success": "alert-success",
"error": "alert-danger",
"warning": "alert-warning",
"info": "alert-info"
}
def get_bootstrap_class(category: str) -> str:
"""Convierte categoría de mensaje a clase Bootstrap"""
return BOOTSTRAP_ALERT_CLASSES.get(category, "alert-info")
Esta función nos permite traducir automáticamente las categorías de nuestros mensajes flash a las clases visuales apropiadas de Bootstrap.
Template con alertas Bootstrap avanzadas
Creamos un template que incluye Bootstrap CDN y renderiza alertas con funcionalidades completas:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mensajes Flash con Bootstrap</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<!-- Área de mensajes flash -->
{% if flash_messages %}
<div id="flash-container">
{% for flash in flash_messages %}
<div class="alert alert-{{ 'danger' if flash.category == 'error' else flash.category }} alert-dismissible fade show"
role="alert" data-auto-dismiss="true">
<div class="d-flex align-items-center">
<!-- Iconos según categoría -->
{% if flash.category == 'success' %}
<svg class="me-2" width="20" height="20" fill="currentColor">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.061L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>
{% elif flash.category == 'error' %}
<svg class="me-2" width="20" height="20" fill="currentColor">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
{% elif flash.category == 'warning' %}
<svg class="me-2" width="20" height="20" fill="currentColor">
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
<path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0L8.55 8.5a.552.552 0 0 1-1.1 0L7.1 5.995z"/>
</svg>
{% else %}
<svg class="me-2" width="20" height="20" fill="currentColor">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
{% endif %}
<span>{{ flash.message }}</span>
</div>
<!-- Botón de cierre -->
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Contenido de la página -->
<div class="row">
<div class="col-md-8">
<h2>Panel de Control</h2>
<p>Contenido principal de la aplicación...</p>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Auto-dismissal de alertas con JavaScript
Implementamos desaparición automática de las alertas después de un tiempo determinado:
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-dismiss para alertas con atributo data-auto-dismiss
const autoDismissAlerts = document.querySelectorAll('[data-auto-dismiss="true"]');
autoDismissAlerts.forEach(function(alert) {
// Determinar tiempo según tipo de alerta
let dismissTime = 5000; // Por defecto 5 segundos
if (alert.classList.contains('alert-success')) {
dismissTime = 4000; // Éxito: 4 segundos
} else if (alert.classList.contains('alert-danger')) {
dismissTime = 8000; // Error: 8 segundos (más tiempo para leer)
} else if (alert.classList.contains('alert-warning')) {
dismissTime = 6000; // Advertencia: 6 segundos
}
// Configurar auto-dismiss
setTimeout(function() {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}, dismissTime);
});
// Añadir barra de progreso visual
autoDismissAlerts.forEach(function(alert) {
const progressBar = document.createElement('div');
progressBar.className = 'alert-progress';
progressBar.innerHTML = '<div class="alert-progress-bar"></div>';
alert.appendChild(progressBar);
// Animar barra de progreso
const bar = progressBar.querySelector('.alert-progress-bar');
bar.style.animation = `progressAnimation ${dismissTime}ms linear`;
});
});
</script>
<style>
.alert-progress {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background-color: rgba(255, 255, 255, 0.3);
overflow: hidden;
}
.alert-progress-bar {
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
width: 0%;
}
@keyframes progressAnimation {
from { width: 100%; }
to { width: 0%; }
}
.alert {
position: relative;
overflow: hidden;
}
</style>
Modales de confirmación Bootstrap
Para acciones importantes, implementamos modales de confirmación que requieren confirmación explícita del usuario:
@app.post("/users/{user_id}/delete")
async def delete_user_confirm(request: Request, user_id: int):
"""Endpoint que requiere confirmación modal"""
user = next((u for u in users if u["id"] == user_id), None)
if not user:
add_error_message(request, "Usuario no encontrado")
return RedirectResponse(url="/users", status_code=303)
return templates.TemplateResponse(
"confirm_delete.html",
{
"request": request,
"user": user,
"action_url": f"/users/{user_id}/delete/confirmed"
}
)
@app.post("/users/{user_id}/delete/confirmed")
async def delete_user_confirmed(request: Request, user_id: int):
"""Eliminación confirmada del usuario"""
global users
user = next((u for u in users if u["id"] == user_id), None)
if user:
users = [u for u in users if u["id"] != user_id]
add_success_message(
request,
f"Usuario '{user['username']}' eliminado correctamente"
)
else:
add_error_message(request, "Error al eliminar usuario")
return RedirectResponse(url="/users", status_code=303)
Template para modal de confirmación
<!-- confirm_delete.html -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">
<svg width="20" height="20" fill="currentColor" class="me-2">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
Confirmar eliminación
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center">
<div class="text-danger me-3">
<svg width="48" height="48" fill="currentColor">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
</div>
<div>
<h6>¿Estás seguro de que deseas eliminar este usuario?</h6>
<p class="mb-0 text-muted">
<strong>Usuario:</strong> {{ user.username }}<br>
<strong>Email:</strong> {{ user.email }}
</p>
<div class="alert alert-warning mt-3 mb-0">
<small><strong>Advertencia:</strong> Esta acción no se puede deshacer.</small>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancelar
</button>
<form method="post" action="{{ action_url }}" class="d-inline">
<button type="submit" class="btn btn-danger">
<svg width="16" height="16" fill="currentColor" class="me-1">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
</svg>
Eliminar usuario
</button>
</form>
</div>
</div>
</div>
</div>
<script>
// Auto-abrir el modal al cargar la página
document.addEventListener('DOMContentLoaded', function() {
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
modal.show();
// Redirigir al cerrar sin confirmar
document.getElementById('confirmDeleteModal').addEventListener('hidden.bs.modal', function() {
if (!this.dataset.confirmed) {
window.location.href = '/users';
}
});
});
</script>
Confirmaciones inline con JavaScript
Para acciones menos críticas, podemos implementar confirmaciones inline sin modales:
<script>
function confirmAction(element, message) {
const originalText = element.innerHTML;
const originalClass = element.className;
// Cambiar apariencia del botón
element.innerHTML = `
<svg width="16" height="16" fill="currentColor" class="me-1">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
${message}
`;
element.className = 'btn btn-warning btn-sm';
element.disabled = false;
// Crear botones de confirmación
const confirmBtn = document.createElement('button');
confirmBtn.className = 'btn btn-danger btn-sm ms-1';
confirmBtn.innerHTML = '✓ Sí';
confirmBtn.onclick = function() {
// Proceder con la acción original
element.closest('form').submit();
};
const cancelBtn = document.createElement('button');
cancelBtn.className = 'btn btn-secondary btn-sm ms-1';
cancelBtn.innerHTML = '✗ No';
cancelBtn.onclick = function() {
// Restaurar estado original
element.innerHTML = originalText;
element.className = originalClass;
confirmBtn.remove();
cancelBtn.remove();
};
// Insertar botones
element.parentNode.insertBefore(confirmBtn, element.nextSibling);
element.parentNode.insertBefore(cancelBtn, confirmBtn.nextSibling);
element.disabled = true;
// Auto-cancelar después de 10 segundos
setTimeout(() => {
if (confirmBtn.parentNode) {
cancelBtn.click();
}
}, 10000);
}
</script>
<!-- Uso en formularios -->
<form method="post" action="/users/{{ user.id }}/delete">
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="confirmAction(this, '¿Eliminar?')">
Eliminar
</button>
</form>
Toasts Bootstrap para notificaciones discretas
Los toasts son ideales para notificaciones que no interrumpen el flujo de trabajo:
<!-- Contenedor para toasts -->
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 11;">
{% for flash in flash_messages %}
<div class="toast" role="alert" data-bs-autohide="true" data-bs-delay="4000">
<div class="toast-header bg-{{ 'danger' if flash.category == 'error' else flash.category }} text-white">
<strong class="me-auto">
{% if flash.category == 'success' %}
✅ Éxito
{% elif flash.category == 'error' %}
❌ Error
{% elif flash.category == 'warning' %}
⚠️ Advertencia
{% else %}
ℹ️ Información
{% endif %}
</strong>
<small>ahora</small>
<button type="button" class="btn-close btn-close-white ms-2" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
{{ flash.message }}
</div>
</div>
{% endfor %}
</div>
<script>
// Mostrar todos los toasts automáticamente
document.addEventListener('DOMContentLoaded', function() {
const toasts = document.querySelectorAll('.toast');
toasts.forEach(function(toast) {
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
});
});
</script>
Esta implementación completa proporciona una experiencia visual rica que combina la funcionalidad de los mensajes flash con el diseño profesional de Bootstrap, ofreciendo múltiples formas de confirmar acciones y mostrar feedback al usuario.
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
- Comprender el concepto y uso de mensajes flash en aplicaciones web.
- Configurar y utilizar el middleware de sesiones en FastAPI para almacenar mensajes temporales.
- Implementar funciones para añadir y recuperar mensajes flash en endpoints.
- Integrar mensajes flash con templates Jinja2 y estilos visuales de Bootstrap.
- Aplicar técnicas avanzadas como auto-dismissal, modales de confirmación y toasts para mejorar la experiencia de usuario.