Formularios de búsqueda
Los formularios de búsqueda permiten a los usuarios filtrar y localizar información específica dentro de aplicaciones web. En FastAPI con Jinja2, implementamos estas funcionalidades combinando formularios HTML, parámetros de consulta y lógica de filtrado en el backend.
Estructura básica de un formulario de búsqueda
Un formulario de búsqueda utiliza el método GET para enviar los términos de búsqueda como parámetros de URL, permitiendo que los usuarios puedan compartir y marcar resultados específicos.
<!-- search_form.html -->
<form method="GET" action="/productos" class="mb-4">
<div class="input-group">
<input type="text"
name="q"
class="form-control"
placeholder="Buscar productos..."
value="{{ request.query_params.get('q', '') }}">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i> Buscar
</button>
</div>
</form>
La clave del formulario está en preservar el valor buscado usando {{ request.query_params.get('q', '') }}
, lo que mantiene el término visible después de realizar la búsqueda.
Manejo de parámetros de consulta en FastAPI
El endpoint debe capturar los parámetros de consulta opcionales y aplicar los filtros correspondientes:
from fastapi import FastAPI, Request, Query
from fastapi.templating import Jinja2Templates
from typing import Optional
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/productos")
async def buscar_productos(
request: Request,
q: Optional[str] = Query(None, description="Término de búsqueda")
):
# Simular datos de productos
todos_productos = [
{"id": 1, "nombre": "Laptop HP", "precio": 899.99},
{"id": 2, "nombre": "Mouse inalámbrico", "precio": 25.50},
{"id": 3, "nombre": "Teclado mecánico", "precio": 120.00},
{"id": 4, "nombre": "Monitor LED", "precio": 299.99}
]
# Aplicar filtro de búsqueda si existe el término
if q and q.strip():
productos_filtrados = [
p for p in todos_productos
if q.lower() in p["nombre"].lower()
]
else:
productos_filtrados = todos_productos
return templates.TemplateResponse("productos.html", {
"request": request,
"productos": productos_filtrados,
"termino_busqueda": q,
"total_resultados": len(productos_filtrados)
})
Template con resultados de búsqueda
El template debe mostrar resultados dinámicamente y manejar casos cuando no se encuentren coincidencias:
<!-- productos.html -->
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>Catálogo de productos</h2>
<!-- Formulario de búsqueda -->
<form method="GET" action="/productos" class="mb-4">
<div class="input-group">
<input type="text"
name="q"
class="form-control"
placeholder="Buscar productos..."
value="{{ request.query_params.get('q', '') }}">
<button type="submit" class="btn btn-primary">Buscar</button>
{% if request.query_params.get('q') %}
<a href="/productos" class="btn btn-outline-secondary">Limpiar</a>
{% endif %}
</div>
</form>
<!-- Información de resultados -->
{% if termino_busqueda %}
<div class="alert alert-info">
<strong>{{ total_resultados }}</strong> resultado(s) encontrado(s) para
"<em>{{ termino_busqueda }}</em>"
</div>
{% endif %}
<!-- Lista de productos -->
{% if productos %}
<div class="row">
{% for producto in productos %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ producto.nombre }}</h5>
<p class="card-text">
<strong>Precio:</strong> ${{ "%.2f"|format(producto.precio) }}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-warning text-center">
<h4>No se encontraron productos</h4>
<p>Intenta con otros términos de búsqueda.</p>
</div>
{% endif %}
</div>
{% endblock %}
Búsqueda con múltiples criterios
Para búsquedas más avanzadas, podemos combinar varios campos de búsqueda:
@app.get("/productos/avanzada")
async def busqueda_avanzada(
request: Request,
nombre: Optional[str] = Query(None),
precio_min: Optional[float] = Query(None, ge=0),
precio_max: Optional[float] = Query(None, ge=0)
):
productos = obtener_productos_db() # Función que obtiene productos
# Aplicar filtros progresivamente
if nombre and nombre.strip():
productos = [p for p in productos if nombre.lower() in p["nombre"].lower()]
if precio_min is not None:
productos = [p for p in productos if p["precio"] >= precio_min]
if precio_max is not None:
productos = [p for p in productos if p["precio"] <= precio_max]
return templates.TemplateResponse("busqueda_avanzada.html", {
"request": request,
"productos": productos,
"filtros_aplicados": any([nombre, precio_min is not None, precio_max is not None])
})
El formulario avanzado correspondiente mantiene todos los valores de búsqueda:
<form method="GET" action="/productos/avanzada">
<div class="row">
<div class="col-md-4">
<label class="form-label">Nombre del producto</label>
<input type="text" name="nombre" class="form-control"
value="{{ request.query_params.get('nombre', '') }}">
</div>
<div class="col-md-3">
<label class="form-label">Precio mínimo</label>
<input type="number" name="precio_min" class="form-control" step="0.01"
value="{{ request.query_params.get('precio_min', '') }}">
</div>
<div class="col-md-3">
<label class="form-label">Precio máximo</label>
<input type="number" name="precio_max" class="form-control" step="0.01"
value="{{ request.query_params.get('precio_max', '') }}">
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Filtrar</button>
</div>
</div>
</form>
Integración con base de datos
Para búsquedas eficientes con SQLAlchemy, utilizamos consultas optimizadas:
from sqlalchemy.orm import Session
from sqlalchemy import or_, and_
@app.get("/productos/db")
async def buscar_productos_db(
request: Request,
q: Optional[str] = Query(None),
db: Session = Depends(get_db)
):
query = db.query(Producto)
if q and q.strip():
# Búsqueda en múltiples campos usando OR
termino = f"%{q.strip()}%"
query = query.filter(
or_(
Producto.nombre.ilike(termino),
Producto.descripcion.ilike(termino),
Producto.categoria.ilike(termino)
)
)
productos = query.limit(50).all() # Limitar resultados para rendimiento
return templates.TemplateResponse("productos.html", {
"request": request,
"productos": productos,
"termino_busqueda": q
})
Los formularios de búsqueda proporcionan una experiencia de usuario intuitiva cuando se implementan correctamente, manteniendo el estado de los filtros y proporcionando retroalimentación clara sobre los resultados obtenidos.
Filtros dinámicos
Los filtros dinámicos mejoran significativamente la experiencia del usuario al permitir filtrado instantáneo sin recargas de página. Utilizamos JavaScript combinado con endpoints FastAPI para crear interfaces interactivas que respondan a las acciones del usuario en tiempo real.
Filtros con JavaScript y fetch()
Los filtros dinámicos funcionan enviando peticiones asíncronas al servidor y actualizando solo las secciones relevantes del DOM:
<!-- filtros_dinamicos.html -->
<div class="container mt-4">
<div class="row">
<div class="col-md-3">
<!-- Panel de filtros -->
<div class="card">
<div class="card-header">
<h5>Filtros</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Categoría</label>
<select id="categoria-filter" class="form-select">
<option value="">Todas las categorías</option>
<option value="electronica">Electrónica</option>
<option value="ropa">Ropa</option>
<option value="hogar">Hogar</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Precio máximo: <span id="precio-valor">1000</span>€</label>
<input type="range" id="precio-range" class="form-range"
min="0" max="1000" value="1000">
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" id="disponible-check" class="form-check-input" checked>
<label class="form-check-label" for="disponible-check">
Solo disponibles
</label>
</div>
</div>
<button id="limpiar-filtros" class="btn btn-outline-secondary btn-sm">
Limpiar filtros
</button>
</div>
</div>
</div>
<div class="col-md-9">
<!-- Área de resultados -->
<div id="loading" class="text-center d-none">
<div class="spinner-border" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
</div>
<div id="resultados-container">
<!-- Los resultados se cargarán aquí dinámicamente -->
</div>
<div id="no-resultados" class="alert alert-info text-center d-none">
<h5>No se encontraron productos</h5>
<p>Prueba ajustando los filtros de búsqueda.</p>
</div>
</div>
</div>
</div>
Lógica JavaScript para filtros interactivos
El JavaScript maneja los eventos de los filtros y actualiza los resultados automáticamente:
<script>
class FiltrosDinamicos {
constructor() {
this.initEventListeners();
this.cargarResultados(); // Carga inicial
}
initEventListeners() {
// Filtro de categoría
document.getElementById('categoria-filter').addEventListener('change', () => {
this.aplicarFiltros();
});
// Slider de precio con actualización en tiempo real
const precioRange = document.getElementById('precio-range');
precioRange.addEventListener('input', (e) => {
document.getElementById('precio-valor').textContent = e.target.value;
this.debounce(() => this.aplicarFiltros(), 300);
});
// Checkbox de disponibilidad
document.getElementById('disponible-check').addEventListener('change', () => {
this.aplicarFiltros();
});
// Botón limpiar filtros
document.getElementById('limpiar-filtros').addEventListener('click', () => {
this.limpiarFiltros();
});
}
// Función debounce para evitar demasiadas peticiones
debounce(func, delay) {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(func, delay);
}
async aplicarFiltros() {
const filtros = this.obtenerFiltrosActivos();
// Mostrar indicador de carga
this.mostrarCarga(true);
try {
const response = await fetch('/api/productos/filtrar', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(filtros)
});
if (response.ok) {
const html = await response.text();
this.actualizarResultados(html);
} else {
console.error('Error al aplicar filtros:', response.status);
}
} catch (error) {
console.error('Error de red:', error);
} finally {
this.mostrarCarga(false);
}
}
obtenerFiltrosActivos() {
return {
categoria: document.getElementById('categoria-filter').value,
precio_max: parseFloat(document.getElementById('precio-range').value),
solo_disponibles: document.getElementById('disponible-check').checked
};
}
actualizarResultados(html) {
const container = document.getElementById('resultados-container');
const noResultados = document.getElementById('no-resultados');
if (html.trim() === '') {
container.innerHTML = '';
noResultados.classList.remove('d-none');
} else {
container.innerHTML = html;
noResultados.classList.add('d-none');
}
}
mostrarCarga(mostrar) {
const loading = document.getElementById('loading');
if (mostrar) {
loading.classList.remove('d-none');
} else {
loading.classList.add('d-none');
}
}
limpiarFiltros() {
document.getElementById('categoria-filter').value = '';
document.getElementById('precio-range').value = 1000;
document.getElementById('precio-valor').textContent = '1000';
document.getElementById('disponible-check').checked = true;
this.aplicarFiltros();
}
async cargarResultados() {
await this.aplicarFiltros();
}
}
// Inicializar cuando se carga la página
document.addEventListener('DOMContentLoaded', () => {
new FiltrosDinamicos();
});
</script>
Endpoint FastAPI para filtros dinámicos
El endpoint de filtrado procesa los filtros y retorna HTML parcial:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import Optional
class FiltrosProductos(BaseModel):
categoria: Optional[str] = None
precio_max: Optional[float] = None
solo_disponibles: bool = True
@app.post("/api/productos/filtrar", response_class=HTMLResponse)
async def filtrar_productos(
request: Request,
filtros: FiltrosProductos
):
# Obtener productos de la base de datos o fuente de datos
productos = obtener_productos_filtrados(filtros)
# Renderizar solo la parte de resultados
return templates.TemplateResponse("partials/productos_grid.html", {
"request": request,
"productos": productos
})
def obtener_productos_filtrados(filtros: FiltrosProductos):
# Simular filtrado de productos
todos_productos = [
{"id": 1, "nombre": "Smartphone", "categoria": "electronica", "precio": 599.99, "disponible": True},
{"id": 2, "nombre": "Camiseta", "categoria": "ropa", "precio": 25.99, "disponible": True},
{"id": 3, "nombre": "Lámpara LED", "categoria": "hogar", "precio": 45.50, "disponible": False},
{"id": 4, "nombre": "Tablet", "categoria": "electronica", "precio": 299.99, "disponible": True}
]
productos_filtrados = todos_productos.copy()
# Aplicar filtro de categoría
if filtros.categoria:
productos_filtrados = [p for p in productos_filtrados if p["categoria"] == filtros.categoria]
# Aplicar filtro de precio
if filtros.precio_max is not None:
productos_filtrados = [p for p in productos_filtrados if p["precio"] <= filtros.precio_max]
# Aplicar filtro de disponibilidad
if filtros.solo_disponibles:
productos_filtrados = [p for p in productos_filtrados if p["disponible"]]
return productos_filtrados
Template parcial para resultados
El template parcial contiene solo la estructura de resultados que se actualizará:
<!-- partials/productos_grid.html -->
{% if productos %}
<div class="row">
{% for producto in productos %}
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100 {% if not producto.disponible %}opacity-75{% endif %}">
<div class="card-body">
<h5 class="card-title">{{ producto.nombre }}</h5>
<p class="card-text">
<span class="badge bg-secondary">{{ producto.categoria|title }}</span>
</p>
<p class="card-text">
<strong class="text-primary">${{ "%.2f"|format(producto.precio) }}</strong>
</p>
{% if producto.disponible %}
<span class="badge bg-success">Disponible</span>
{% else %}
<span class="badge bg-warning">Agotado</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="mt-3 text-muted">
<small>{{ productos|length }} producto(s) encontrado(s)</small>
</div>
{% endif %}
Filtros con autocompletado dinámico
Para búsquedas más sofisticadas, implementamos autocompletado que se actualiza mientras el usuario escribe:
<div class="mb-3">
<label class="form-label">Buscar producto</label>
<div class="position-relative">
<input type="text" id="busqueda-input" class="form-control"
placeholder="Escribe para buscar...">
<div id="sugerencias" class="position-absolute w-100 bg-white border rounded-bottom shadow-sm d-none"
style="top: 100%; z-index: 1000; max-height: 200px; overflow-y: auto;">
</div>
</div>
</div>
<script>
// Agregar a la clase FiltrosDinamicos
initBusquedaAutocompletado() {
const input = document.getElementById('busqueda-input');
const sugerencias = document.getElementById('sugerencias');
input.addEventListener('input', (e) => {
const termino = e.target.value.trim();
if (termino.length >= 2) {
this.debounce(() => this.buscarSugerencias(termino), 200);
} else {
sugerencias.classList.add('d-none');
}
});
// Ocultar sugerencias al hacer clic fuera
document.addEventListener('click', (e) => {
if (!input.contains(e.target) && !sugerencias.contains(e.target)) {
sugerencias.classList.add('d-none');
}
});
}
async buscarSugerencias(termino) {
try {
const response = await fetch(`/api/productos/sugerencias?q=${encodeURIComponent(termino)}`);
const sugerenciasData = await response.json();
const sugerenciasEl = document.getElementById('sugerencias');
if (sugerenciasData.length > 0) {
sugerenciasEl.innerHTML = sugerenciasData.map(item =>
`<div class="p-2 border-bottom cursor-pointer hover-bg-light" onclick="this.seleccionarSugerencia('${item.nombre}')">
<strong>${item.nombre}</strong>
<small class="text-muted d-block">${item.categoria} - $${item.precio}</small>
</div>`
).join('');
sugerenciasEl.classList.remove('d-none');
} else {
sugerenciasEl.classList.add('d-none');
}
} catch (error) {
console.error('Error al buscar sugerencias:', error);
}
}
</script>
Endpoint para sugerencias
El endpoint de sugerencias proporciona resultados rápidos para el autocompletado:
@app.get("/api/productos/sugerencias")
async def obtener_sugerencias(q: str = Query(..., min_length=2)):
# Búsqueda rápida en productos
productos = buscar_productos_por_nombre(q)
# Limitar a 5 sugerencias para rendimiento
sugerencias = [
{
"nombre": p["nombre"],
"categoria": p["categoria"],
"precio": p["precio"]
}
for p in productos[:5]
]
return sugerencias
Los filtros dinámicos transforman la experiencia de búsqueda de estática a interactiva, proporcionando retroalimentación inmediata y reduciendo la fricción en la navegación de contenido.
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 cómo crear formularios de búsqueda con método GET en Jinja2.
- Manejar parámetros de consulta en FastAPI para filtrar datos.
- Diseñar templates que muestren resultados de búsqueda y gestionen casos sin coincidencias.
- Implementar filtros avanzados y dinámicos con JavaScript y endpoints FastAPI.
- Integrar autocompletado y filtros interactivos para mejorar la usabilidad.