Funciones y macros en Jinja2

Intermedio
FastAPI
FastAPI
Actualizado: 18/09/2025

Definición de macros

Los macros en Jinja2 son funciones reutilizables que permiten encapsular fragmentos de código HTML repetitivo, facilitando el mantenimiento y la consistencia en nuestras aplicaciones FastAPI. Un macro funciona de manera similar a una función en Python, pero genera código HTML en lugar de ejecutar lógica de programación.

¿Qué es un macro en Jinja2?

Un macro es esencialmente una plantilla reutilizable que acepta parámetros y genera contenido HTML dinámico. Los macros resuelven el problema de la duplicación de código en plantillas, permitiendo definir una vez un componente y reutilizarlo múltiples veces con diferentes datos.

La sintaxis básica para definir un macro utiliza las etiquetas {% macro %} y {% endmacro %}:

{% macro nombre_macro(parametros) %}
  <!-- Contenido HTML del macro -->
{% endmacro %}

Creación de un macro simple

Veamos un ejemplo básico de un macro que genera una tarjeta de usuario:

{% macro user_card(name, email, role="Usuario") %}
<div class="user-card">
  <h3>{{ name }}</h3>
  <p class="email">{{ email }}</p>
  <span class="role">{{ role }}</span>
</div>
{% endmacro %}

Este macro acepta tres parámetros: name y email son obligatorios, mientras que role tiene un valor por defecto. Para utilizar este macro, simplemente lo invocamos con los parámetros necesarios:

{{ user_card("Ana García", "ana@ejemplo.com", "Administradora") }}
{{ user_card("Carlos López", "carlos@ejemplo.com") }}

Macros con lógica condicional

Los macros pueden incluir estructuras de control para generar contenido dinámico más complejo. Consideremos un macro para mostrar alertas:

{% macro alert(message, type="info") %}
<div class="alert alert-{{ type }}">
  {% if type == "danger" %}
    <i class="icon-error"></i>
  {% elif type == "success" %}
    <i class="icon-check"></i>
  {% else %}
    <i class="icon-info"></i>
  {% endif %}
  <span>{{ message }}</span>
</div>
{% endmacro %}

Este macro genera diferentes tipos de alertas según el parámetro type, mostrando iconos apropiados para cada situación:

{{ alert("Datos guardados correctamente", "success") }}
{{ alert("Error al procesar la solicitud", "danger") }}
{{ alert("Información importante") }}

Macros con bucles y datos complejos

Los macros pueden procesar estructuras de datos complejas como listas o diccionarios. Un ejemplo útil es un macro para generar tablas HTML:

{% macro data_table(headers, rows) %}
<table class="table">
  <thead>
    <tr>
      {% for header in headers %}
        <th>{{ header }}</th>
      {% endfor %}
    </tr>
  </thead>
  <tbody>
    {% for row in rows %}
    <tr>
      {% for cell in row %}
        <td>{{ cell }}</td>
      {% endfor %}
    </tr>
    {% endfor %}
  </tbody>
</table>
{% endmacro %}

Para usar este macro desde FastAPI, pasaríamos los datos desde la ruta:

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

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

@app.get("/productos")
async def listar_productos(request: Request):
    headers = ["ID", "Nombre", "Precio", "Stock"]
    productos = [
        [1, "Laptop", "$999", 5],
        [2, "Mouse", "$29", 15],
        [3, "Teclado", "$79", 8]
    ]
    
    return templates.TemplateResponse(
        "productos.html", 
        {"request": request, "headers": headers, "productos": productos}
    )

Y en el template productos.html:

<!DOCTYPE html>
<html>
<head>
    <title>Lista de Productos</title>
</head>
<body>
    {{ data_table(headers, productos) }}
</body>
</html>

Macros con contenido anidado

Los macros pueden incluir contenido anidado utilizando la palabra clave caller. Esto permite crear macros que actúan como contenedores:

{% macro modal(title, id) %}
<div class="modal" id="{{ id }}">
  <div class="modal-content">
    <div class="modal-header">
      <h2>{{ title }}</h2>
      <button class="close" data-dismiss="modal">&times;</button>
    </div>
    <div class="modal-body">
      {{ caller() }}
    </div>
  </div>
</div>
{% endmacro %}

Para usar este macro con contenido personalizado:

{% call modal("Confirmar acción", "confirm-modal") %}
  <p>¿Estás seguro de que deseas eliminar este elemento?</p>
  <button class="btn btn-danger">Confirmar</button>
  <button class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
{% endcall %}

Organización de macros en archivos separados

Para mantener organizados los macros en aplicaciones grandes, es recomendable crear archivos dedicados. Por ejemplo, crear macros.html:

<!-- macros.html -->
{% macro form_field(name, label, type="text", required=false) %}
<div class="form-group">
  <label for="{{ name }}">
    {{ label }}
    {% if required %}<span class="required">*</span>{% endif %}
  </label>
  <input type="{{ type }}" id="{{ name }}" name="{{ name }}" 
         class="form-control" {% if required %}required{% endif %}>
</div>
{% endmacro %}

{% macro submit_button(text="Enviar", class="btn-primary") %}
<button type="submit" class="btn {{ class }}">{{ text }}</button>
{% endmacro %}

Para importar estos macros en otras plantillas:

{% from 'macros.html' import form_field, submit_button %}

<form method="post">
  {{ form_field("name", "Nombre completo", required=true) }}
  {{ form_field("email", "Correo electrónico", "email", required=true) }}
  {{ form_field("phone", "Teléfono", "tel") }}
  {{ submit_button("Registrarse", "btn-success") }}
</form>

Ventajas de utilizar macros

Los macros proporcionan varios beneficios importantes:

  • Reutilización: Evitan duplicación de código HTML repetitivo
  • Mantenimiento: Cambios en un macro se reflejan automáticamente en todos los lugares donde se usa
  • Consistencia: Garantizan que los componentes tengan el mismo aspecto y comportamiento
  • Legibilidad: Hacen que las plantillas sean más fáciles de leer y entender
  • Parametrización: Permiten personalizar el contenido sin duplicar la estructura

Los macros se convierten en una herramienta fundamental cuando desarrollamos aplicaciones FastAPI con interfaces de usuario complejas, ya que nos permiten crear componentes reutilizables que mantienen la coherencia visual y funcional a lo largo de toda la aplicación.

Funciones globales personalizadas

Las funciones globales personalizadas en Jinja2 son funciones Python que se registran en el entorno de templates para estar disponibles directamente en cualquier plantilla sin necesidad de importarlas. A diferencia de los macros, estas funciones se ejecutan del lado del servidor y pueden realizar operaciones más complejas antes de renderizar el contenido.

Diferencias entre macros y funciones globales

Mientras que los macros son fragmentos de código HTML reutilizables definidos dentro de las plantillas, las funciones globales son funciones Python que se definen en el código del servidor y se registran en el entorno de Jinja2. Las funciones globales permiten:

  • Ejecutar lógica de negocio compleja
  • Acceder a bases de datos o APIs externas
  • Realizar cálculos matemáticos avanzados
  • Formatear datos de manera específica
  • Integrar funcionalidades del sistema

Registrando funciones globales en FastAPI

Para agregar funciones personalizadas al entorno de Jinja2 en FastAPI, necesitamos acceder al objeto Environment y usar el método globals.update():

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
import datetime
from decimal import Decimal

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

# Función personalizada para formatear fechas
def format_date(date_obj, format_str="%d/%m/%Y"):
    """Formatea una fecha según el formato especificado"""
    if isinstance(date_obj, str):
        date_obj = datetime.datetime.fromisoformat(date_obj)
    return date_obj.strftime(format_str)

# Función para formatear precios
def format_currency(amount, currency="€"):
    """Formatea un número como moneda"""
    return f"{amount:.2f} {currency}"

# Función para calcular descuentos
def calculate_discount(price, discount_percent):
    """Calcula el precio con descuento aplicado"""
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount

# Registrar las funciones en el entorno de Jinja2
templates.env.globals.update({
    'format_date': format_date,
    'format_currency': format_currency,
    'calculate_discount': calculate_discount,
    'now': datetime.datetime.now
})

Usando las funciones globales en templates

Una vez registradas, las funciones están disponibles en cualquier template sin necesidad de pasarlas como variables:

<!DOCTYPE html>
<html>
<head>
    <title>Productos en Oferta</title>
</head>
<body>
    <h1>Ofertas del {{ format_date(now(), "%d de %B de %Y") }}</h1>
    
    <div class="products">
        {% for product in products %}
        <div class="product-card">
            <h3>{{ product.name }}</h3>
            <p class="original-price">
                Precio original: {{ format_currency(product.price) }}
            </p>
            <p class="discount-price">
                Precio con descuento: {{ format_currency(calculate_discount(product.price, 15)) }}
            </p>
            <small>Oferta válida hasta {{ format_date(product.offer_end) }}</small>
        </div>
        {% endfor %}
    </div>
</body>
</html>

Funciones globales con parámetros opcionales

Las funciones globales pueden incluir parámetros con valores por defecto y lógica condicional compleja:

def truncate_text(text, max_length=100, suffix="..."):
    """Trunca un texto a una longitud específica"""
    if len(text) <= max_length:
        return text
    return text[:max_length].rstrip() + suffix

def get_status_badge(status):
    """Genera un badge HTML según el estado"""
    status_config = {
        'active': {'class': 'badge-success', 'text': 'Activo'},
        'inactive': {'class': 'badge-danger', 'text': 'Inactivo'},
        'pending': {'class': 'badge-warning', 'text': 'Pendiente'},
        'draft': {'class': 'badge-secondary', 'text': 'Borrador'}
    }
    
    config = status_config.get(status, {'class': 'badge-light', 'text': 'Desconocido'})
    return f'<span class="badge {config["class"]}">{config["text"]}</span>'

# Registrar las nuevas funciones
templates.env.globals.update({
    'truncate_text': truncate_text,
    'get_status_badge': get_status_badge
})

Estas funciones se pueden usar directamente en los templates:

<div class="article-list">
    {% for article in articles %}
    <article>
        <h2>{{ article.title }}</h2>
        <p>{{ truncate_text(article.content, 150) }}</p>
        <div class="article-meta">
            {{ get_status_badge(article.status)|safe }}
            <span class="date">{{ format_date(article.created_at) }}</span>
        </div>
    </article>
    {% endfor %}
</div>

Funciones globales para utilidades comunes

Las funciones globales son especialmente útiles para operaciones frecuentes que se necesitan en múltiples templates:

import math
import re
from urllib.parse import urlencode

def pluralize(count, singular, plural=None):
    """Devuelve la forma singular o plural según la cantidad"""
    if plural is None:
        plural = singular + "s"
    return singular if count == 1 else plural

def paginate_range(current_page, total_pages, window=2):
    """Genera un rango de páginas para paginación"""
    start = max(1, current_page - window)
    end = min(total_pages, current_page + window)
    return range(start, end + 1)

def build_query_string(**params):
    """Construye una query string a partir de parámetros"""
    # Filtra valores None y vacíos
    clean_params = {k: v for k, v in params.items() if v is not None and v != ''}
    return urlencode(clean_params)

def extract_domain(url):
    """Extrae el dominio de una URL"""
    pattern = r'https?://([^/]+)'
    match = re.search(pattern, url)
    return match.group(1) if match else url

# Registrar funciones de utilidad
templates.env.globals.update({
    'pluralize': pluralize,
    'paginate_range': paginate_range,
    'build_query_string': build_query_string,
    'extract_domain': extract_domain,
    'abs': abs,
    'round': round,
    'len': len
})

Integración con FastAPI y datos dinámicos

Las funciones globales pueden acceder a servicios o datos externos. Aquí un ejemplo que integra con una base de datos:

from sqlalchemy.orm import Session
from database import get_db
from models import User, Category

def get_user_name(user_id):
    """Obtiene el nombre de usuario por ID"""
    db = next(get_db())  # En producción, usar dependency injection apropiado
    try:
        user = db.query(User).filter(User.id == user_id).first()
        return user.name if user else "Usuario desconocido"
    finally:
        db.close()

def get_category_tree():
    """Obtiene el árbol de categorías"""
    db = next(get_db())
    try:
        categories = db.query(Category).filter(Category.parent_id.is_(None)).all()
        return categories
    finally:
        db.close()

def calculate_reading_time(content):
    """Calcula el tiempo estimado de lectura"""
    word_count = len(content.split())
    # Promedio de 200 palabras por minuto
    minutes = math.ceil(word_count / 200)
    return f"{minutes} min de lectura"

# Registrar funciones que acceden a datos
templates.env.globals.update({
    'get_user_name': get_user_name,
    'get_category_tree': get_category_tree,
    'calculate_reading_time': calculate_reading_time
})

Ejemplo práctico completo

Veamos un ejemplo completo que utiliza múltiples funciones globales personalizadas en una aplicación FastAPI:

from fastapi import FastAPI, Request, Depends
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
import datetime

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

# Funciones globales personalizadas
def is_recent(date_obj, days=7):
    """Verifica si una fecha es reciente"""
    if isinstance(date_obj, str):
        date_obj = datetime.datetime.fromisoformat(date_obj)
    delta = datetime.datetime.now() - date_obj
    return delta.days <= days

def get_initials(full_name):
    """Obtiene las iniciales de un nombre completo"""
    words = full_name.strip().split()
    return ''.join(word[0].upper() for word in words if word)

def format_file_size(bytes_size):
    """Formatea el tamaño de archivo en unidades legibles"""
    for unit in ['B', 'KB', 'MB', 'GB']:
        if bytes_size < 1024:
            return f"{bytes_size:.1f} {unit}"
        bytes_size /= 1024
    return f"{bytes_size:.1f} TB"

# Registrar todas las funciones globales
templates.env.globals.update({
    'is_recent': is_recent,
    'get_initials': get_initials,
    'format_file_size': format_file_size,
    'enumerate': enumerate,
    'zip': zip
})

@app.get("/dashboard")
async def dashboard(request: Request):
    # Datos de ejemplo
    documents = [
        {
            'name': 'Informe_Q1_2024.pdf',
            'author': 'María González',
            'size': 2048576,
            'created_at': '2024-01-15T10:30:00'
        },
        {
            'name': 'Presentación_Ventas.pptx',
            'author': 'Carlos Ruiz',
            'size': 5242880,
            'created_at': '2024-01-20T14:15:00'
        }
    ]
    
    return templates.TemplateResponse(
        "dashboard.html",
        {"request": request, "documents": documents}
    )

Y el template correspondiente:

<!DOCTYPE html>
<html>
<head>
    <title>Dashboard de Documentos</title>
    <style>
        .recent { color: #28a745; font-weight: bold; }
        .avatar { 
            display: inline-block; 
            width: 40px; 
            height: 40px; 
            border-radius: 50%; 
            background: #007bff; 
            color: white; 
            text-align: center; 
            line-height: 40px;
        }
    </style>
</head>
<body>
    <h1>Documentos Recientes</h1>
    
    <div class="documents-grid">
        {% for doc in documents %}
        <div class="document-card">
            <div class="doc-header">
                <span class="avatar">{{ get_initials(doc.author) }}</span>
                <div class="doc-info">
                    <h3>{{ doc.name }}</h3>
                    <p>por {{ doc.author }}</p>
                </div>
            </div>
            
            <div class="doc-meta">
                <span class="file-size">{{ format_file_size(doc.size) }}</span>
                <span class="created-date {% if is_recent(doc.created_at) %}recent{% endif %}">
                    {{ format_date(doc.created_at, "%d %b %Y") }}
                    {% if is_recent(doc.created_at) %}(Nuevo){% endif %}
                </span>
            </div>
        </div>
        {% endfor %}
    </div>
</body>
</html>

Consideraciones importantes

Al trabajar con funciones globales personalizadas, es importante tener en cuenta:

  • Rendimiento: Las funciones que acceden a bases de datos pueden impactar el rendimiento si se ejecutan múltiples veces por template
  • Seguridad: Funciones que generan HTML deben usar el filtro |safe con precaución
  • Mantenimiento: Organizar las funciones en módulos separados cuando la aplicación crece
  • Testing: Las funciones globales deben ser testeadas independientemente
  • Documentación: Documentar claramente qué hace cada función y sus parámetros

Las funciones globales personalizadas proporcionan una manera elegante de extender las capacidades de Jinja2, permitiendo que la lógica de presentación compleja se mantenga organizada y reutilizable en toda la aplicación FastAPI.

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

  • Comprender qué son los macros en Jinja2 y cómo definirlos para reutilizar código HTML.
  • Aprender a incluir lógica condicional, bucles y contenido anidado dentro de macros.
  • Conocer cómo organizar macros en archivos separados e importarlos en plantillas.
  • Entender la diferencia entre macros y funciones globales personalizadas en Jinja2.
  • Saber registrar y utilizar funciones globales personalizadas en FastAPI para operaciones complejas y reutilizables en templates.