Bootstrap CSS en FastAPI

Intermedio
FastAPI
FastAPI
Actualizado: 15/09/2025

Integración de Bootstrap y componentes básicos

Bootstrap es el framework CSS más utilizado para crear interfaces web responsivas y atractivas. Su integración con FastAPI y Jinja2 permite desarrollar aplicaciones web con un diseño profesional sin necesidad de escribir CSS personalizado extenso.

Configuración de Bootstrap en plantillas FastAPI

La forma más sencilla de incluir Bootstrap en nuestras plantillas es mediante su CDN oficial. Esto nos permite acceder a todas las funcionalidades sin descargar archivos adicionales.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    {{ content }}
    
    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

El viewport meta tag es esencial para el comportamiento responsivo de Bootstrap, asegurando que el contenido se adapte correctamente a diferentes tamaños de pantalla.

Estructura base con containers y grid system

Bootstrap utiliza un sistema de containers para envolver el contenido y proporcionar un ancho máximo apropiado:

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

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

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    return templates.TemplateResponse("home.html", {
        "request": request,
        "title": "Mi Aplicación FastAPI",
        "usuarios": ["Ana", "Carlos", "Elena"]
    })

La plantilla correspondiente utiliza clases de Bootstrap para estructurar el contenido:

{% extends "base.html" %}

{% block content %}
<div class="container mt-4">
    <div class="row">
        <div class="col-md-12">
            <h1 class="display-4 text-center">Bienvenido a {{ title }}</h1>
            <p class="lead text-center">Una aplicación web moderna con Bootstrap</p>
        </div>
    </div>
</div>
{% endblock %}

Componentes de navegación

La navbar de Bootstrap proporciona una navegación consistente y responsiva. Su implementación en Jinja2 permite personalización dinámica:

<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
    <div class="container">
        <a class="navbar-brand" href="/">{{ app_name | default("Mi App") }}</a>
        
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" 
                data-bs-target="#navbarNav">
            <span class="navbar-toggler-icon"></span>
        </button>
        
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav ms-auto">
                <li class="nav-item">
                    <a class="nav-link" href="/">Inicio</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/usuarios">Usuarios</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/contacto">Contacto</a>
                </li>
            </ul>
        </div>
    </div>
</nav>

Cards para presentación de contenido

Las cards de Bootstrap son componentes versátiles para mostrar información estructurada. Su uso con datos dinámicos de FastAPI es especialmente útil:

@app.get("/productos", response_class=HTMLResponse)
async def productos(request: Request):
    productos_data = [
        {"nombre": "Laptop", "precio": 899.99, "descripcion": "Laptop de alto rendimiento"},
        {"nombre": "Mouse", "precio": 29.99, "descripcion": "Mouse inalámbrico ergonómico"},
        {"nombre": "Teclado", "precio": 79.99, "descripcion": "Teclado mecánico retroiluminado"}
    ]
    
    return templates.TemplateResponse("productos.html", {
        "request": request,
        "title": "Nuestros Productos",
        "productos": productos_data
    })

La plantilla utiliza iteración Jinja2 para generar cards dinámicamente:

{% extends "base.html" %}

{% block content %}
<div class="container mt-4">
    <h2 class="mb-4">{{ title }}</h2>
    
    <div class="row">
        {% for producto in productos %}
        <div class="col-md-4 mb-4">
            <div class="card h-100">
                <div class="card-body">
                    <h5 class="card-title">{{ producto.nombre }}</h5>
                    <p class="card-text">{{ producto.descripcion }}</p>
                    <p class="card-text">
                        <small class="text-muted">€{{ "%.2f"|format(producto.precio) }}</small>
                    </p>
                </div>
                <div class="card-footer">
                    <a href="#" class="btn btn-primary btn-sm">Ver detalles</a>
                    <a href="#" class="btn btn-success btn-sm">Comprar</a>
                </div>
            </div>
        </div>
        {% endfor %}
    </div>
</div>
{% endblock %}

Botones y elementos interactivos

Bootstrap ofrece una amplia gama de botones con diferentes estilos y comportamientos. Su integración con FastAPI permite crear interfaces intuitivas:

<!-- Botones básicos -->
<div class="btn-group" role="group">
    <a href="/crear" class="btn btn-primary">
        <i class="bi bi-plus-circle"></i> Crear nuevo
    </a>
    <button type="button" class="btn btn-secondary" data-bs-toggle="modal" 
            data-bs-target="#helpModal">
        Ayuda
    </button>
    <a href="/configuracion" class="btn btn-outline-info">
        Configuración
    </a>
</div>

<!-- Badges para mostrar estado -->
<div class="mt-3">
    {% for usuario in usuarios %}
    <span class="badge bg-{{ 'success' if usuario.activo else 'secondary' }} me-2">
        {{ usuario.nombre }}
        {% if usuario.activo %}
        <i class="bi bi-check-circle"></i>
        {% endif %}
    </span>
    {% endfor %}
</div>

Alerts y mensajes informativos

Los componentes de alerta de Bootstrap son ideales para mostrar mensajes de estado o información importante:

@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request):
    # Simulamos diferentes tipos de mensajes
    mensajes = [
        {"tipo": "success", "texto": "Datos guardados correctamente"},
        {"tipo": "warning", "texto": "Hay 3 tareas pendientes de revisión"},
        {"tipo": "info", "texto": "Nueva actualización disponible"}
    ]
    
    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "title": "Panel de Control",
        "mensajes": mensajes
    })

La implementación en la plantilla utiliza clases dinámicas basadas en los datos:

{% extends "base.html" %}

{% block content %}
<div class="container mt-4">
    <h1>{{ title }}</h1>
    
    <!-- Mostrar mensajes dinámicos -->
    {% if mensajes %}
    <div class="mt-3">
        {% for mensaje in mensajes %}
        <div class="alert alert-{{ mensaje.tipo }} alert-dismissible fade show" role="alert">
            {{ mensaje.texto }}
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
        {% endfor %}
    </div>
    {% endif %}
    
    <!-- Panel de estadísticas -->
    <div class="row mt-4">
        <div class="col-md-3">
            <div class="card text-white bg-primary">
                <div class="card-header">Usuarios activos</div>
                <div class="card-body">
                    <h4 class="card-title">1,247</h4>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-white bg-success">
                <div class="card-header">Ventas hoy</div>
                <div class="card-body">
                    <h4 class="card-title">€3,450</h4>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

Utilidades de espaciado y tipografía

Bootstrap incluye clases utilitarias que simplifican el control del espaciado y la tipografía sin CSS personalizado:

<div class="container">
    <!-- Espaciado responsivo -->
    <div class="mt-5 mb-3 px-4">
        <h2 class="display-5 fw-bold text-primary">
            Título principal
        </h2>
        <p class="lead fs-5 text-muted lh-base">
            Descripción con tipografía optimizada para legibilidad.
        </p>
    </div>
    
    <!-- Alineación y colores -->
    <div class="text-center py-4 bg-light rounded">
        <p class="text-success mb-2">Operación completada</p>
        <small class="text-secondary">{{ fecha_actual }}</small>
    </div>
</div>

La integración completa de Bootstrap con FastAPI y Jinja2 proporciona una base sólida para crear interfaces web profesionales que se adaptan automáticamente a diferentes dispositivos y resoluciones de pantalla.

Formularios responsivos con Bootstrap

Los formularios responsivos son elementos fundamentales en aplicaciones web modernas. Bootstrap proporciona un conjunto completo de clases y componentes que se adaptan automáticamente a diferentes tamaños de pantalla, garantizando una experiencia de usuario óptima en cualquier dispositivo.

Estructura básica de formularios Bootstrap

Bootstrap utiliza clases específicas para estilizar elementos de formulario de manera consistente. La integración con FastAPI permite manejar tanto la presentación como el procesamiento de datos:

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

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

@app.get("/registro", response_class=HTMLResponse)
async def mostrar_registro(request: Request):
    return templates.TemplateResponse("registro.html", {
        "request": request,
        "title": "Registro de Usuario"
    })

@app.post("/registro")
async def procesar_registro(
    request: Request,
    nombre: str = Form(...),
    email: str = Form(...),
    telefono: str = Form(...),
    mensaje: str = Form(...)
):
    # Procesamiento de datos del formulario
    return templates.TemplateResponse("confirmacion.html", {
        "request": request,
        "nombre": nombre,
        "email": email
    })

Controles de formulario responsivos

La plantilla de formulario utiliza clases Bootstrap para crear elementos que se adaptan automáticamente:

{% extends "base.html" %}

{% block content %}
<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-lg-8 col-md-10">
            <div class="card shadow">
                <div class="card-header bg-primary text-white">
                    <h3 class="card-title mb-0">{{ title }}</h3>
                </div>
                <div class="card-body">
                    <form method="post" action="/registro">
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label for="nombre" class="form-label">Nombre completo *</label>
                                <input type="text" class="form-control" id="nombre" 
                                       name="nombre" required>
                            </div>
                            <div class="col-md-6 mb-3">
                                <label for="email" class="form-label">Correo electrónico *</label>
                                <input type="email" class="form-control" id="email" 
                                       name="email" required>
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            <label for="telefono" class="form-label">Teléfono</label>
                            <input type="tel" class="form-control" id="telefono" 
                                   name="telefono" placeholder="+34 600 000 000">
                        </div>
                        
                        <div class="mb-4">
                            <label for="mensaje" class="form-label">Mensaje adicional</label>
                            <textarea class="form-control" id="mensaje" name="mensaje" 
                                      rows="4" placeholder="Cuéntanos algo sobre ti..."></textarea>
                        </div>
                        
                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                            <button type="reset" class="btn btn-outline-secondary me-md-2">
                                Limpiar
                            </button>
                            <button type="submit" class="btn btn-primary">
                                Registrarse
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

Grupos de entrada con iconos y validación

Los input groups de Bootstrap permiten combinar campos de entrada con elementos adicionales como iconos o botones:

@app.get("/login", response_class=HTMLResponse)
async def mostrar_login(request: Request, error: str = None):
    return templates.TemplateResponse("login.html", {
        "request": request,
        "title": "Iniciar Sesión",
        "error": error
    })

@app.post("/login")
async def procesar_login(
    request: Request,
    username: str = Form(...),
    password: str = Form(...)
):
    # Validación básica (en producción usar autenticación real)
    if username == "admin" and password == "password":
        return RedirectResponse(url="/dashboard", status_code=303)
    
    return templates.TemplateResponse("login.html", {
        "request": request,
        "title": "Iniciar Sesión",
        "error": "Credenciales incorrectas",
        "username": username
    })

La plantilla incorpora validación visual y retroalimentación inmediata:

{% extends "base.html" %}

{% block content %}
<div class="container-fluid vh-100">
    <div class="row h-100">
        <div class="col-md-6 d-flex align-items-center justify-content-center">
            <div class="card" style="width: 100%; max-width: 400px;">
                <div class="card-body p-4">
                    <h2 class="card-title text-center mb-4">{{ title }}</h2>
                    
                    {% if error %}
                    <div class="alert alert-danger alert-dismissible fade show" role="alert">
                        <i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                    </div>
                    {% endif %}
                    
                    <form method="post" action="/login" novalidate>
                        <div class="mb-3">
                            <label for="username" class="form-label">Usuario</label>
                            <div class="input-group">
                                <span class="input-group-text">
                                    <i class="bi bi-person"></i>
                                </span>
                                <input type="text" class="form-control" id="username" 
                                       name="username" value="{{ username or '' }}" required>
                            </div>
                        </div>
                        
                        <div class="mb-4">
                            <label for="password" class="form-label">Contraseña</label>
                            <div class="input-group">
                                <span class="input-group-text">
                                    <i class="bi bi-lock"></i>
                                </span>
                                <input type="password" class="form-control" id="password" 
                                       name="password" required>
                                <button class="btn btn-outline-secondary" type="button" 
                                        onclick="togglePassword()">
                                    <i class="bi bi-eye" id="toggleIcon"></i>
                                </button>
                            </div>
                        </div>
                        
                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">
                                Iniciar Sesión
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
        
        <div class="col-md-6 bg-primary d-none d-md-flex align-items-center justify-content-center">
            <div class="text-white text-center">
                <h1 class="display-4 fw-bold mb-3">Bienvenido</h1>
                <p class="lead">Accede a tu cuenta para continuar</p>
            </div>
        </div>
    </div>
</div>

<script>
function togglePassword() {
    const passwordField = document.getElementById('password');
    const toggleIcon = document.getElementById('toggleIcon');
    
    if (passwordField.type === 'password') {
        passwordField.type = 'text';
        toggleIcon.className = 'bi bi-eye-slash';
    } else {
        passwordField.type = 'password';
        toggleIcon.className = 'bi bi-eye';
    }
}
</script>
{% endblock %}

Formularios de múltiples columnas y secciones

Para formularios complejos, Bootstrap permite organizar campos en múltiples columnas que se reorganizan automáticamente en dispositivos móviles:

@app.get("/perfil", response_class=HTMLResponse)
async def mostrar_perfil(request: Request):
    # Datos de ejemplo para pre-llenar el formulario
    usuario_data = {
        "nombre": "Juan",
        "apellidos": "García López",
        "email": "juan.garcia@example.com",
        "telefono": "+34 600 123 456",
        "direccion": "Calle Mayor 123",
        "ciudad": "Madrid",
        "codigo_postal": "28001",
        "pais": "España"
    }
    
    return templates.TemplateResponse("perfil.html", {
        "request": request,
        "title": "Mi Perfil",
        "usuario": usuario_data
    })

La implementación utiliza el sistema de grid de Bootstrap para crear diseños responsivos:

{% extends "base.html" %}

{% block content %}
<div class="container mt-4">
    <div class="row">
        <div class="col-xl-8 col-lg-10 mx-auto">
            <div class="card">
                <div class="card-header">
                    <h3 class="mb-0">{{ title }}</h3>
                    <small class="text-muted">Actualiza tu información personal</small>
                </div>
                <div class="card-body">
                    <form method="post" action="/perfil">
                        <!-- Información personal -->
                        <fieldset class="mb-4">
                            <legend class="fs-5 fw-semibold border-bottom pb-2 mb-3">
                                Información Personal
                            </legend>
                            <div class="row">
                                <div class="col-md-6 mb-3">
                                    <label for="nombre" class="form-label">Nombre</label>
                                    <input type="text" class="form-control" id="nombre" 
                                           name="nombre" value="{{ usuario.nombre }}">
                                </div>
                                <div class="col-md-6 mb-3">
                                    <label for="apellidos" class="form-label">Apellidos</label>
                                    <input type="text" class="form-control" id="apellidos" 
                                           name="apellidos" value="{{ usuario.apellidos }}">
                                </div>
                            </div>
                            
                            <div class="row">
                                <div class="col-lg-8 mb-3">
                                    <label for="email" class="form-label">Email</label>
                                    <input type="email" class="form-control" id="email" 
                                           name="email" value="{{ usuario.email }}">
                                </div>
                                <div class="col-lg-4 mb-3">
                                    <label for="telefono" class="form-label">Teléfono</label>
                                    <input type="tel" class="form-control" id="telefono" 
                                           name="telefono" value="{{ usuario.telefono }}">
                                </div>
                            </div>
                        </fieldset>
                        
                        <!-- Dirección -->
                        <fieldset class="mb-4">
                            <legend class="fs-5 fw-semibold border-bottom pb-2 mb-3">
                                Dirección
                            </legend>
                            <div class="mb-3">
                                <label for="direccion" class="form-label">Dirección</label>
                                <input type="text" class="form-control" id="direccion" 
                                       name="direccion" value="{{ usuario.direccion }}">
                            </div>
                            
                            <div class="row">
                                <div class="col-md-6 mb-3">
                                    <label for="ciudad" class="form-label">Ciudad</label>
                                    <input type="text" class="form-control" id="ciudad" 
                                           name="ciudad" value="{{ usuario.ciudad }}">
                                </div>
                                <div class="col-md-3 mb-3">
                                    <label for="codigo_postal" class="form-label">C.P.</label>
                                    <input type="text" class="form-control" id="codigo_postal" 
                                           name="codigo_postal" value="{{ usuario.codigo_postal }}">
                                </div>
                                <div class="col-md-3 mb-3">
                                    <label for="pais" class="form-label">País</label>
                                    <select class="form-select" id="pais" name="pais">
                                        <option value="España" {{ 'selected' if usuario.pais == 'España' }}>
                                            España
                                        </option>
                                        <option value="Francia" {{ 'selected' if usuario.pais == 'Francia' }}>
                                            Francia
                                        </option>
                                        <option value="Portugal" {{ 'selected' if usuario.pais == 'Portugal' }}>
                                            Portugal
                                        </option>
                                    </select>
                                </div>
                            </div>
                        </fieldset>
                        
                        <!-- Botones de acción -->
                        <div class="row">
                            <div class="col-12">
                                <div class="d-flex flex-column flex-sm-row gap-2 justify-content-end">
                                    <button type="button" class="btn btn-outline-secondary">
                                        Cancelar
                                    </button>
                                    <button type="submit" class="btn btn-primary">
                                        Guardar Cambios
                                    </button>
                                </div>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

Validación en tiempo real con Bootstrap

Bootstrap incluye clases de validación que proporcionan retroalimentación visual inmediata. Su combinación con JavaScript permite validación del lado cliente:

from typing import Optional
from pydantic import BaseModel, EmailStr, validator

class ContactoForm(BaseModel):
    nombre: str
    email: EmailStr
    asunto: str
    mensaje: str
    
    @validator('nombre')
    def validar_nombre(cls, v):
        if len(v.strip()) < 2:
            raise ValueError('El nombre debe tener al menos 2 caracteres')
        return v.strip()
    
    @validator('mensaje')
    def validar_mensaje(cls, v):
        if len(v.strip()) < 10:
            raise ValueError('El mensaje debe tener al menos 10 caracteres')
        return v.strip()

@app.get("/contacto", response_class=HTMLResponse)
async def mostrar_contacto(request: Request, errors: Optional[dict] = None):
    return templates.TemplateResponse("contacto.html", {
        "request": request,
        "title": "Contacto",
        "errors": errors or {}
    })

La plantilla implementa estados de validación que se muestran dinámicamente:

{% extends "base.html" %}

{% block content %}
<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-lg-8">
            <form method="post" action="/contacto" class="needs-validation" novalidate>
                <div class="mb-3">
                    <label for="nombre" class="form-label">Nombre *</label>
                    <input type="text" 
                           class="form-control {{ 'is-invalid' if errors.get('nombre') else '' }}" 
                           id="nombre" name="nombre" required minlength="2">
                    {% if errors.get('nombre') %}
                    <div class="invalid-feedback">
                        {{ errors.nombre }}
                    </div>
                    {% else %}
                    <div class="invalid-feedback">
                        Por favor, introduce un nombre válido.
                    </div>
                    {% endif %}
                </div>
                
                <div class="mb-3">
                    <label for="email" class="form-label">Email *</label>
                    <input type="email" 
                           class="form-control {{ 'is-invalid' if errors.get('email') else '' }}" 
                           id="email" name="email" required>
                    {% if errors.get('email') %}
                    <div class="invalid-feedback">
                        {{ errors.email }}
                    </div>
                    {% else %}
                    <div class="invalid-feedback">
                        Por favor, introduce un email válido.
                    </div>
                    {% endif %}
                </div>
                
                <div class="mb-3">
                    <label for="asunto" class="form-label">Asunto *</label>
                    <select class="form-select {{ 'is-invalid' if errors.get('asunto') else '' }}" 
                            id="asunto" name="asunto" required>
                        <option value="">Selecciona un asunto</option>
                        <option value="consulta">Consulta general</option>
                        <option value="soporte">Soporte técnico</option>
                        <option value="comercial">Información comercial</option>
                    </select>
                    <div class="invalid-feedback">
                        Por favor, selecciona un asunto.
                    </div>
                </div>
                
                <div class="mb-4">
                    <label for="mensaje" class="form-label">Mensaje *</label>
                    <textarea class="form-control {{ 'is-invalid' if errors.get('mensaje') else '' }}" 
                              id="mensaje" name="mensaje" rows="5" required minlength="10"></textarea>
                    {% if errors.get('mensaje') %}
                    <div class="invalid-feedback">
                        {{ errors.mensaje }}
                    </div>
                    {% else %}
                    <div class="invalid-feedback">
                        El mensaje debe tener al menos 10 caracteres.
                    </div>
                    {% endif %}
                    <div class="form-text">
                        Mínimo 10 caracteres. <span id="charCount">0/10</span>
                    </div>
                </div>
                
                <div class="d-grid">
                    <button type="submit" class="btn btn-primary btn-lg">
                        Enviar Mensaje
                    </button>
                </div>
            </form>
        </div>
    </div>
</div>

<script>
// Validación en tiempo real y contador de caracteres
document.addEventListener('DOMContentLoaded', function() {
    const form = document.querySelector('.needs-validation');
    const mensajeField = document.getElementById('mensaje');
    const charCount = document.getElementById('charCount');
    
    // Contador de caracteres
    mensajeField.addEventListener('input', function() {
        const count = this.value.length;
        charCount.textContent = `${count}/10`;
        charCount.className = count >= 10 ? 'text-success' : 'text-muted';
    });
    
    // Validación del formulario
    form.addEventListener('submit', function(event) {
        if (!form.checkValidity()) {
            event.preventDefault();
            event.stopPropagation();
        }
        form.classList.add('was-validated');
    });
    
    // Validación en tiempo real para cada campo
    const inputs = form.querySelectorAll('input, select, textarea');
    inputs.forEach(input => {
        input.addEventListener('blur', function() {
            if (this.checkValidity()) {
                this.classList.remove('is-invalid');
                this.classList.add('is-valid');
            } else {
                this.classList.remove('is-valid');
                this.classList.add('is-invalid');
            }
        });
    });
});
</script>
{% endblock %}

Los formularios responsivos con Bootstrap en FastAPI proporcionan una experiencia de usuario coherente y profesional, adaptándose automáticamente a diferentes dispositivos mientras mantienen la funcionalidad completa de validación y procesamiento de datos.

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 cómo integrar Bootstrap mediante CDN en plantillas Jinja2 con FastAPI.
  • Aprender a estructurar páginas usando containers, grid system y componentes básicos de Bootstrap.
  • Implementar componentes interactivos como navbar, cards, botones y alertas con datos dinámicos.
  • Diseñar formularios responsivos con validación visual y manejo de datos en FastAPI.
  • Aplicar utilidades de Bootstrap para espaciado, tipografía y validación en tiempo real.