SENA - ADSO

Full Stack Web:JSP → FastAPI → React

Guía completa de aprendizaje para construir aplicaciones web profesionales conJakarta EE 10,JDK 21,PostgreSQL,FastAPIyReact 19, aplicando el patrón de diseñoModelo-Vista-Controlador.

56hDuración
23+Archivos
3Tablas BD
5+Simuladores
FastAPIAPI REST
ReactFrontend SPA
ProductoServlet.java
@WebServlet("/productos")
public class ProductoServlet extends HttpServlet {
    private final ProductoDAO dao = new ProductoDAO();
    
    @Override
    protected void doGet(HttpServletRequest req, 
                         HttpServletResponse resp) {
        String accion = req.getParameter("accion");
        switch (accion) {
            case "nuevo" -> mostrarFormulario(req, resp);
            case "editar" -> mostrarEditar(req, resp);
            case "eliminar" -> eliminar(req, resp);
            default -> listar(req, resp);
        }
    }
}

Stack Tecnológico

Del ecosistema Java empresarial al stack moderno Python + React

JDK 21

Versión LTS con Records, Pattern Matching y Virtual Threads

LTS

Jakarta EE 10

Estándar empresarial con Servlet 6.0 y JSP 3.1

Enterprise

PostgreSQL 16

Base de datos relacional robusta y de código abierto

Open Source

Tomcat 10.1

Servidor de aplicaciones compatible con Jakarta EE 10

Web Server

Maven 3.9

Gestión de dependencias y automatización de builds

Build Tool

Docker

Contenedores para despliegue portable y reproducible

DevOps

FastAPI

Framework moderno para APIs REST con Python 3.10+

Python

React 19

Biblioteca moderna para interfaces de usuario SPA

Frontend

Haz clic en cualquier tarjeta o en el encabezado para ver los detalles completos de cada tecnología

JDK 21 - Java Development Kit

La base del desarrollo Java moderno

¿Qué es JDK 21?

JDK 21 es la versiónLTS (Long Term Support)más reciente de Java, lanzada en septiembre de 2023. Es la plataforma de desarrollo que incluye el compilador (javac), la máquina virtual (JVM) y todas las herramientas necesarias para crear aplicaciones Java.

Soporte hasta 2031

Actualizaciones de seguridad por 8 años

Mejor rendimiento

Hasta 40% más rápido que JDK 11

Sintaxis moderna

Records, Pattern Matching, Switch Expressions

Características Principales

Records

Clases inmutables con sintaxis concisa

public record Producto(
    int id,
    String nombre,
    BigDecimal precio
) {
    // Constructor alternativo
    public Producto(String nombre, BigDecimal precio) {
        this(0, nombre, precio);
    }
}

// Uso:
Producto p = new Producto("Laptop", new BigDecimal("2500000"));
System.out.println(p.nombre());
// "Laptop"
Pattern Matching

Validación de tipos simplificada

// Antes (verbose):
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Ahora (conciso):
if (obj instanceof String s) {
    System.out.println(s.length());
}

// Con switch:
String tipo = switch (obj) {
    case Integer i -> "Número entero: " + i;
    case String s  -> "Texto: " + s;
    case null      -> "Nulo";
    default        -> "Otro tipo";
};
Virtual Threads

Concurrencia masiva eficiente

// Crear 1 millón de threads sin problemas
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 1_000_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}
// Cada thread es ligero (~1KB vs ~1MB de threads tradicionales)
Switch Expressions

Sintaxis moderna con flechas

// Switch tradicional (antiguo):
String nombre;
switch (dia) {
    case 1:
        nombre = "Lunes";
        break;
    case 2:
        nombre = "Martes";
        break;
    default:
        nombre = "Otro";
        break;
}

// Switch expression (moderno):
String nombre = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> "Miércoles";
    default -> "Otro día";
};

Instalación

1
Descargar JDK 21

Visitaadoptium.nety descarga Eclipse Temurin JDK 21 para tu sistema operativo

2
Instalar y configurar JAVA_HOME
# Windows (PowerShell como administrador):
$ruta = 'C:\Program Files\Eclipse Adoptium\jdk-21.0.0.0-hotspot'
[System.Environment]::SetEnvironmentVariable('JAVA_HOME', $ruta, 'Machine')
[System.Environment]::SetEnvironmentVariable(
    'Path', $env:Path + ";$ruta\bin", 'Machine')

# Linux/Mac (en ~/.bashrc o ~/.zshrc):
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
export PATH=$JAVA_HOME/bin:$PATH
3
Verificar instalación
java --version
# Debe mostrar: openjdk 21.x.x
javac --version
# Debe mostrar: javac 21.x.x

Tips y Mejores Prácticas

  • Usa records para DTOs y entidades inmutables- Reducen boilerplate y son thread-safe
  • Pattern matching en if/switch- Código más seguro y legible
  • Virtual Threads para I/O- Ideal para aplicaciones web con muchas llamadas a BD
  • Text blocks para SQL/JSON-"""SELECT * FROM..."""más legible
  • Sealed classes- Controla qué clases pueden extender

Jakarta EE 10 - Enterprise Edition

El estándar para aplicaciones web empresariales

¿Qué es Jakarta EE 10?

Jakarta EE es la evolución de Java EE, mantenida por la Eclipse Foundation desde 2017. Es un conjunto de especificaciones para desarrollar aplicaciones empresariales en Java, incluyendo Servlets, JSP, JPA, CDI, y más.

Java EE (antes de 2017)
import javax.servlet.*;
import javax.servlet.http.*;
import javax.persistence.*;
Jakarta EE (desde 2017)
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.persistence.*;

Componentes Principales

Servlet 6.0

Manejo de peticiones HTTP

@WebServlet("/productos")
public class ProductoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req,                          HttpServletResponse resp)             throws ServletException, IOException {
        // Lógica aquí        req.getRequestDispatcher("/lista.jsp")           .forward(req, resp);
    }
}
JSP 3.1

Páginas dinámicas con Expression Language

<%@ taglib prefix="c"
uri="jakarta.tags.core" %><c:forEach var="p" items="${productos}">
<tr>
<td>${p.nombre}</td>
<td>$${p.precio}</td>
</tr></c:forEach>
JSTL 3.0

Standard Tag Library para JSP

<%@ taglib prefix="c"
uri="jakarta.tags.core" %><c:if test="${usuario != null}">
Bienvenido ${usuario.nombre}</c:if><c:choose>
<c:when test="${stock > 10}">
Disponible
</c:when>
<c:otherwise>
Agotado
</c:otherwise></c:choose>
WebSocket 2.1

Comunicación en tiempo real

public class ChatEndpoint {
    public void onMessage(String message,                           Session session) {
        // Broadcast a todos        session.getOpenSessions().forEach(s -> {
            s.getAsyncRemote().sendText(message);
        });
}
}

Configuración en Maven

<!-- pom.xml -->
<dependencies>
    <!-- Jakarta Servlet API (proporcionado por Tomcat) -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>
    <!-- Jakarta JSTL -->
    <dependency>
        <groupId>jakarta.servlet.jsp.jstl</groupId>
        <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
        <version>3.0.0</version>
    </dependency>
    <!-- Implementación JSTL -->
    <dependency>
        <groupId>org.glassfish.web</groupId>
        <artifactId>jakarta.servlet.jsp.jstl</artifactId>
        <version>3.0.1</version>
    </dependency>
</dependencies>

Tips y Mejores Prácticas

  • Usa anotaciones-@WebServlet,@WebFilteren lugar de web.xml
  • JSTL sobre scriptlets- Nunca uses<% %>, siempre<c:forEach>
  • Expression Language-${producto.nombre}es más seguro que<%= %>
  • Scope correcto- Usarequestpara datos temporales,sessionpara usuario
  • Forward vs Redirect- Forward mantiene URL, Redirect cambia URL

PostgreSQL 16

Base de datos relacional robusta y de código abierto

¿Qué es PostgreSQL?

PostgreSQL es un sistema de gestión de bases de datos relacional (RDBMS) de código abierto, conocido por su robustez, cumplimiento de estándares SQL y características avanzadas. Es la base de datos preferida para aplicaciones empresariales modernas.

ACID Compliant

Transacciones seguras y confiables

Escalabilidad

Soporta terabytes de datos

Extensible

Tipos personalizados, funciones, índices

Tipos de Datos Principales

Tipo Descripción Ejemplo
SERIAL Entero auto-incremental id SERIAL PRIMARY KEY
VARCHAR(n) Texto de longitud variable nombre VARCHAR(100)
TEXT Texto sin límite descripcion TEXT
INTEGER Número entero stock INTEGER
DECIMAL(p,s) Número decimal preciso precio DECIMAL(10,2)
BOOLEAN Verdadero/Falso estado BOOLEAN DEFAULT TRUE
TIMESTAMP Fecha y hora fecha TIMESTAMP DEFAULT NOW()
JSONB JSON binario metadata JSONB

Ejemplos SQL

Crear tabla con restricciones
CREATE TABLE productos (
    id SERIAL PRIMARY KEY,
    codigo VARCHAR(50) UNIQUE NOT NULL,
    nombre VARCHAR(100) NOT NULL,
    descripcion TEXT,
    precio DECIMAL(10,2) CHECK (precio >= 0),
    stock INTEGER DEFAULT 0 CHECK (stock >= 0),
    categoria VARCHAR(50),
    estado BOOLEAN DEFAULT TRUE,
    fecha_registro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Insertar datos
INSERT INTO productos (codigo, nombre, precio, stock, categoria)
VALUES
    ('PROD-001', 'Laptop HP Pavilion', 2500000.00, 15, 'Tecnología'),
    ('PROD-002', 'Mouse Logitech', 45000.00, 50, 'Accesorios');

-- Insertar y retornar el ID generado
INSERT INTO productos (codigo, nombre, precio, stock)
VALUES ('PROD-003', 'Teclado', 180000.00, 30)
RETURNING id;
Consultas avanzadas
-- JOIN entre tablas
SELECT u.username, r.nombre as rol
FROM usuarios u
INNER JOIN roles r ON u.rol_id = r.id
WHERE u.estado = TRUE;

-- Agrupación y agregación
SELECT categoria,
       COUNT(*) as total,
       AVG(precio) as precio_promedio,
       SUM(stock) as stock_total
FROM productos
WHERE estado = TRUE
GROUP BY categoria
HAVING COUNT(*) > 5
ORDER BY total DESC;

-- Subconsulta
SELECT * FROM productos
WHERE precio > (SELECT AVG(precio) FROM productos);
Índices para mejor rendimiento
-- Índice en columna frecuentemente consultada
CREATE INDEX idx_productos_categoria ON productos(categoria);

-- Índice compuesto
CREATE INDEX idx_productos_estado_categoria ON productos(estado, categoria);

-- Índice único
CREATE UNIQUE INDEX idx_productos_codigo ON productos(codigo);

Instalación

1
Descargar PostgreSQL 16

Visitapostgresql.org/downloady descarga el instalador para tu sistema

2
Instalar y configurar

Durante la instalación, establece una contraseña para el usuariopostgres. Recuerda esta contraseña para la conexión JDBC.

3
Verificar instalación
# Abrir psql (cliente de línea de comandos)
psql -U postgres
# En psql:
CREATE DATABASE inventario_db;
\c inventario_db
\dt  -- Listar tablas

Tips y Mejores Prácticas

  • Usa SERIAL para IDs auto-incrementales- Más simple que secuencias manuales
  • Índices en columnas WHERE frecuentes- Mejora rendimiento de consultas
  • Constraints para integridad-NOT NULL,UNIQUE,CHECK
  • Usa DECIMAL para dinero- Nunca FLOAT (problemas de precisión)
  • TIMESTAMP WITH TIME ZONE- Para aplicaciones multi-zona horaria

Apache Tomcat 10.1

Servidor de aplicaciones Jakarta EE

¿Qué es Tomcat?

Apache Tomcat es un servidor de aplicaciones web de código abierto que implementa las tecnologías Jakarta EE (Servlet, JSP, WebSocket). Es el servidor más utilizado para aplicaciones Java web.

Compatible con Jakarta EE 10

Soporta Servlet 6.0, JSP 3.1, WebSocket 2.1

Ligero y rápido

Inicio en segundos, bajo consumo de memoria

Altamente configurable

Thread pools, connection pools, SSL/TLS

Estructura de Directorios

apache-tomcat-10.1.x/
├── bin/              # Scripts de inicio/parada
│   ├── startup.bat   # Iniciar en Windows
│   ├── shutdown.bat  # Parar en Windows
│   ├── startup.sh    # Iniciar en Linux/Mac
│   └── shutdown.sh   # Parar en Linux/Mac
├── conf/             # Configuración
│   ├── server.xml    # Configuración principal
│   ├── web.xml       # Configuración global
│   └── tomcat-users.xml  # Usuarios y roles
├── webapps/          # Aplicaciones desplegadas
│   ├── ROOT/         # Aplicación raíz
│   └── mi-app.war    # Tu aplicación
├── logs/             # Logs del servidor
├── lib/              # Librerías compartidas
└── temp/             # Archivos temporales

Comandos Básicos

Iniciar Tomcat
# Windows
cd C:\apache-tomcat-10.1.x\bin
startup.bat
# Linux/Mac
cd /opt/apache-tomcat-10.1.x/bin
./startup.sh
# Verificar: http://localhost:8080
Parar Tomcat
# Windows
shutdown.bat
# Linux/Mac
./shutdown.sh
# Forzar parada (si no responde)
kill -9 $(cat tomcat.pid)
Desplegar aplicación
# Copiar WAR a webapps
copy target\mi-app.war C:\apache-tomcat-10.1.x\webapps\
# O usar Tomcat Manager
# http://localhost:8080/manager/html
# Despliegue automático al reiniciar
Ver logs
# Ver log en tiempo real
tail -f logs/catalina.out
# O en Windows
Get-Content logs\catalina.out -Wait
# Logs de aplicación
logs\localhost.YYYY-MM-DD.log

Configuración Importante

<!-- conf/server.xml - Configuración de puerto -->
<Connector port="8080"
    protocol="HTTP/1.1"
    connectionTimeout="20000"
    redirectPort="8443"
    maxThreads="200"
    minSpareThreads="10" />

<!-- Configuración de SSL/TLS (HTTPS) -->
<Connector port="8443"
    protocol="org.apache.coyote.http11.Http11NioProtocol"
    maxThreads="150"
    SSLEnabled="true">
    <SSLHostConfig>
        <Certificate certificateKeystoreFile="conf/keystore.jks"
            certificateKeystorePassword="changeit" />
    </SSLHostConfig>
</Connector>

Tips y Mejores Prácticas

  • Usa Tomcat 10.1+ para Jakarta EE 10- Tomcat 9 solo soporta Java EE 8
  • Cambia el puerto por defecto- Evita conflictos con otras aplicaciones
  • Configura connection pool- Para mejor rendimiento con BD
  • Habilita HTTPS en producción- Nunca uses HTTP plano en producción
  • Monitorea logs regularmente- Detecta errores y problemas de rendimiento

Apache Maven 3.9

Gestión de dependencias y automatización de builds

¿Qué es Maven?

Maven es una herramienta de gestión de proyectos y automatización de builds para Java. Gestiona dependencias, compila código, ejecuta tests y empaqueta aplicaciones de forma estandarizada.

Gestión de dependencias

Descarga librerías automáticamente desde Maven Central

Estructura estándar

Convención sobre configuración

Ciclo de vida definido

compile, test, package, install, deploy

Estructura pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <!-- Coordenadas del proyecto -->
    <groupId>com.sena.inventario</groupId>
    <artifactId>inventario-mvc</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <!-- Propiedades -->
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- Dependencias -->
    <dependencies>
        <!-- Jakarta Servlet API -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- PostgreSQL Driver -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.1</version>
        </dependency>

        <!-- BCrypt -->
        <dependency>
            <groupId>at.favre.lib</groupId>
            <artifactId>bcrypt</artifactId>
            <version>0.10.2</version>
        </dependency>
    </dependencies>

    <!-- Plugins de build -->
    <build>
        <finalName>${project.artifactId}-${project.version}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Comandos Maven

Compilar proyecto
mvn compile# Compila código Java a .class
Ejecutar tests
mvn test# Ejecuta pruebas unitarias
Empaquetar WAR
mvn package# Genera target/inventario-mvc-1.0.war
Instalar en repositorio local
mvn install# Instala en ~/.m2/repository
Limpiar proyecto
mvn clean# Elimina directorio target/
Build completo
mvn clean package# Limpia y empaqueta desde cero

Instalación

1
Descargar Maven

Visitamaven.apache.org/downloady descarga la versión binaria

2
Extraer y configurar
# Extraer a C:\apache-maven-3.9.x

# Windows (PowerShell como administrador):
[System.Environment]::SetEnvironmentVariable('MAVEN_HOME', 'C:\apache-maven-3.9.x', 'Machine')
[System.Environment]::SetEnvironmentVariable('Path', $env:Path + ';C:\apache-maven-3.9.x\bin', 'Machine')

# Linux/Mac (en ~/.bashrc):
export MAVEN_HOME=/opt/apache-maven-3.9.x
export PATH=$MAVEN_HOME/bin:$PATH
3
Verificar instalación
mvn --version# Debe mostrar: Apache Maven 3.9.x

Tips y Mejores Prácticas

  • Usa scope "provided" para Servlet API- Tomcat ya la incluye
  • Mantén versiones actualizadas- Usa dependencias recientes y seguras
  • Usa properties para versiones-<jakarta.version>6.0.0</jakarta.version>
  • mvn clean package antes de desplegar- Evita archivos obsoletos
  • Configura repositorio corporativo- Nexus/Artifactory para empresas

Docker

Contenedores para despliegue portable

¿Qué es Docker?

Docker es una plataforma de contenedores que permite empaquetar aplicaciones con todas sus dependencias en unidades estandarizadas llamadas contenedores. Estos contenedores pueden ejecutarse en cualquier sistema que tenga Docker instalado.

Portabilidad

"Funciona en mi máquina" = funciona en todas

Aislamiento

Cada aplicación en su propio contenedor

Ligero

Contenedores vs máquinas virtuales (MB vs GB)

Dockerfile para tu aplicación

FROM tomcat:10.1-jdk21-temurin-jakarta

LABEL maintainer="ADSO SENA"
LABEL version="1.0"
LABEL description="Sistema de Inventario JSP MVC"

RUN rm -rf /usr/local/tomcat/webapps/*

ENV TZ=America/Bogota
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

COPY target/inventario-mvc-1.0.war /usr/local/tomcat/webapps/ROOT.war

EXPOSE 8080

ENV DB_HOST=localhost
ENV DB_PORT=5432
ENV DB_NAME=inventario_db
ENV DB_USER=postgres
ENV DB_PASSWORD=postgres

CMD ["catalina.sh", "run"]

Comandos Docker

Construir imagen
docker build -t inventario-mvc:1.0 .
# -t = tag (nombre:versión)
# . = contexto actual
Listar imágenes
docker images# Muestra todas las imágenes locales
Ejecutar contenedor
docker run -d -p 8080:8080 \
  -e DB_HOST=host.docker.internal \
  -e DB_PORT=5432 \
  -e DB_NAME=inventario_db \
  -e DB_USER=postgres \
  -e DB_PASSWORD=tu_password \
  --name inventario-app \
  inventario-mvc:1.0

# -d = detached (segundo plano)
# -p = mapeo de puertos (host:contenedor)
# -e = variables de entorno
# --name = nombre del contenedor
Listar contenedores
docker ps
# Muestra contenedores en ejecución
docker ps -a
# Muestra todos (incluyendo parados)
Ver logs
docker logs inventario-app
# Muestra logs del contenedor
docker logs -f inventario-app
# -f = follow (tiempo real)
Parar/Eliminar
docker stop inventario-app
# Para el contenedor
docker rm inventario-app
# Elimina el contenedor
docker rmi inventario-mvc:1.0
# Elimina la imagen

Docker Compose (Multi-contenedor)

# docker-compose.yml
version: '3.8'

services:
  # Base de datos PostgreSQL
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: inventario_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: tu_password_seguro
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./recursos/sql/inventario_db.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - app-network

  # Aplicación Java
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_NAME: inventario_db
      DB_USER: postgres
      DB_PASSWORD: tu_password_seguro
    depends_on:
      - db
    networks:
      - app-network

volumes:
  pgdata:

networks:
  app-network:
    driver: bridge
# Levantar todo
docker compose up -d

# Ver estado
docker compose ps

# Ver logs de todos los servicios
docker compose logs -f

# Parar todo
docker compose down

# Parar y eliminar volúmenes
docker compose down -v

Instalación

1
Descargar Docker Desktop

Visitadocker.com/products/docker-desktopy descarga para tu sistema operativo

2
Instalar y configurar

Ejecuta el instalador y reinicia tu computador. Docker Desktop incluye Docker Engine, Docker CLI y Docker Compose.

3
Verificar instalación
docker --version
# Debe mostrar: Docker version 24.x.x
docker compose version
# Debe mostrar: Docker Compose version v2.x.x

Tips y Mejores Prácticas

  • Usa imágenes oficiales-tomcat:10.1-jdk21-temurin-jakartaes confiable
  • Minimiza capas en Dockerfile- Combina comandosRUNcon&&
  • Usa .dockerignore- Excluyenode_modules,.git,target
  • Variables de entorno para configuración- Nunca hardcodees credenciales
  • Docker Compose para desarrollo- Levanta BD + app con un comando

FastAPI - APIs REST Modernas con Python

El reemplazo moderno del backend JSP/Servlet

¿Qué es FastAPI?

FastAPI es un framework moderno para construir APIs REST con Python 3.10+. Es la alternativa directa a Jakarta EE + Servlets, pero con sintaxis Python moderna, validación automática con Pydantic y documentación interactiva generada automáticamente. Si ya dominas JSP + MVC, FastAPI te resultará familiar: tienes Modelos (Pydantic/SQLAlchemy), Controladores (rutas/routers), y respuestas JSON.

Rendimiento superior

Tan rápido como Node.js o Go (gracias a Starlette + Uvicorn)

Validación automática

Pydantic valida tipos y datos automáticamente

Documentación interactiva

Swagger UI y ReDoc generados automáticamente

Características Principales

Pydantic Models

Validación de datos con tipos Python

from pydantic import BaseModel
from decimal import Decimal
from datetime import datetime

class Producto(BaseModel):
    id: int | None = None
    codigo: str
    nombre: str
    precio: Decimal
    stock: int = 0
    categoria: str | None = None
    estado: bool = True
    fecha_registro: datetime | None = None

# Uso con validación automática:
p = Producto(codigo="P001", nombre="Laptop",
             precio=2500000.00, stock=15)
print(p.model_dump())
# {"codigo": "P001", "nombre": "Laptop",
#  "precio": Decimal('2500000.00'), "stock": 15,
#  "estado": True, "categoria": None, ...}
Rutas con Tipado

Parámetros de ruta y query con tipos

from fastapi import FastAPI

app = FastAPI()

@app.get("/productos")
async def listar_productos(
    categoria: str | None = None,
    stock_min: int = 0
):
    """GET /productos?categoria=Tecnologia&stock_min=10"""
    return {"categoria": categoria, "stock_min": stock_min}

@app.get("/productos/{id}")
async def obtener_producto(id: int):
    """GET /productos/5 - Retorna: {"id": 5, ...}"""
    return {"id": id, "nombre": "Ejemplo"}
Documentación Swagger

UI interactiva automática en /docs

# FastAPI genera automáticamente:
# http://localhost:8000/docs → Swagger UI
# http://localhost:8000/redoc → ReDoc
#
# Puedes probar los endpoints directamente
# desde el navegador sin Postman
SQLAlchemy + Async

ORM moderno con soporte async

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# Conexión a PostgreSQL (como con JDBC)
engine = create_engine(
    "postgresql://postgres:pass@localhost:5432/inventario_db"
)

# Equivalente al PreparedStatement de JDBC
with Session(engine) as session:
    productos = session.query(ProductoDB).all()
    # Retorna lista de objetos ProductoDB

Migración: JSP/Servlet → FastAPI

Jakarta EE (JSP + Servlet)
// ProductoServlet.java - 100+ líneas

@WebServlet("/productos")
public class ProductoServlet extends HttpServlet {
    private ProductoDAO dao = new ProductoDAO();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        List<Producto> lista = dao.listarTodos();
        req.setAttribute("productos", lista);
        req.getRequestDispatcher("lista.jsp").forward(req, resp);
    }
}
FastAPI (Python)
# productos_api.py - 10 líneas
@router.get("/productos")
async def listar_productos(db: Session = Depends(get_db)):
    """GET /api/productos → JSON array"""
    productos = db.query(ProductoDB).all()
    return [Producto.model_validate(p) for p in productos]

Instalación

1
Instalar Python 3.10+

Descarga desdepython.orgy verifica:python --version

2
Crear entorno virtual e instalar
# Crear entorno virtual
python -m venv venv

# Activar (Windows)
venv\Scripts\activate

# Activar (Linux/Mac)
source venv/bin/activate

# Instalar FastAPI + servidor + ORM
pip install fastapi uvicorn sqlalchemy psycopg2-binary pydantic
3
Verificar instalación
python -c "import fastapi; print(fastapi.__version__)"
# Debe mostrar: 0.111.0 o superior

# Iniciar servidor de prueba
uvicorn main:app --reload
# http://localhost:8000/docs → Swagger UI

Tips y Mejores Prácticas

  • Usa type hints siempre- FastAPI usa los tipos Python para validación y documentación
  • Separa routers por entidad-productos.py,usuarios.py(como los Servlets)
  • Usa dependencias para sesiones-Depends(get_db)es como el patrón DAO
  • Pydantic v2 para modelos-model_validate()en vez defrom_orm()
  • Uvicorn --reload en desarrollo- Recarga automática al cambiar código (como Tomcat con debug)

React 19 - Interfaces de Usuario Modernas

El reemplazo moderno de las vistas JSP

¿Qué es React?

React es una biblioteca de JavaScript para construir interfaces de usuario. Es la alternativa moderna a las vistas JSP: mientras JSP renderiza HTML en el servidor, React renderiza componentes en el navegador (cliente) y se comunica con el backend vía APIs REST. Si entiendes MVC, React es la "Vista" potenciada: componentes reutilizables, estado local, y efectos para consumir APIs.

Componentes

UI encapsulada y reutilizable (como tags JSP personalizados)

Estado y Efectos

useState, useEffect para datos dinámicos

Virtual DOM

Actualizaciones eficientes sin recargar página

Características Principales

Componente Funcional

UI como funciones JavaScript

function ProductoCard({ producto }) {
    return (
        <div className="producto-card">
            <h3>{producto.nombre}</h3>
            <p className="precio">
                ${Number(producto.precio).toLocaleString()}
            </p>
            <span className={stock ${producto.stock < 10 ? 'bajo' : 'normal'}}>
                {producto.stock} unidades
            </span>
        </div>
    );
}

// Uso:
// <ProductoCard producto={{nombre:"Laptop", precio:2500000, stock:15}} />

// Resultado renderizado:
// ┌─────────────────────────┐
// │ Laptop                  │
// │ $2,500,000              │
// │ 15 unidades (normal)    │
// └─────────────────────────┘
useState + useEffect

Estado local y llamadas a API

import { useState, useEffect } from 'react';
import axios from 'axios';

function ListaProductos() {
    const [productos, setProductos] = useState([]);
    const [cargando, setCargando] = useState(true);

    useEffect(() => {
        // Equivalente a: ProductoDAO.listarTodos()
        axios.get('http://localhost:8000/api/productos')
            .then(res => setProductos(res.data))
            .finally(() => setCargando(false));
    }, []);

    if (cargando) return <p>Cargando...</p>;

    return (
        <div className="grid">
            {productos.map(p => (
                <ProductoCard key={p.id} producto={p} />
            ))}
        </div>
    );
}
Formularios Controlados

Manejo de formularios con estado

function FormularioProducto() {
    const [form, setForm] = useState({
        codigo: '', nombre: '', precio: '', stock: 0
    });

    const handleSubmit = async (e) => {
        e.preventDefault();
        // POST a FastAPI (como Servlet doPost)
        await axios.post(
            'http://localhost:8000/api/productos',
            form
        );
        alert('Producto creado!');
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                name="codigo"
                value={form.codigo}
                onChange={e => setForm({ ...form, codigo: e.target.value })}
            />
            <button type="submit">Guardar</button>
        </form>
    );
}
React Router

Navegación SPA sin recargar

import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
    return (
        <BrowserRouter>
            <nav>
                <Link to="/">Inicio</Link>
                <Link to="/productos">Productos</Link>
            </nav>
            <Routes>
                <Route path="/" element={<Inicio />} />
                <Route path="/productos" element={<ListaProductos />} />
                <Route path="/productos/nuevo" element={<FormularioProducto />} />
            </Routes>
        </BrowserRouter>
    );
}

Migración: JSP → React

JSP con JSTL (Server-side)
<table>
<c:forEach var="p" items="${productos}">
<tr>
<td>${p.codigo}</td>
<td>${p.nombre}</td>
<td>$${p.precio}</td>
</tr>
</c:forEach></table>
React (Client-side)
<table>
    <tbody>
        {productos.map(p => (
            <tr key={p.id}>
                <td>{p.codigo}</td>
                <td>{p.nombre}</td>
                <td>${Number(p.precio).toLocaleString()}</td>
            </tr>
        ))}
    </tbody>
</table>

Instalación

1
Instalar Node.js 18+

Descarga desdenodejs.orgy verifica:node --version

2
Crear proyecto con Vite
# Crear proyecto React con Vite
npm create vite@latest frontend-inventario -- --template react

# Entrar e instalar dependencias
cd frontend-inventario
npm install
npm install axios react-router-dom

# Iniciar en desarrollo
npm run dev
# http://localhost:5173
3
Estructura del proyecto
frontend-inventario/
├── src/
│   ├── components/      # Componentes reutilizables
│   │   ├── ProductoCard.jsx
│   │   └── Navbar.jsx
│   ├── pages/           # Páginas (vistas)
│   │   ├── Inicio.jsx
│   │   ├── ListaProductos.jsx
│   │   └── FormularioProducto.jsx
│   ├── services/        # Llamadas a la API
│   │   └── api.js
│   ├── App.jsx          # Componente raíz
│   └── main.jsx         # Punto de entrada
├── index.html
└── package.json

Tips y Mejores Prácticas

  • Componentes pequeños y enfocados- Cada componente hace una sola cosa (como JSP dividido)
  • Axios para llamadas HTTP- Maneja mejor los errores que fetch nativo
  • Custom hooks para lógica compartida-useProductos()como el patrón DAO
  • useEffect sin dependencias = componentDidMount- Carga datos al montar el componente
  • Vite sobre CRA- Más rápido, menor peso, mejor DX

Ruta de Aprendizaje

Tu camino paso a paso hacia el dominio de JSP + MVC

Por qué esta guía está organizada así

Esta ruta de aprendizaje sigue tres principios de la psicología cognitiva: 1) Andamiaje (scaffolding) — primero construyes una base sólida con conceptos simples (MVC, JSP), luego añades capas de complejidad (JDBC, ORM). 2) Aprendizaje espaciado — los conceptos se repiten en distintos contextos (JDBC en fundamentos, JDBC en tutorial, JDBC vs ORM). 3) Transferencia — al final aplicas lo aprendido en un stack completamente diferente (FastAPI + React), demostrando que has adquirido conocimientos transferibles, no solo memoria mecánica. Cada módulo está diseñado para que llegues al siguiente con la preparación justa — ni tan fácil que te aburras, ni tan difícil que te frustres.

01

Identificación de la Guía

Información oficial del programa de formación

Programa Análisis y Desarrollo de Software (ADSO)
Nivel Técnico / Tecnólogo
Competencia Desarrollar la solución de software de acuerdo con el diseño y metodologías de desarrollo establecidas
Resultado Construir el aplicativo web con conexión a base de datos usando el patrón Modelo-Vista-Controlador (MVC)
Duración 56 horas (28 teoría + 28 práctica)
Tecnologías JDK 21Jakarta EE 10JSPJSTLPostgreSQLMavenTomcat 10.1DockerFastAPIReact
Metodología Aprendizaje basado en proyectos - Patrón MVC → API REST → SPA

Quiz Rápido

Verifica tu comprensión de la guía

Pregunta1/5
⏱️ Tiempo:00:000aciertos
¿Cuál es el resultado de aprendizaje esperado?
02

Presentación

Introducción al desarrollo web profesional con Java

¿Por qué el Patrón MVC?

El patrón Modelo-Vista-Controlador es el estándar de facto en el desarrollo web profesional. Separar tu aplicación en estas tres capas te permite:

  • Mantenibilidad:Cambiar la interfaz no afecta la lógica de negocio
  • Testabilidad:Cada componente se prueba independientemente
  • Trabajo en equipo:Diferentes desarrolladores en capas distintas
  • Reutilización:Los modelos se reutilizan fácilmente

¿Por qué JDK 21 y Jakarta EE?

JDK 21 es la versión LTS más reciente, que incluye características revolucionarias:

  • Records:Clases inmutables con sintaxis concisa
  • Pattern Matching:Código más expresivo y seguro
  • Virtual Threads:Concurrencia masiva eficiente
  • Switch Expressions:Sintaxis moderna con flechas

Jakarta EE (sucesor de Java EE) es el estándar para aplicaciones empresariales, usandojakarta.servlet.*en lugar del antiguojavax.servlet.*.

¿Por qué Docker y Coolify?

El despliegue moderno requiere conocimientos de contenedores:

  • Portabilidad:Funciona igual en cualquier entorno
  • Reproducibilidad:Mismas dependencias siempre
  • Escalabilidad:Fácil de escalar horizontalmente
  • DevOps:Integración continua simplificada

Coolify es una plataforma autohospedada que facilita el despliegue profesional.

Arquitectura MVC

Modelo

Datos y lógica de negocio

Records + DAO

Controlador

Procesa peticiones HTTP

Servlets

Vista

Interfaz de usuario

JSP + JSTL

Objetivos de Aprendizaje

Comprender MVC

Entender el patrón Modelo-Vista-Controlador y su aplicación en aplicaciones web

Dominar Jakarta EE

Desarrollar Servlets y JSPs con Jakarta EE 10 y JDK 21

Implementar CRUD

Crear operaciones completas de base de datos con JDBC y PostgreSQL

Desplegar con Docker

Contenerizar y desplegar aplicaciones en entornos de producción

03

3.1 Reflexión Inicial

El problema del código espagueti y por qué MVC lo soluciona

Psicología del Aprendizaje: Reconocer el Error es el Primer Paso

Este código espagueti no está aquí para burlarse de nadie — está aquí porque todos hemos escrito código así cuando empezamos. La ciencia del aprendizaje nos dice que reconocer patrones incorrectos es tan importante como aprender los correctos. Si al ver este código identificaste problemas de seguridad, mantenibilidad y organización, ya estás pensando como un desarrollador profesional. La diferencia entre un principiante y un experto no es que el experto nunca cometa errores — es que el experto ha desarrollado la capacidad de detectarlos y corregirlos. Sigue adelante, porque lo que viene a continuación (MVC, DAO, PreparedStatement) es exactamente lo que soluciona cada uno de estos problemas.

Caso de Estudio: El Código Espagueti

Imagina que eres el nuevo desarrollador en una empresa. Tu jefe te pide modificar una página JSP que muestra una lista de productos. Al abrir el archivo, encuentras algo así:

Código Problemático
<%@ page import="java.sql.*" %><%
String url = "jdbc:postgresql://localhost:5432/mi_db";
String user = "postgres";
String pass = "mi_password_secreta";
Connection conn = DriverManager.getConnection(url, user, pass);
String sql = "SELECT * FROM productos WHERE precio > " + request.getParameter("precio");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);%><html><body>
<table>
<% while(rs.next()) { %>
<tr>
<td><%= rs.getString("nombre") %></td>
<td><%= rs.getDouble("precio") %></td>
</tr>
<% } %>
</table>
<%
if (request.getParameter("accion") != null && request.getParameter("accion").equals("eliminar")) {
String id = request.getParameter("id");
conn.createStatement().executeUpdate("DELETE FROM productos WHERE id = " + id);
}
%></body></html>

Problemas de Seguridad

  • Contraseña hardcodeada en el código
  • Inyección SQL directa (concatenación)
  • Sin validación de entrada del usuario
  • Sin control de acceso ni autenticación

Problemas de Mantenibilidad

  • HTML y Java mezclados en el mismo archivo
  • Lógica de negocio, acceso a datos y presentación juntos
  • Imposible de probar unitariamente
  • Difícil de entender y modificar

Problemas de Colaboración

  • Dos desarrolladores no pueden trabajar en el mismo archivo
  • Cambiar la base de datos requiere modificar vistas
  • Cambiar el diseño requiere tocar lógica de negocio
  • No hay separación de responsabilidades

Preguntas de Reflexión

1

¿Puedes identificar al menos 3 problemas de seguridad en este código?

2

¿Qué pasaría si necesitas cambiar la base de datos de PostgreSQL a MySQL? ¿Cuántos archivos tendrías que modificar?

3

¿Cómo harías para probar la lógica de negocio sin tener un servidor web funcionando?

4

Si dos desarrolladores necesitan trabajar en este archivo al mismo tiempo, ¿qué problemas podrían surgir?

5

¿Por qué el patrón MVC soluciona todos estos problemas?

Has dado el primer paso: reconocer el problema

Identificar qué está mal en el código espagueti es la habilidad más importante que puedes desarrollar. En la siguiente sección aprenderás los fundamentos teóricos de MVC, JSP, Jakarta EE y JDBC — las herramientas que convierten el caos en código limpio y mantenible. Continúa, que lo mejor está por venir.

04

3.2 Fundamentos Teóricos

Conceptos clave antes de programar — MVC, JSP, Jakarta EE, JDBC, Records, CDI

Cómo usar esta sección: aprendizaje por capas (scaffolding)

Esta sección tiene 6 tabs. No necesitas dominarlos todos hoy. La psicología del aprendizaje recomienda el scaffolding (andamiaje): empieza por MVC y JSP (los conceptos más generales), luego profundiza en Jakarta EE y JDBC, y finalmente explora CDI y Records cuando tengas contextos. Vuelve a estos tabs cuando los necesites — no hay presión de aprender todo de una vez. Cada tab es un nivel de profundidad distinto; entra hasta donde te sientas cómodo y regresa cuando tengas más experiencia.

El Patrón Modelo-Vista-Controlador

MVC es un patrón de arquitectura de software que separa la aplicación en tres componentes interconectados, permitiendo el desarrollo en paralelo y el mantenimiento independiente de cada capa:

Modelo

Representa los datos y la lógica de negocio. Contiene las entidades (Records) y las clases DAO para acceso a datos. Es la capa que desconoce la interfaz de usuario.

Producto.java, ProductoDAO.java

Vista

La interfaz de usuario. Muestra los datos y captura la entrada del usuario. Solo renderiza, no contiene lógica de negocio. Implementada con JSP + JSTL.

lista.jsp, formulario.jsp

Controlador

Recibe las peticiones HTTP, interpreta los parámetros, invoca al modelo y selecciona la vista. Es el orquestador del flujo. Implementado con Servlets.

ProductoServlet.java

Flujo de una Petición MVC (Crear Producto)

1

Usuario llena el formulario y hace clic en "Guardar"

2

POST /productos con accion=guardar → ProductoServlet.doPost()

3

Servlet extrae parámetros (codigo, nombre, precio) del request

4

Servlet invoca ProductoDAO.insertar(producto) → PreparedStatement

5

DAO ejecuta INSERT en PostgreSQL y retorna resultado

6

Servlet redirige (sendRedirect) a GET /productos para listar

Model 1 vs Model 2 (MVC)

Jakarta EE soporta dos enfoques históricos. El estándar moderno es Model 2 (MVC puro):

Aspecto Model 1 (Antiguo) Model 2 (MVC Moderno)
Flujo JSP → JavaBean (todo en JSP) Servlet → DAO → JSP (separado)
Responsabilidad JSP hace todo: lógica + presentación Servlet controla, DAO accede datos, JSP solo muestra
Mantenibilidad ❌ Código espagueti (scriptlets mezclados) ✅ Capas separadas, fácil de mantener
Esta guía usa ✅ Model 2 (MVC puro)

Front Controller Pattern (Opcional)

En aplicaciones grandes, un Front Controller (único Servlet) recibe todas las peticiones y las distribuye a acciones específicas. Frameworks como Spring MVC y Struts lo usan. En esta guía cada Servlet maneja su propio recurso (enfoque directo).

// Enfoque directo (esta guía): cada recurso tiene su Servlet
@WebServlet("/productos")   → ProductoServlet.java
@WebServlet("/usuarios")    → UsuarioServlet.java
@WebServlet("/login")       → LoginServlet.java

// Front Controller (Spring MVC): un Servlet que distribuye
@WebServlet("/*")           → DispatcherServlet.java
// → usa un router para llamar al controlador adecuado

JavaServer Pages (JSP)

JSP es una tecnología que permite crear páginas web dinámicas usando Java. Con Jakarta EE 10, JSP 3.1 ofrece un modelo de vistas limpio, sin scriptlets Java, usando Expression Language (EL) y JSTL (JSP Standard Tag Library).

Ciclo de Vida de una JSP

Cuando un usuario solicita una JSP por primera vez, el servidor realiza estos pasos automáticamente:

1

Traducción: JSP → Servlet (.java)

2

Compilación: Servlet.java → .class

3

Carga: ClassLoader carga el Servlet

4

Ejecución: _jspService() genera HTML

Las siguientes veces solo se ejecuta el paso 4 (a menos que la JSP cambie). Esto significa que la primera carga es más lenta — pero las siguientes son rápidas.

Características Principales de JSP 3.1

Expression Language (EL) 5.0

Sintaxis ${producto.nombre} para acceder a datos sin scriptlets. Soporta navegación de propiedades, operadores aritméticos/lógicos y colecciones.

JSTL (Core, Format, SQL, XML, Functions)

Etiquetas como <c:forEach>, <c:if>, <c:choose>, <fmt:formatNumber> para lógica sin Java. 5 bibliotecas estándar.

Directivas: page, taglib, include

<%@ page %> configura la página (contentType, isErrorPage, session). <%@ taglib %> importa bibliotecas. <%@ include %> incluye fragmentos en tiempo de compilación.

9 Objetos Implícitos

request, response, session, application, pageContext, out, config, page, exception — disponibles en EL sin declararlos.

Directivas JSP Explicadas

Directiva Uso Ejemplo
<%@ page %> Configura la página (contentType, sesión, buffer, errorPage) <%@ page contentType="text/html" session="false" %>
<%@ taglib %> Importa bibliotecas de etiquetas (JSTL, personalizadas) <%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ include %> Incluye contenido en tiempo de compilación (estático) <%@ include file="header.jsp" %>
<c:import> Incluye en tiempo de ejecución (dinámico, desde URL externa) <c:import url="http://api.com/data" />

Página de Error Global (isErrorPage)

JSP permite manejar errores de forma declarativa sin try/catch en cada Servlet:

error.jsp — Página de error global
<%@ page isErrorPage="true" %>
<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body>
    <h1>Ha ocurrido un error</h1>
    <p>Mensaje: <c:out value="${pageContext.exception.message}"/></p>
    <p>Status: ${pageContext.errorData.statusCode}</p>
</body>
</html>
<%-- En web.xml: --%>
<!--
<error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/error.jsp</location>
</error-page>
<error-page>
    <error-code>404</error-code>
    <location>/404.jsp</location>
</error-page>
-->

Ejemplo Completo: JSP con JSTL (Sin Scriptlets)

productos/lista.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Lista de Productos</title>
</head>
<body>
    <table>
        <thead>
            <tr>
                <th>Código</th>
                <th>Nombre</th>
                <th>Precio</th>
                <th>Stock</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
            <c:forEach var="producto" items="${productos}">
                <tr>
                    <td><c:out value="${producto.codigo}" /></td>
                    <td><c:out value="${producto.nombre}" /></td>
                    <td>$<fmt:formatNumber value="${producto.precio}" type="currency" /></td>
                    <td><c:out value="${producto.stock}" /></td>
                    <td>
                        <a href="productos?accion=editar&id=${producto.id}">Editar</a>
                        <a href="productos?accion=eliminar&id=${producto.id}" onclick="return confirm('¿Eliminar?')">Eliminar</a>
                    </td>
                </tr>
            </c:forEach>
        </tbody>
    </table>
    <c:if test="${empty productos}">
        <p>No hay productos registrados.</p>
    </c:if>
</body>
</html>

Jakarta EE 10 — El Estándar Empresarial

Jakarta EE es la evolución de Java EE, mantenido por la Eclipse Foundation desde 2017. La migración de Java EE a Jakarta EE implica cambiar el prefijo de los paquetes de javax.* a jakarta.*:

Java EE (Antiguo)

import javax.servlet.*;
import javax.servlet.http.*;
import javax.persistence.*;
import javax.validation.*;

Jakarta EE (Moderno)

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.persistence.*;
import jakarta.validation.*;

Especificaciones Clave de Jakarta EE 10

Especificación Versión Propósito Anotación clave
Servlet 6.0 Manejo de peticiones HTTP @WebServlet
JSP 3.1 Páginas dinámicas del lado del servidor <%@ page %>
JSTL 3.0 Biblioteca de etiquetas estándar <c:forEach>
JPA 3.1 Persistencia objeto-relacional (ORM) @Entity
CDI 4.0 Inyección de dependencias y contextos @Inject
Bean Validation 3.0 Validación de datos con anotaciones @NotBlank
REST (JAX-RS) 3.1 APIs REST con Java @Path
WebSocket 2.1 Comunicación bidireccional en tiempo real @ServerEndpoint
JSON-B 3.0 Binding JSON ↔ Java objects @JsonbProperty
Security 3.0 Autenticación y autorización @RolesAllowed

Bean Validation — Validación Declarativa

En lugar de validar campos manualmente en cada Servlet (if nombre==null, if precio<0...), Jakarta EE ofrece validación con anotaciones en las entidades:

Validación con Bean Validation
import jakarta.validation.constraints.*;

public class ProductoDTO {

    @NotBlank(message = "El código es obligatorio")
    @Size(min = 3, max = 50)
    private String codigo;

    @NotBlank
    @Size(min = 2, max = 100)
    private String nombre;

    @NotNull
    @DecimalMin(value = "0.01", message = "Precio debe ser > 0")
    @DecimalMax(value = "999999.99")
    private BigDecimal precio;

    @Min(0)
    private int stock;

    @Email(message = "Email inválido")
    private String emailProveedor;

    @Past
    private LocalDate fechaIngreso;
}

// En el Servlet:
// ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
// Set<ConstraintViolation<ProductoDTO>> violations = validator.validate(dto);

JSON-B — Binding JSON en Jakarta EE

Jakarta EE incluye soporte nativo para convertir Java Objects ↔ JSON sin librerías externas:

JSON-B — Serialización
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.annotation.JsonbProperty;

public class Producto {
    @JsonbProperty("codigo_producto")
    private String codigo;
    private String nombre;
    @JsonbProperty("precio_venta")
    private BigDecimal precio;
    @JsonbProperty(nillable = true)
    private String descripcion;
}

// Uso:
Jsonb jsonb = JsonbBuilder.create();

// Java → JSON
String json = jsonb.toJson(producto);
// {"codigo_producto":"P1","nombre":"Laptop","precio_venta":2500000}

// JSON → Java
Producto p = jsonb.fromJson(json, Producto.class);

CDI — Contexts and Dependency Injection

CDI (Jakarta Contexts and Dependency Injection) es el estándar de Jakarta EE para inyección de dependencias. Permite que el contenedor gestione las dependencias entre objetos, eliminando la creación manual con new.

¿Por qué necesitamos CDI?

❌ Sin CDI (Acoplamiento rígido)
public class ProductoServlet {
    // Creación manual: acoplado a la implementación
    private ProductoDAO dao = new ProductoDAO();
    // Si cambia ProductoDAO, hay que cambiar
    // TODOS los Servlets que lo usan
    // Dificultad: testear con mock
}
✅ Con CDI (Desacoplado)
public class ProductoServlet {
    // Jakarta EE inyecta la dependencia automáticamente
    @Inject
    private ProductoDAO dao;
    // Tomcat/WildFly crea el DAO y lo asigna
    // Fácil de testear con mocks
    // Si cambia la implementación, no tocas el Servlet
}

Anotaciones Clave de CDI

Anotación Propósito Ejemplo
@Inject Inyecta una dependencia @Inject private ProductoDAO dao;
@Named Da un nombre al bean para usarlo en EL @Named("productoCtrl")
@ApplicationScoped Una instancia para toda la aplicación @ApplicationScoped // Singleton
@RequestScoped Una instancia por petición HTTP @RequestScoped // Por request
@SessionScoped Una instancia por sesión de usuario @SessionScoped // Carrito
@Produces Factory method para objetos no gestionados @Produces EntityManager em;

Scopes (Ámbitos) en CDI

El scope define cuándo se crea y destruye un bean:

Scope de instancia única
  • @ApplicationScoped — 1 instancia para toda la app (Singleton). Ideal para configuración, pool de conexiones.
  • @SessionScoped — 1 instancia por sesión de usuario. Ideal para carrito de compras, usuario logueado.
Scope de instancia temporal
  • @RequestScoped — 1 instancia por petición HTTP. Ideal para controladores, DTOs.
  • @Dependent — (default) 1 instancia por cada inyección. No comparte.
  • @ConversationScoped — Multi-request controlado, ideal para wizards.

Inyección con Cualificadores (@Qualifier)

Cuando hay múltiples implementaciones de una misma interfaz, usas cualificadores personalizados:

@Qualifier — Múltiples implementaciones
// 1. Definir cualificadores
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER})
public @interface TipoDAO {
    Tipo value();
    enum Tipo { PRODUCTO, USUARIO }
}

// 2. Implementaciones con cualificador
@TipoDAO(Tipo.PRODUCTO)
public class ProductoDAO implements DAO { ... }

@TipoDAO(Tipo.USUARIO)
public class UsuarioDAO implements DAO { ... }

// 3. Inyección con cualificador
@Inject @TipoDAO(Tipo.PRODUCTO)
private DAO productoDAO;

CDI necesita un contenedor Jakarta EE

CDI funciona automáticamente en servidores Jakarta EE completos (WildFly, Payara, GlassFish, Tomcat con weld-servlet). En Tomcat puro, CDI no está disponible por defecto — necesitas agregar Weld (implementación de CDI) como dependencia. En esta guía usamos inyección manual (new DAO) para mantener la simplicidad.

JDBC — Java Database Connectivity

JDBC es la API estándar de Java para conectarse a bases de datos relacionales. En esta guía usamos PostgreSQL con JDBC 42.7. JDBC es la capa más baja de acceso a datos — JPA/Hibernate construye sobre ella.

Arquitectura JDBC

App Java

Servlet / DAO

JDBC API

Connection, Statement, ResultSet

Driver

org.postgresql.Driver

PostgreSQL

Base de datos

DataSource vs DriverManager

JDBC ofrece dos formas de obtener conexiones. Siempre prefiere DataSource (con pool de conexiones) sobre DriverManager:

Aspecto DriverManager DataSource + Pool
Conexión Nueva conexión física cada vez (lento) Reutiliza conexiones del pool (rápido)
Configuración URL, usuario, password en cada llamada Centralizada en DataSource (JNDI o HikariCP)
Pool No tiene pool HikariCP, DBCP2, Tomcat CP
Rendimiento ❌ Lento (conexión física cada vez) ✅ Rápido (reutiliza conexiones)
Producción ❌ No recomendado ✅ Estándar de la industria
ConexionDB.java — Con HikariCP Pool
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class ConexionDB {
    private static final HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost:5432/inventario_db");
        config.setUsername("postgres");
        config.setPassword(System.getenv("DB_PASSWORD"));
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(5);
        config.setIdleTimeout(300000);
        config.setConnectionTimeout(10000);
        config.setPoolName("HikariPool-Inventario");
        dataSource = new HikariDataSource(config);
    }

    public static Connection conectar() throws SQLException {
        return dataSource.getConnection();
        // No cierres el DataSource — solo la Connection
    }
}

// Uso en DAO:
// try (Connection conn = ConexionDB.conectar()) {
//     ... // la conexión vuelve al pool automáticamente
// }

Patrón DAO (Data Access Object)

El patrón DAO separa la lógica de acceso a datos del resto de la aplicación. Cada entidad tiene su propio DAO con métodos CRUD:

ProductoDAO.java — CRUD completo
public class ProductoDAO {
    private static final String SQL_INSERT =
        "INSERT INTO productos (codigo, nombre, precio, stock) VALUES (?, ?, ?, ?)";
    private static final String SQL_SELECT_BY_ID =
        "SELECT * FROM productos WHERE id = ?";
    private static final String SQL_SELECT_ALL =
        "SELECT * FROM productos ORDER BY id";
    private static final String SQL_UPDATE =
        "UPDATE productos SET codigo=?, nombre=?, precio=?, stock=? WHERE id=?";
    private static final String SQL_DELETE =
        "DELETE FROM productos WHERE id = ?";

    public boolean insertar(Producto p) {
        try (Connection c = ConexionDB.conectar();
             PreparedStatement ps = c.prepareStatement(SQL_INSERT)) {
            ps.setString(1, p.codigo());
            ps.setString(2, p.nombre());
            ps.setBigDecimal(3, p.precio());
            ps.setInt(4, p.stock());
            return ps.executeUpdate() > 0;
        } catch (SQLException e) {
            Logger.getLogger(getClass().getName())
                .log(Level.SEVERE, "Error al insertar", e);
            return false;
        }
    }

    public Producto buscarPorId(int id) {
        String sql = "SELECT * FROM productos WHERE id = ?";
        try (Connection c = ConexionDB.conectar();
             PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return new Producto(
                        rs.getInt("id"),
                        rs.getString("codigo"),
                        rs.getString("nombre"),
                        rs.getBigDecimal("precio"),
                        rs.getInt("stock")
                    );
                }
            }
        } catch (SQLException e) { e.printStackTrace(); }
        return null;
    }

    public List<Producto> listarTodos() { /* similar */ }
    public boolean actualizar(Producto p) { /* similar */ }
    public boolean eliminar(int id) { /* similar */ }
}

Niveles de Aislamiento (Transaction Isolation)

Controlan cómo las transacciones ven los cambios de otras transacciones concurrentes:

Nivel Dirty Read Non-repeatable Phantom Performance
READ_UNCOMMITTED ❌ Posible ❌ Posible ❌ Posible Máxima
READ_COMMITTED ✅ Evita ❌ Posible ❌ Posible Alta
REPEATABLE_READ ✅ Evita ✅ Evita ❌ Posible Media
SERIALIZABLE ✅ Evita ✅ Evita ✅ Evita Baja

PostgreSQL usa READ_COMMITTED por defecto. Para cambiarlo: conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

Batch Processing (Procesamiento por Lotes)

Cuando necesitas insertar/actualizar miles de registros, el batch processing es órdenes de magnitud más rápido que ejecutar cada sentencia individualmente:

Batch Insert — 10,000 productos
// Batch: 100x más rápido que insertar uno por uno
public void insertarBatch(List<Producto> productos) {
    String sql = "INSERT INTO productos (codigo, nombre, precio, stock) VALUES (?, ?, ?, ?)";
    try (Connection c = ConexionDB.conectar();
         PreparedStatement ps = c.prepareStatement(sql)) {

        c.setAutoCommit(false);
        int count = 0;

        for (Producto p : productos) {
            ps.setString(1, p.codigo());
            ps.setString(2, p.nombre());
            ps.setBigDecimal(3, p.precio());
            ps.setInt(4, p.stock());
            ps.addBatch();
            count++;

            if (count % 1000 == 0) {
                ps.executeBatch();
                c.commit();
            }
        }
        ps.executeBatch(); // último lote
        c.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw new RuntimeException("Batch falló", e);
    }
}

Prevención de Inyección SQL

NUNCA uses concatenación de strings para valores SQL

// ¡PELIGROSO! SQL Injection
// Si el usuario ingresa: 1; DROP TABLE productos;--
String sql = "SELECT * FROM productos WHERE id = " + id;
Statement stmt = conn.createStatement();

// SEGURO — PreparedStatement con parámetros
// PostgreSQL escapa los valores automáticamente
String sql = "SELECT * FROM productos WHERE id = ? AND nombre LIKE ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, id);
ps.setString(2, "%" + busqueda + "%");
// El driver escapa los caracteres especiales de busqueda

Tipos de Sentencias JDBC

Interfaz Uso SQL Injection Performance
Statement SQL sin parámetros (DDL, COUNT) ❌ Vulnerable Menor (no cachea plan)
PreparedStatement SQL con parámetros (CRUD, WHERE) ✅ Seguro Mayor (cachea plan)
CallableStatement Procedimientos almacenados ✅ Seguro Alta

Records de JDK 21

Los records son una característica de JDK 14+ (estable desde JDK 16) que proporciona una sintaxis concisa para crear clases inmutables. Son ideales para DTOs, modelos, y objetos de transferencia de datos.

Clase Tradicional vs Record

Clase Tradicional (50+ líneas)

public final class Producto {
    private final int id;
    private final String codigo;
    private final String nombre;
    private final BigDecimal precio;

    // Constructor
    public Producto(int id, String codigo,
                    String nombre, BigDecimal precio) {
        this.id = id;
        this.codigo = codigo;
        this.nombre = nombre;
        this.precio = precio;
    }

    // Getters
    public int id() { return id; }
    public String codigo() { return codigo; }
    public String nombre() { return nombre; }
    public BigDecimal precio() { return precio; }

    // equals() y hashCode()
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Producto)) return false;
        Producto p = (Producto) o;
        return id == p.id
            && codigo.equals(p.codigo)
            && nombre.equals(p.nombre)
            && precio.equals(p.precio);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, codigo, nombre, precio);
    }

    @Override
    public String toString() {
        return "Producto{" + "id=" + id + ", ...";
    }
}

Record JDK 21 (10 líneas)

public record Producto(
    int id,
    String codigo,
    String nombre,
    BigDecimal precio
) {
    // Compact constructor con validación
    public Producto {
        if (precio != null && precio.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Precio negativo");
    }

    // Constructor alternativo (sin ID = nuevo)
    public Producto(String codigo, String nombre,
                    BigDecimal precio) {
        this(0, codigo, nombre, precio);
    }
}

// Genera automáticamente:
// - Canonical constructor
// - Getters: id(), codigo(), nombre(), precio()
// - equals(), hashCode(), toString()
// - Componentes (para pattern matching)

Validación en Compact Constructor

Los records permiten validar datos en el compact constructor (sin parámetros, solo cuerpo):

Compact Constructor con validaciones
public record Usuario(
    int id,
    String username,
    String email,
    String rol
) {
    // Compact constructor: validaciones automáticas
    public Usuario {
        // Validaciones (se ejecutan antes de asignar)
        if (username == null || username.isBlank())
            throw new IllegalArgumentException("Username requerido");
        if (email != null && !email.contains("@"))
            throw new IllegalArgumentException("Email inválido");
        if (rol != null && !Set.of("Admin", "Cliente", "Invitado").contains(rol))
            throw new IllegalArgumentException("Rol inválido: " + rol);

        // Normalización automática
        username = username.toLowerCase().trim();
        if (email != null) email = email.toLowerCase().trim();
    }
}

// Uso:
var u = new Usuario(1, "Admin", "admin@sena.edu.co", "Admin");
// Si username es "  ADMIN  " → se guarda como "admin"
// Si email es "ADMIN@SENA.EDU.CO" → se guarda como "admin@sena.edu.co"

Records con Pattern Matching

Los records se integran perfectamente con Pattern Matching de JDK 21. Puedes desestructurar un record en un switch:

Pattern Matching con Records
public record Producto(int id, String nombre, BigDecimal precio) {}

// Pattern Matching en switch (JDK 21)
String descripcion = switch (producto) {
    case Producto(int id, String nom, BigDecimal p)
        when p.compareTo(new BigDecimal("1000000")) > 0
            -> nom + " (Producto Premium #" + id + ")";
    case Producto(int id, String nom, _)
        -> nom + " (Producto Regular #" + id + ")";
};

// Pattern Matching con instanceof
if (obj instanceof Producto(int id, String nombre, BigDecimal precio)) {
    System.out.println("Producto: " + nombre + " - $" + precio);
}

// Records en streams
productos.stream()
    .filter(p -> p.precio().compareTo(new BigDecimal("100000")) > 0)
    .map(Producto::nombre)
    .forEach(System.out::println);

Records como DTOs en Capas

Los records son excelentes para transferir datos entre capas sin acoplarse a entidades JPA:

Records como DTOs
// En el DAO, mapeas a un Record DTO
public record ProductoDTO(
    int id,
    String codigo,
    String nombre,
    BigDecimal precio,
    String categoriaNombre  // de un JOIN con categorias
) {}

// El DAO retorna ProductoDTO en lugar de la entidad JPA
public List<ProductoDTO> listarConCategoria() {
    String sql = "SELECT p.id, p.codigo, p.nombre, p.precio, c.nombre as cat " +
                 "FROM productos p JOIN categorias c ON p.categoria_id = c.id";
    try (var ps = conn.prepareStatement(sql);
         var rs = ps.executeQuery()) {
        List<ProductoDTO> resultado = new ArrayList<>();
        while (rs.next()) {
            resultado.add(new ProductoDTO(
                rs.getInt("id"),
                rs.getString("codigo"),
                rs.getString("nombre"),
                rs.getBigDecimal("precio"),
                rs.getString("cat")
            ));
        }
        return resultado;
    }
}

// Beneficio: ProductoDTO es inmutable, thread-safe,
// y contiene exactamente los datos que necesita la vista

Records vs Clases Tradicionales — Cuándo usar cada uno

Escenario Usa Record Usa Clase Tradicional
DTO / VO (Transfer Object) ✅ Ideal (inmutable, conciso) ❌ Overkill
Entidad JPA (@Entity) ❌ No (JPA requiere mutabilidad) ✅ Requiere setters
Embeddable JPA (@Embeddable) ✅ Ideal (inmutable, value object) ❌ Verboso
Objeto con lógica de negocio ❌ Limitado (no hay herencia) ✅ Métodos complejos
Clave en Map/Set ✅ equals/hashCode automático ❌ Hay que implementar manual
Multi-hilo / concurrencia ✅ Thread-safe por diseño ❌ Sincronización manual
JSON serialización ✅ Jackson/Gson soportan ✅ También, más boilerplate

Recomendación: Records para Modelo + DAO

En esta guía usamos Records para el modelo (Producto.java, Usuario.java, Rol.java) porque son inmutables, concisos y perfectos para representar datos. Los DAOs retornan Records, los Servlets los reciben, y las vistas (JSP) los leen. Este patrón se llama DTO inmutable y es el estándar moderno.

Ventajas Clave de los Records

Inmutabilidad: Los datos no pueden cambiar después de creados — evita bugs por estado mutable
Concisión: 1 línea vs 50+ líneas — menos código, menos errores, más legible
Seguridad: Thread-safe por diseño — no necesitas sincronización
Compact Constructor: Validación y normalización automática al construir
Pattern Matching: Desestructuración directa en switch e instanceof
equals/hashCode: Basados en componentes — funcionan correctamente en collections
05

Configuración de PostgreSQL

Conecta tu aplicación a la base de datos existente

Requisito previo

Esta sección asume que ya tienes PostgreSQL instalado y la base de datosinventario_dbcreada. Si no es así, ejecuta primero el script SQL completo que viene con la guía.

1

Verificar que la BD existe

psql — Terminal
# Conectar a PostgreSQL
psql -U postgres
# Listar bases de datos
\l
# Deberías ver: inventario_db
# Conectar a la BD
\c inventario_db
# Verificar tablas
\dt
# Deberías ver: roles, usuarios, productos
2

Configurar credenciales en db.properties

Edita el archivosrc/main/resources/db.propertiescon tus credenciales reales:

db.properties
# Configuración de conexión a PostgreSQL
db.driver=org.postgresql.Driver
db.url=jdbc:postgresql://localhost:5432/inventario_db
db.username=postgres
db.password=tu_password_aqui

# Nota: Cambia 'tu_password_aqui' por tu contraseña real de PostgreSQL
# Si PostgreSQL corre en otro puerto, cambia 5432 por el puerto correcto
3

Entendiendo la URL JDBC

jdbc:postgresql://Protocolo JDBC para PostgreSQL
localhost:5432Host y puerto (cambia si tu BD está en otro servidor)
/inventario_dbNombre de la base de datos (debe existir)

Puerto compartido: usa 5434 si es la BD central

Si estás usando el PostgreSQL compartido del ecosistema DevBrain, el puerto es 5434 en lugar de 5432. La URL quedaría: jdbc:postgresql://localhost:5434/inventario_db.

4

Probar la conexión

Crea una clase de prueba para verificar que todo funciona:

TestConexion.java
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

public class TestConexion {
    public static void main(String[] args) {
        try (Connection conn = ConexionDB.conectar();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM productos")) {
            if (rs.next()) {
                System.out.println("✓ Conexión exitosa!");
                System.out.println("Productos en BD: " + rs.getInt(1));
            }
        } catch (Exception e) {
            System.err.println("✗ Error: " + e.getMessage());
        }
    }
}
5

Variables de entorno (Producción)

Para producción, usa variables de entorno en lugar de hardcodear credenciales:

PowerShell — Variables de entorno
# Configurar variables de entorno
$env:DB_HOST="localhost"
$env:DB_PORT="5432"
$env:DB_NAME="inventario_db"
$env:DB_USER="postgres"
$env:DB_PASSWORD="tu_password_seguro"

# En Docker o Coolify, configura estas variables en el panel

Script SQL de la base de datos

Descarga el script completo con las tablas roles, usuarios y productos, incluyendo datos de prueba. Ejecútalo en PostgreSQL para tener la base de datos lista.

Descargar inventario_db.sql (2 KB)

¡Base de datos configurada!

Acabas de conectar tu aplicación Java con PostgreSQL. Esto puede parecer un paso técnico menor, pero es uno de los momentos más importantes en el desarrollo de cualquier aplicación: el momento en que tu código habla con la base de datos. Has configurado la conexión JDBC, entendido la URL de conexión, y probado que todo funciona. En la siguiente sección verás cómo esta conexión se convierte en un superpoder cuando usamos un ORM como JPA/Hibernate.

Solución de Problemas Comunes

Error: "Connection refused"

Causa:PostgreSQL no está corriendo o el puerto es incorrecto.

Solución:Verifica que PostgreSQL esté activo:netstat -an | findstr 5432

Error: "database \"inventario_db\" does not exist"

Causa:La base de datos no existe.

Solución:EjecutaCREATE DATABASE inventario_db;en psql

Error: "password authentication failed"

Causa:Credenciales incorrectas en db.properties.

Solución:Verifica usuario y contraseña en el archivo de configuración

06

ORM: JPA/Hibernate vs JDBC

Del SQL manual a la persistencia con objetos — todas las comparaciones

¿Qué es un ORM?

Un ORM (Object-Relational Mapping) mapea automáticamente objetos Java a tablas de base de datos, eliminando la necesidad de escribir SQL manual. JPA (Jakarta Persistence) es el estándar, Hibernate es la implementación más popular. Es el equivalente a SQLAlchemy en Flask/Python.

JDBC Puro

Entidad (Record)

public record Producto(
    int id,
    String codigo,
    String nombre,
    BigDecimal precio,
    int stock
) {}

DAO con SQL manual

public class ProductoDAO {
    private static final String SQL_INSERT =
        "INSERT INTO (codigo, nombre, " +
        "precio, stock) VALUES (?,?,?,?)";

    public boolean insertar(Producto p) {
        try (Connection c = ConexionDB.conectar();
             PreparedStatement ps =
                 c.prepareStatement(SQL_INSERT)) {
            ps.setString(1, p.codigo());
            ps.setString(2, p.nombre());
            ps.setBigDecimal(3, p.precio());
            ps.setInt(4, p.stock());
            return ps.executeUpdate() > 0;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Mapeo manual ResultSet → Objeto

public Producto buscarPorId(int id) {
    String sql = "SELECT * FROM productos WHERE id=?";
    try (var ps = conn.prepareStatement(sql)) {
        ps.setInt(1, id);
        try (var rs = ps.executeQuery()) {
            if (rs.next()) {
                return new Producto(
                    rs.getInt("id"),
                    rs.getString("codigo"),
                    rs.getString("nombre"),
                    rs.getBigDecimal("precio"),
                    rs.getInt("stock")
                );
            }
            return null;
        }
    }
}
VS

JPA/Hibernate

Entidad (@Entity)

@Entity
@Table(name = "productos")
public class Producto {
    @Id @GeneratedValue
    private int id;
    @Column(nullable = false, unique = true)
    private String codigo;
    @Column(nullable = false)
    private String nombre;
    @Column(precision = 10, scale = 2)
    private BigDecimal precio;
    private int stock;

    // Getters y setters (obligatorios)
}

Repository (sin SQL manual)

@Repository
public class ProductoRepository {
    @PersistenceContext
    private EntityManager em;

    public void insertar(Producto p) {
        em.persist(p);
    }

    public Producto buscar(int id) {
        return em.find(Producto.class, id);
    }

    public List<Producto> listar() {
        return em.createQuery(
            "SELECT p FROM Producto p",
            Producto.class).getResultList();
    }
}

Sin mapeo manual — automático

// JPA mapea automáticamente:
// productos.id → Producto.id (int)
// productos.codigo → Producto.codigo (String)
// productos.precio → Producto.precio (BigDecimal)
// Sin ResultSet, sin getInt/getString manual

Comparación Detallada: JDBC vs JPA/Hibernate

Dimensión JDBC Puro JPA / Hibernate
Código boilerplate Alto: try/catch/finally, PreparedStatement, ResultSet Mínimo: persist(), find(), createQuery()
Mapeo Objeto-Relacional Manual: rs.getInt() → setter, columna por columna Automático via @Entity, @Column, @Id
SQL generado Escrito a mano (control total) Generado automáticamente (configurable)
Relaciones (FK) JOIN manual + mapeo manual de objetos anidados @OneToMany, @ManyToOne — carga automática
Transacciones Manual: conn.setAutoCommit(false), commit(), rollback() @Transactional — propagación automática
Cache Ninguno (cada consulta va a la BD) 1er nivel (sesión) + 2do nivel (opcional, compartido)
Lazy Loading No aplica (cargas todo explícitamente) @OneToMany(fetch=LAZY) — carga bajo demanda
Optimización Control total del SQL, índices, planes de ejecución JPQL/HQL + Criteria API + query hints
Curva de aprendizaje Baja: solo SQL + JDBC API Media: anotaciones, JPQL, ciclo de vida, proxies
Portabilidad BD SQL específico del motor (cambia al migrar) Dialect cambia: Hibernate genera SQL óptimo para cada motor
Testing Fácil: SQL directo, sin contexto Requiere bootstrap de EntityManagerFactory
Performance Predecible: tú controlas cada query Variable: puede ser mejor (cache) o peor (N+1, lazy)
Proyectos ideales Pequeños, aprendizaje, consultas complejas, reporting Grandes, CRUD intensivo, relaciones complejas, equipo

Mapeo de Tipos: Java ↔ SQL ↔ JDBC

Tipo Java Tipo SQL (PostgreSQL) JDBC getter/setter Anotación JPA
int / Integer INTEGER rs.getInt() / ps.setInt() @Column
long / Long BIGINT rs.getLong() / ps.setLong() @Column
String VARCHAR(n) / TEXT rs.getString() / ps.setString() @Column(length=100)
BigDecimal NUMERIC(p,s) rs.getBigDecimal() / ps.setBigDecimal() @Column(precision=10, scale=2)
boolean / Boolean BOOLEAN rs.getBoolean() / ps.setBoolean() @Column
LocalDate DATE rs.getDate().toLocalDate() @Temporal(TemporalType.DATE)
LocalDateTime TIMESTAMP rs.getTimestamp().toLocalDateTime() @Temporal(TemporalType.TIMESTAMP)
byte[] BYTEA rs.getBytes() / ps.setBytes() @Lob
Enum VARCHAR o INTEGER Manual: toString/valueOf @Enumerated(EnumType.STRING)
UUID UUID rs.getObject()(UUID) @Column(columnDefinition = "UUID")

Relaciones (FK): La Gran Diferencia

El manejo de relaciones es donde JPA realmente brilla. Veamos la diferencia con un ejemplo concreto:

JDBC Manual

// Producto + Categoria: 2 queries + mapeo manual
String sqlProd = "SELECT p.*, c.nombre as cat_nom " +
    "FROM productos p " +
    "JOIN categorias c ON p.cat_id = c.id " +
    "WHERE p.id = ?";
try (var ps = conn.prepareStatement(sqlProd)) {
    ps.setInt(1, id);
    try (var rs = ps.executeQuery()) {
        if (rs.next()) {
            Categoria c = new Categoria(
                rs.getInt("cat_id"),
                rs.getString("cat_nom")
            );
            return new Producto(
                rs.getInt("id"),
                rs.getString("nombre"),
                c  // mapeo manual
            );
        }
    }
}
// Si hay lista: bucle manual para agrupar

JPA Automático

@Entity
@Table(name = "productos")
public class Producto {
    @Id @GeneratedValue
    private int id;
    private String nombre;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "cat_id")
    private Categoria categoria;
    // Categoria se carga automáticamente
    // cuando se accede a producto.getCategoria()
}

// Buscar: 1 línea, sin JOIN manual
Producto p = em.find(Producto.class, id);
// p.getCategoria().getNombre() → carga lazy

Anotaciones de Relaciones en JPA

Anotación Relación SQL Ejemplo Fetch por defecto
@OneToOne 1:1 (FK única) Usuario ↔ Perfil EAGER
@OneToMany 1:N (FK en hija) Categoria → Productos LAZY
@ManyToOne N:1 (FK en actual) Producto → Categoria EAGER
@ManyToMany N:N (tabla intermedia) Producto ↔ Proveedor LAZY

Manejo de Transacciones

JDBC Manual

Connection conn = null;
try {
    conn = ConexionDB.conectar();
    conn.setAutoCommit(false); // inicio

    dao1.insertar(a, conn);
    dao2.actualizar(b, conn);

    conn.commit(); // confirmar
} catch (SQLException e) {
    conn.rollback(); // deshacer
    throw e;
} finally {
    conn.close();
}
// Boilerplate: 15+ líneas por operación

JPA @Transactional

@Service
public class ProductoService {
    @Autowired
    private ProductoRepository repo;

    @Transactional
    public void transferirStock(
            int origen, int destino, int qty) {
        var p1 = repo.buscar(origen);
        var p2 = repo.buscar(destino);
        p1.setStock(p1.getStock() - qty);
        p2.setStock(p2.getStock() + qty);
        // 1 anotación = commit automático
        // Si falla: rollback automático
    }
}
// Sin try/catch/finally
// Sin commit/rollback manual

Propagación @Transactional

REQUIRED (default) — usa transacción existente o crea una nueva. REQUIRES_NEW — siempre crea una nueva (suspende la actual). MANDATORY — lanza error si no hay transacción activa.

Caching: JDBC vs JPA

Nivel JDBC JPA/Hibernate
1er nivel No existe Siempre activo (Session). Misma entidad dentro de la misma transacción no consulta BD dos veces
2do nivel No existe Opcional (Ehcache, Redis). Compartido entre sesiones. Configurable por entidad
Cache de queries No existe Opcional: cachea results de JPQL/HQL si la consulta no ha cambiado
Actualización Siempre datos frescos de la BD Automática: Hibernate detecta cambios y hace flush antes del commit

⚠️ El Problema N+1 (y cómo evitarlo)

El error más común al migrar de JDBC a JPA:

❌ N+1 Query (LENTO)
// 1 query para categorias
// + N queries para productos
// = 4 queries para 3 categorias!
List<Categoria> cats = em
  .createQuery("FROM Categoria", Categoria.class)
  .getResultList();
for (Categoria c : cats) {
  c.getProductos().size(); // ← query aquí!
}
✅ JOIN FETCH (RÁPIDO)
// 1 query con JOIN = 1 sola
// llamada a la BD
List<Categoria> cats = em
  .createQuery(
    "SELECT DISTINCT c FROM Categoria c " +
    "LEFT JOIN FETCH c.productos",
    Categoria.class)
  .getResultList();
for (Categoria c : cats) {
  c.getProductos().size(); // ya cargado
}

Solución JDBC: haces JOIN una vez y mapeas manualmente. Solución JPA: usa JOIN FETCH en JPQL, @EntityGraph o FetchType.EAGER con criterio.

Configuración de Hibernate (JPA)

Dependencias Maven (pom.xml)
<!-- Hibernate (implementación JPA) -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.3.0.Final</version>
</dependency>
<!-- Driver PostgreSQL -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.1</version>
</dependency>
<!-- Pool de conexiones -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>
</dependency>
+
persistence.xml (src/main/resources/META-INF/)
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
    version="3.0">
    <persistence-unit name="inventarioPU">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <!-- Conexión -->
            <property name="jakarta.persistence.jdbc.url"
                value="jdbc:postgresql://localhost:5432/inventario_db"/>
            <property name="jakarta.persistence.jdbc.user"
                value="postgres"/>
            <property name="jakarta.persistence.jdbc.password"
                value="sena_adso_2026"/>

            <!-- Pool HikariCP -->
            <property name="hibernate.hikari.connectionTimeout"
                value="20000"/>
            <property name="hibernate.hikari.maximumPoolSize"
                value="10"/>

            <!-- DDL automático -->
            <property name="jakarta.persistence.schema-generation"
                value="none"/>
            <!-- none | create | create-drop | update -->

            <!-- SQL log -->
            <property name="hibernate.show_sql"
                value="true"/>
            <property name="hibernate.format_sql"
                value="true"/>

            <!-- Dialect PostgreSQL -->
            <property name="hibernate.dialect"
                value="org.hibernate.dialect.PostgreSQLDialect"/>
        </properties>
    </persistence-unit>
</persistence>

Herramientas del Ecosistema

Necesidad JDBC Puro JPA/Hibernate
Pool de conexiones HikariCP, DBCP2 (configuración manual) HikariCP integrado (vía hibernate.hikari.*)
Migraciones BD Flyway, Liquibase (scripts SQL versionados) Flyway/Liquibase + hibernate.ddl-auto (dev)
Auditoría Manual: columnas fecha_creacion, trigger @CreatedDate, @LastModifiedDate (Spring Data)
Validación Manual en Servlet o DAO @NotBlank, @Size, @NotNull (Bean Validation)
Paginación LIMIT + OFFSET manual en SQL setFirstResult(), setMaxResults() en Query
Logging SQL System.out o Logger manual hibernate.show_sql=true (formateado automático)
DTO projections JOIN manual + constructor en Record JPQL SELECT new + constructor expression

JPQL vs SQL: El Mismo Lenguaje, Distinta Sintaxis

Operación SQL (JDBC) JPQL (JPA)
SELECT todos SELECT * FROM productos SELECT p FROM Producto p
WHERE con parámetro WHERE nombre LIKE ? WHERE p.nombre LIKE :nombre
JOIN con condición JOIN categorias c ON p.cat_id=c.id JOIN p.categoria c
INSERT INSERT INTO productos VALUES (?,?) em.persist(producto) (no hay INSERT JPQL)
UPDATE UPDATE productos SET nombre=? WHERE id=? UPDATE Producto p SET p.nombre=:nom WHERE p.id=:id
DELETE DELETE FROM productos WHERE id=? DELETE FROM Producto p WHERE p.id=:id
Paginación LIMIT 10 OFFSET 20 q.setFirstResult(20).setMaxResults(10)
Agregación SELECT COUNT(*), AVG(precio) FROM productos SELECT COUNT(p), AVG(p.precio) FROM Producto p
Native SQL Es SQL mismo em.createNativeQuery("SELECT * FROM productos")

Diferencia clave: Trabajas con objetos, no con tablas

En SQL seleccionas columnas (SELECT nombre FROM productos). En JPQL seleccionas objetos (SELECT p.nombre FROM Producto p). El alias p representa una instancia de la clase Producto, no una fila de la tabla.

Performance: Cuándo usar cada uno

✅ Gana JDBC

  • Consultas masivas de solo lectura (reportes)
  • Bulk INSERT/UPDATE/DELETE de miles de registros
  • Consultas con SQL complejo (CTEs, window functions)
  • Operaciones batch sin necesidad de objetos en memoria
  • Proyectos pequeños (< 10 tablas) donde JPA es overkill

✅ Gana JPA/Hibernate

  • CRUD con relaciones complejas (muchas FK)
  • Cache de 1er nivel evita consultas repetitivas
  • Dirty checking: solo actualiza columnas cambiadas
  • Aplicaciones con muchas transacciones pequeñas
  • Equipos grandes: menos código, menos errores

Analogía con Flask + SQLAlchemy

Flask (Python)
# Modelo SQLAlchemy
class Producto(db.Model):
    __tablename__ = "productos"
    id = db.Column(db.Integer, primary_key=True)
    codigo = db.Column(db.String(50), unique=True)
    nombre = db.Column(db.String(100))
    precio = db.Column(db.Numeric(10,2))

# Guardar
p = Producto(codigo="P1", nombre="Laptop")
db.session.add(p)
db.session.commit()

# Buscar
Producto.query.get(1)
Producto.query.filter_by(codigo="P1").first()
Equivalente en
Jakarta EE (Java)
@Entity
@Table(name = "productos")
public class Producto {
    @Id @GeneratedValue
    private int id;
    @Column(unique = true)
    private String codigo;
    private String nombre;
    @Column(precision = 10, scale = 2)
    private BigDecimal precio;
}

// Guardar
em.persist(producto);

// Buscar
em.find(Producto.class, 1);
em.createQuery("FROM Producto WHERE codigo=:c",
    Producto.class)
    .setParameter("c", "P1")
    .getSingleResult();
Concepto Flask/SQLAlchemy Jakarta EE/JPA
Modelo class Producto(db.Model) @Entity class Producto
Columna db.Column(db.String(50)) @Column(length=50) String
Insert db.session.add(obj) em.persist(obj)
Commit db.session.commit() @Transactional (automático)
Query by ID Modelo.query.get(id) em.find(Entidad.class, id)
Query filtrada .filter_by(codigo="X") WHERE p.codigo=:x
Relaciones db.relationship() @OneToMany, @ManyToOne
Migraciones flask db migrate Flyway: mvn flyway:migrate

Pros y Contras Detallados

JDBC Puro — Ventajas
  • Control total del SQL generado
  • Sin dependencias pesadas (solo driver JDBC)
  • Ideal para aprender cómo funciona la persistencia
  • Performance predecible y fácil de depurar
  • Sin sorpresas: lo que ves (SQL) es lo que obtienes
  • Funciona con cualquier versión de Java
JDBC Puro — Desventajas
  • 70% más código que JPA para el mismo CRUD
  • Mapeo manual propenso a errores (columnas mal escritas)
  • Sin cache: cada consulta viaja a la BD
  • Sin lazy loading: cargas todo explícitamente
  • Sin dirty checking: debes escribir UPDATE aunque solo cambie 1 campo
  • Difícil de mantener en proyectos con muchas relaciones
JPA/Hibernate — Ventajas
  • 70% menos código boilerplate
  • SQL generado automáticamente (consistente)
  • Cache de 1er nivel gratis (misma sesión)
  • Lazy loading: carga solo lo que necesitas
  • Dirty checking: UPDATE automático de columnas cambiadas
  • Portabilidad: cambia dialect y funciona con MySQL, Oracle, etc.
JPA/Hibernate — Desventajas
  • Curva de aprendizaje: anotaciones, ciclo de vida, proxies
  • Problema N+1: puede generar consultas ineficientes
  • Menos control del SQL generado (hay que tunear)
  • Dependencia pesada (Hibernate core ~7MB)
  • Lazy loading fuera de transacción → LazyInitializationException
  • Debugging más complejo: el error no está en tu código sino en Hibernate

Hoja de Ruta: Migración Gradual JDBC → JPA/Hibernate

No necesitas migrar todo de golpe. Esta guía te muestra cómo hacerlo de forma incremental:

Fase Qué hacer Duración Riesgo
1. Aprende JDBC Domina Connection, PreparedStatement, ResultSet, DAO pattern. Haz CRUD completo manual. 2-3 semanas Bajo
2. Entiende ORM Conceptos: mapeo objeto-relacional, session, transaction, cache. Compara con tu código JDBC. 1 semana Bajo
3. JPA básico Configura Hibernate en un proyecto nuevo. Migra 1 entidad simple (sin relaciones). 1 semana Medio
4. Relaciones Agrega @OneToMany, @ManyToOne. Compara JOIN manual vs automático. Detecta N+1. 1 semana Medio
5. JPA avanzado Cache 2do nivel, Criteria API, Entity Graphs, batch processing, locking optimista. 2 semanas Alto
6. Producción Monitorea SQL generado, ajusta fetch plans, usa Flyway para migraciones, profiling JPA. Continua Alto

Recomendación: No migres todo a la vez. Empieza con las entidades más simples y de bajo tráfico. Mantén JDBC para los reportes complejos y JPA para el CRUD transaccional.

Analogía Final: Cocinar vs Pedir Delivery

🍳 JDBC = Cocinar

Tú controlas cada ingrediente: eliges la verdura, la cortas tú mismo, controlas el fuego, sazonas al gusto. Sabes exactamente qué está pasando en cada paso. Pero si cocinas para 100 personas, es agotador.

📦 JPA = Delivery

Pides lo que quieres y llega. No ves la cocina, pero es eficiente para la mayoría de casos. Si quieres algo muy específico, puedes dar instrucciones especiales (queries nativas). Pero si el delivery se equivoca, es más difícil depurar.

Recomendación para el Aprendiz

Aprende primero JDBC puro (como en esta guía) para entender cómo funciona realmente la persistencia. Luego, cuando domines los conceptos, migra a JPA/Hibernate para proyectos más grandes. Esta base sólida te permitirá entender qué hace el ORM "bajo el capó" y evitar los errores clásicos (N+1, lazy sin transacción, etc.).

Comparación Visual: JDBC vs ORM

Comparación JDBC vs ORM
07

Simuladores Interactivos

Experimenta con la arquitectura MVC de forma visual

Ponte a prueba: el aprendizaje activo es más efectivo

Los estudios en psicología cognitiva muestran que el aprendizaje activo (hacer, experimentar, equivocarse) es significativamente más efectivo que la lectura pasiva. Estos simuladores están diseñados para que juegues con los conceptos que has aprendido: el flujo MVC, el CRUD, la inyección SQL, el ciclo de vida del Servlet y la comparación JDBC vs ORM. No tengas miedo de equivocarte — el simulador está aquí para que explores sin consecuencias. Cada error es una oportunidad de aprendizaje.

Diagrama de Arquitectura MVC Completo

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ │ │ VISTA │────→│ CONTROLADOR │────→│ MODELO │ │ (JSP+JSTL) │ │ (Servlet) │ │ (Record+DAO)│ │ │←────│ │←────│ │ └─────────────┘ └─────────────┘ └──────┬──────┘ │ ┌────▼──────┐ │ POSTGRESQL│ │ BD │ └───────────┘ Flujo: 1. El usuario interactúa con la Vista (JSP) 2. La Vista envía la petición al Controlador (Servlet) 3. El Controlador procesa e invoca al Modelo (DAO) 4. El Modelo accede a PostgreSQL y retorna datos 5. El Controlador selecciona la Vista y pasa los datos 6. La Vista renderiza el HTML y lo muestra al usuario

Flujo Completo de una Petición HTTP (CRUD Crear)

CLIENTE SERVIDOR TOMCAT POSTGRESQL │ │ │ │ POST /productos │ │ │ (codigo=P1, nombre=Laptop)│ │ │───────────────────────────→│ │ │ │ 1. ProductoServlet.doPost() │ │ │ 2. Extrae parámetros │ │ │ 3. Crea Producto Record │ │ │ 4. Invoca ProductoDAO │ │ │─────────────────────────────→│ │ │ 5. INSERT INTO productos │ │ │ (codigo,nombre,precio,...) │ │ │ VALUES (?,?,?,...) │ │ │←─────────────────────────────│ │ │ 6. Resultado: filas>0 │ │ │ │ │ Redirect GET /productos │ │ │←───────────────────────────│ │ │ │ │ │ GET /productos │ │ │───────────────────────────→│ │ │ │ 7. ProductoDAO.listarTodos()│ │ │─────────────────────────────→│ │ │ 8. SELECT * FROM productos │ │ │←─────────────────────────────│ │ HTML con tabla │ │ │←───────────────────────────│ │
08

3.3 Tutorial Práctico Paso a Paso

Construye el Sistema de Inventario desde cero

🧪

Primer Ejemplo: Hola Mundo con Servlet + JSP

Tu primer Servlet ejecutable — ¡en 5 minutos!

Objetivo: Ver "Hola Mundo" en el navegador

Crea, compila y ejecuta tu primer Servlet. Al final verás ¡Hola Mundo desde Jakarta EE 10! en http://localhost:8080/hola-mundo.

Si algo sale mal, es parte del proceso

Es normal que el primer Servlet no funcione al primer intento. Tal vez el puerto 8080 esté ocupado, la estructura de carpetas no coincida, o el WAR no se despliegue correctamente. Cada error es una lección. Los desarrolladores experimentados pasan más tiempo depurando que escribiendo código nuevo. Si te encuentras con un error, léelo con calma, busca en la sección de solución de problemas y recuerda: la perseverancia es la habilidad más importante en programación.

Paso 1: Crear la estructura del proyecto

Terminal — Crear proyecto
# Crea esta estructura de carpetas manualmente:
mkdir hola-mundo-servlet\src\main\java\com\sena\holamundo
mkdir hola-mundo-servlet\src\main\webapp\WEB-INF
mkdir hola-mundo-servlet\src\main\resources

Paso 2: Crear pom.xml (Maven)

hola-mundo-servlet/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.sena</groupId>
    <artifactId>hola-mundo</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

Paso 3: Crear el Servlet (Controlador)

src/main/java/com/sena/holamundo/HolaMundoServlet.java
package com.sena.holamundo;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class HolaMundoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
        resp.setContentType("text/html;
        charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html><head><title>Hola Mundo</title></head><body>");
        out.println("<h1>¡Hola Mundo desde Jakarta EE 10!</h1>");
        out.println("<p>Mi primer Servlet con JDK 21</p>");
        out.println("<p>Fecha: " + new java.util.Date() + "</p>");
        out.println("</body></html>");
    }
}

Paso 4: Compilar y generar el WAR

Terminal — Compilar con Maven
cd hola-mundo-servlet
mvn clean package
# Resultado esperado:
# [INFO] Building war: hola-mundo-servlet\target\hola-mundo-1.0.war
# [INFO] BUILD SUCCESS

Paso 5: Desplegar en Tomcat y probar

Terminal — Desplegar y probar
# 1. Copiar el WAR a Tomcat
copy target\hola-mundo-1.0.war C:\apache-tomcat-10.1.x\webapps\

# 2. Iniciar Tomcat (si no está corriendo)
C:\apache-tomcat-10.1.x\bin\startup.bat

# 3. Abrir en el navegador:
#    http://localhost:8080/hola-mundo-1.0/hola-mundo
#
# Resultado esperado:
# ┌──────────────────────────────────────────────┐
# │  ¡Hola Mundo desde Jakarta EE 10!            │
# │  Mi primer Servlet con JDK 21                │
# │  Fecha: Sun Jun 28 12:00:00 COT 2026         │
# └──────────────────────────────────────────────┘

¡Ya tienes tu primer Servlet funcionando!

Acabas de crear, compilar y ejecutar un Servlet Jakarta EE 10. Has visto el flujo completo: código → compilar → desplegar → navegador. Ahora que entiendes el mecanismo básico, continuemos con el proyecto completo del Sistema de Inventario.

🔗 Puente JSP → FastAPI: JWT desde Java

En el proyecto JSP encontrarásJwtUtil.javaque genera tokens JWT usando la mismaSECRET_KEYque FastAPI. Esto significa queReact puede autenticarse tanto contra JSP como contra FastAPIcon el mismo token. Es el puente entre el monolito y los microservicios:

// JwtUtil.java — genera el mismo JWT que FastAPI
String token = JwtUtil.generarToken("admin", "Administrador");
// → React usa este token con: Authorization: Bearer <token>

Proyecto JSP+MVC listo para ejecutar

Descarga el proyecto Maven completo con el Sistema de Inventario. Ya incluye todos los archivos: Modelo (Records JDK 21), DAO (JDBC), Controladores (Servlets Jakarta EE 10) y Vistas (JSP con JSTL).

Solo necesitas: mvn clean package cargo:run y ya tienes la app en http://localhost:8080/inventario-mvc-1.0/

Descargar proyecto JSP+MVC (32 KB)
1

Configuración del Entorno

Instalación de herramientas necesarias

Herramientas Requeridas

JDK 21Compilación y ejecución
Maven 3.9+Gestión de dependencias
Tomcat 10.1Servidor de aplicaciones
PostgreSQL 12+Base de datos

Verificar Instalaciones

Terminal — Verificación
# Verificar JDK 21
java --version
# Debe mostrar: java 21.x.x
# Verificar Maven
mvn --version
# Debe mostrar: Apache Maven 3.9.x
# Verificar PostgreSQL
psql --version
# Debe mostrar: psql (PostgreSQL) 12 o superior

Iniciar Tomcat

Windows — Iniciar Tomcat
# Navegar al directorio de Tomcat
cd C:\apache-tomcat-10.1.x\bin
# Iniciar servidor
startup.bat
# Verificar en: http://localhost:8080
2

Base de Datos PostgreSQL

Crear tablas y datos de prueba

Crear Base de Datos

SQL — Crear BD
-- Crear base de datos
CREATE DATABASE inventario_db;

-- Conectar a la base de datos
\c inventario_db

Script Completo de Tablas

El siguiente script crea las 3 tablas principales con datos de prueba:

recursos/sql/inventario_db.sql
-- Tabla de Roles
CREATE TABLE roles (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(50) NOT NULL UNIQUE,
    descripcion VARCHAR(200),
    estado BOOLEAN DEFAULT TRUE,
    fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insertar roles
INSERT INTO roles (nombre, descripcion) VALUES
    ('Administrador', 'Acceso completo al sistema'),
    ('Cliente', 'Acceso limitado para consultas'),
    ('Invitado', 'Acceso de solo lectura');

-- Tabla de Usuarios
CREATE TABLE usuarios (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    nombre_completo VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    rol_id INTEGER NOT NULL,
    CONSTRAINT fk_usuarios_roles FOREIGN KEY (rol_id)
        REFERENCES roles(id),
    estado BOOLEAN DEFAULT TRUE,
    fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insertar usuarios (password: admin123)
INSERT INTO usuarios (username, password, nombre_completo, email, rol_id) VALUES
    ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMye...', 'Administrador', 'admin@sena.edu.co', 1),
    ('cliente1', '$2a$10$N9qo8uLOickgx2ZMRZoMye...', 'Juan Pérez', 'juan@sena.edu.co', 2);

-- Tabla de Productos
CREATE TABLE productos (
    id SERIAL PRIMARY KEY,
    codigo VARCHAR(50) NOT NULL UNIQUE,
    nombre VARCHAR(100) NOT NULL,
    descripcion TEXT,
    precio DECIMAL(10,2) NOT NULL CHECK (precio >= 0),
    stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
    categoria VARCHAR(50),
    estado BOOLEAN DEFAULT TRUE,
    fecha_registro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insertar productos de ejemplo
INSERT INTO productos (codigo, nombre, descripcion, precio, stock, categoria) VALUES
    ('PROD-001', 'Laptop HP Pavilion', 'Laptop 15.6" Intel Core i5 8GB', 2500000.00, 15, 'Tecnología'),
    ('PROD-002', 'Mouse Logitech M185', 'Mouse inalámbrico', 45000.00, 50, 'Accesorios'),
    ('PROD-003', 'Teclado Mecánico Redragon', 'Teclado mecánico RGB', 180000.00, 30, 'Accesorios'),
    ('PROD-004', 'Monitor Samsung 24"', 'Monitor Full HD IPS', 650000.00, 20, 'Tecnología'),
    ('PROD-005', 'Audífonos Sony WH-1000XM4', 'Audífonos con cancelación de ruido', 1200000.00, 25, 'Audio');
3

Estructura del Proyecto Maven

Organización estándar de directorios

Estructura de Directorios

Estructura del Proyecto
inventario-mvc/
├── pom.xml
├── src/main/
│   ├── java/com/sena/inventario/
│   │   ├── modelo/
│   │   │   ├── Rol.java
│   │   │   ├── Usuario.java
│   │   │   └── Producto.java
│   │   ├── dao/
│   │   │   ├── ConexionDB.java
│   │   │   ├── RolDAO.java
│   │   │   ├── UsuarioDAO.java
│   │   │   └── ProductoDAO.java
│   │   ├── controlador/
│   │   │   ├── LoginServlet.java
│   │   │   ├── ProductoServlet.java
│   │   │   └── UsuarioServlet.java
│   │   └── util/
│   │       └── PasswordUtil.java
│   ├── resources/
│   │   └── db.properties
│   └── webapp/
│       ├── WEB-INF/
│       │   └── web.xml
│       ├── productos/
│       │   ├── lista.jsp
│       │   └── formulario.jsp
│       ├── usuarios/
│       │   ├── lista.jsp
│       │   └── formulario.jsp
│       ├── css/
│       │   └── estilo.css
│       ├── index.jsp
│       └── login.jsp
└── Dockerfile

Archivo pom.xml

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.sena.inventario</groupId>
    <artifactId>inventario-mvc</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- Jakarta Servlet API -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- Jakarta JSTL -->
        <dependency>
            <groupId>jakarta.servlet.jsp.jstl</groupId>
            <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
            <version>3.0.0</version>
        </dependency>
        <!-- PostgreSQL JDBC Driver -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.1</version>
        </dependency>
        <!-- BCrypt para contraseñas -->
        <dependency>
            <groupId>at.favre.lib</groupId>
            <artifactId>bcrypt</artifactId>
            <version>0.10.2</version>
        </dependency>
    </dependencies>
</project>
4

Modelo - Records de JDK 21

Entidades inmutables con sintaxis moderna

Producto.java - Record Principal

Producto.java
package com.sena.inventario.modelo;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public record Producto(
    int id,
    String codigo,
    String nombre,
    String descripcion,
    BigDecimal precio,
    int stock,
    String categoria,
    boolean estado,
    LocalDateTime fechaRegistro,
    LocalDateTime fechaActualizacion
) {
    public Producto(
        String codigo, String nombre, String descripcion,
        BigDecimal precio, int stock, String categoria
    ) {
        this(0, codigo, nombre, descripcion, precio, stock, categoria,
             true, LocalDateTime.now(), LocalDateTime.now());
    }
}
5

DAO - Acceso a Datos con JDBC

Patrón DAO con PreparedStatement

ProductoDAO.java - CRUD Completo

ProductoDAO.java
package com.sena.inventario.dao;

import com.sena.inventario.modelo.Producto;
import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class ProductoDAO {

    private static final String SQL_INSERT =
        "INSERT INTO productos (codigo, nombre, descripcion, precio, stock, categoria) " +
        "VALUES (?, ?, ?, ?, ?, ?)";

    private static final String SQL_SELECT_ALL =
        "SELECT * FROM productos ORDER BY id";

    private static final String SQL_SELECT_BY_ID =
        "SELECT * FROM productos WHERE id = ?";

    private static final String SQL_UPDATE =
        "UPDATE productos SET codigo=?, nombre=?, descripcion=?, precio=?, " +
        "stock=?, categoria=?, estado=? WHERE id=?";

    private static final String SQL_DELETE =
        "DELETE FROM productos WHERE id = ?";

    // ---------- CREATE ----------
    public boolean insertar(Producto producto) {
        try (Connection conn = ConexionDB.conectar();
             PreparedStatement ps = conn.prepareStatement(SQL_INSERT)) {
            ps.setString(1, producto.codigo());
            ps.setString(2, producto.nombre());
            ps.setString(3, producto.descripcion());
            ps.setBigDecimal(4, producto.precio());
            ps.setInt(5, producto.stock());
            ps.setString(6, producto.categoria());
            return ps.executeUpdate() > 0;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    // ---------- READ / LIST ----------
    public List<Producto> listarTodos() {
        List<Producto> productos = new ArrayList<>();
        try (Connection conn = ConexionDB.conectar();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(SQL_SELECT_ALL)) {
            while (rs.next()) {
                productos.add(new Producto(
                    rs.getInt("id"),
                    rs.getString("codigo"),
                    rs.getString("nombre"),
                    rs.getString("descripcion"),
                    rs.getBigDecimal("precio"),
                    rs.getInt("stock"),
                    rs.getString("categoria"),
                    rs.getBoolean("estado"),
                    rs.getTimestamp("fecha_registro").toLocalDateTime(),
                    rs.getTimestamp("fecha_actualizacion").toLocalDateTime()
                ));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return productos;
    }

    // ---------- UPDATE ----------
    public boolean actualizar(Producto producto) {
        try (Connection conn = ConexionDB.conectar();
             PreparedStatement ps = conn.prepareStatement(SQL_UPDATE)) {
            ps.setString(1, producto.codigo());
            ps.setString(2, producto.nombre());
            ps.setString(3, producto.descripcion());
            ps.setBigDecimal(4, producto.precio());
            ps.setInt(5, producto.stock());
            ps.setString(6, producto.categoria());
            ps.setBoolean(7, producto.estado());
            ps.setInt(8, producto.id());
            return ps.executeUpdate() > 0;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    // ---------- DELETE ----------
    public boolean eliminar(int id) {
        try (Connection conn = ConexionDB.conectar();
             PreparedStatement ps = conn.prepareStatement(SQL_DELETE)) {
            ps.setInt(1, id);
            return ps.executeUpdate() > 0;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }
}
6

Controlador - Servlets Jakarta EE

Manejo de peticiones HTTP con @WebServlet

ProductoServlet.java - CRUD Completo

ProductoServlet.java
package com.sena.inventario.controlador;

import com.sena.inventario.dao.ProductoDAO;
import com.sena.inventario.modelo.Producto;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;

@WebServlet("/productos")
public class ProductoServlet extends HttpServlet {

    private final ProductoDAO dao = new ProductoDAO();

    // --- GET: listar, nuevo, editar, eliminar ---
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        String accion = req.getParameter("accion");
        if (accion == null) accion = "listar";
        switch (accion) {
            case "nuevo" -> mostrarFormularioNuevo(req, res);
            case "editar" -> mostrarFormularioEditar(req, res);
            case "eliminar" -> eliminarProducto(req, res);
            default -> listarProductos(req, res);
        }
    }

    // --- POST: guardar producto ---
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        if ("guardar".equals(req.getParameter("accion")))
            guardarProducto(req, res);
    }

    // --- Listar todos los productos ---
    private void listarProductos(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        req.setAttribute("productos", dao.listarTodos());
        req.getRequestDispatcher("/productos/lista.jsp").forward(req, res);
    }

    // --- Mostrar formulario nuevo ---
    private void mostrarFormularioNuevo(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        req.setAttribute("modo", "crear");
        req.getRequestDispatcher("/productos/formulario.jsp").forward(req, res);
    }

    // --- Mostrar formulario editar ---
    private void mostrarFormularioEditar(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        int id = Integer.parseInt(req.getParameter("id"));
        Producto p = dao.buscarPorId(id);
        if (p == null) {
            req.setAttribute("error", "Producto no encontrado");
            listarProductos(req, res);
            return;
        }
        req.setAttribute("producto", p);
        req.setAttribute("modo", "editar");
        req.getRequestDispatcher("/productos/formulario.jsp").forward(req, res);
    }

    // --- Guardar (crear o actualizar) ---
    private void guardarProducto(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        String id = req.getParameter("id");
        String codigo = req.getParameter("codigo");
        String nombre = req.getParameter("nombre");
        String descripcion = req.getParameter("descripcion");
        BigDecimal precio = new BigDecimal(req.getParameter("precio"));
        int stock = Integer.parseInt(req.getParameter("stock"));
        String categoria = req.getParameter("categoria");

        if (id == null || id.isEmpty()) {
            dao.insertar(new Producto(
                codigo, nombre, descripcion, precio, stock, categoria));
        } else {
            boolean estado = "on".equals(req.getParameter("estado"));
            dao.actualizar(new Producto(
                Integer.parseInt(id), codigo, nombre, descripcion,
                precio, stock, categoria, estado, LocalDateTime.now(),
                LocalDateTime.now()));
        }
        res.sendRedirect(req.getContextPath() + "/productos");
    }

    // --- Eliminar producto por ID ---
    private void eliminarProducto(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        dao.eliminar(Integer.parseInt(req.getParameter("id")));
        res.sendRedirect(req.getContextPath() + "/productos");
    }
}
7

Vista - JSP con JSTL

Páginas dinámicas sin código Java

lista.jsp - Tabla de Productos con JSTL

productos/lista.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Lista de Productos</title>
</head>
<body>
    <table>
        <thead>
            <tr>
                <th>Código</th>
                <th>Nombre</th>
                <th>Precio</th>
                <th>Stock</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
            <c:forEach var="producto" items="${productos}">
                <tr>
                    <td><c:out value="${producto.codigo}" /></td>
                    <td><c:out value="${producto.nombre}" /></td>
                    <td>$<c:out value="${producto.precio}" /></td>
                    <td><c:out value="${producto.stock}" /></td>
                    <td>
                        <a href="productos?accion=editar&id=${producto.id}">Editar</a>
                        <a href="productos?accion=eliminar&id=${producto.id}">Eliminar</a>
                    </td>
                </tr>
            </c:forEach>
        </tbody>
    </table>
</body>
</html>
8

Compilación y Despliegue

Arrancar con Maven Cargo o Script Automatizado

🚀 Método Recomendado: Ejecución Automática (Cero Configuración)

Hemos configurado el pluginCargoenpom.xmlpara que descargue y configure Tomcat 10 automáticamente. También cuenta con un script interactivo de PowerShell en la raíz que se encarga de verificar y preparar PostgreSQL y arrancar el servidor en un solo clic.

Opción A: Ejecutar usando el script interactivo (Recomendado)

PowerShell — Iniciar Todo
# Ejecute el script en la raíz del proyecto
# para automatizar puertos, base de datos y Tomcat:
.\run-project-jsp.ps1

Opción B: Arrancar Tomcat manualmente vía Maven Cargo

Terminal — Maven Cargo
# 1. Ingrese a la carpeta del código de ejemplo
cd recursos/codigo-ejemplo
# 2. Compile e inicie el servidor Tomcat incrustado
mvn clean package cargo:run
# 3. Abra su navegador en: http://localhost:8080/inventario-mvc-1.0/

Opción C: Despliegue Tradicional (Copia de WAR Manual)

Si prefiere utilizar una instalación externa de Apache Tomcat de forma manual:

Terminal — Despliegue Manual
# 1. Compile y empaquete el archivo WAR
cd recursos/codigo-ejemplo
mvn clean package

# 2. Copie el archivo WAR a la carpeta webapps de Tomcat
copy target\inventario-mvc-1.0.war C:\apache-tomcat-10.1.x\webapps\

# 3. Reinicie el servidor Tomcat
C:\apache-tomcat-10.1.x\bin\shutdown.bat
C:\apache-tomcat-10.1.x\bin\startup.bat
09

3.4 Ejercicios de Transferencia

Ejercicios retadores para demostrar tu dominio

1
Intermedio

Buscador de Productos

Modifica el sistema para agregar un buscador de productos por nombre y rango de precios.

Requisitos:

  • Agregar campo de búsqueda en lista.jsp
  • Crear métodobuscarPorNombre(String)en ProductoDAO
  • UsarLIKEen SQL para búsqueda parcial
  • Agregar acción "buscar" en ProductoServlet
  • Mostrar resultados filtrados con contador
  • Bonus:Búsqueda por rango de precios

Pista:UsaWHERE nombre LIKE ?conps.setString(1, "%" + nombre + "%")

2
Avanzado

Tabla Categorías con Relación 1:N

Crea una nueva entidad Categoria y enlázala con Productos mediante una relación uno a muchos.

Requisitos:

  • Crear tablacategoriasen PostgreSQL
  • Crear recordCategoria.java
  • CrearCategoriaDAO.javacon CRUD completo
  • Modificar tabla productos: agregarcategoria_idFK
  • CrearCategoriaServlet.javacon vistas JSP
  • Modificar formulario de productos con<select>de categorías
  • Bonus:Eliminación en cascada o restricción

Pista:UsaINNER JOINen el DAO para obtener productos con nombre de categoría

3
Experto

Sistema de Auditoría con Filtros

Implementa un sistema de auditoría que registre todas las operaciones CRUD con filtros avanzados.

Requisitos:

  • Crear tablaauditoria(tabla, operacion, usuario, fecha, datos)
  • Crear recordAuditoria.javay su DAO
  • Implementar Jakarta Servlet Filter para registrar peticiones
  • Modificar métodos DAO para registrar en auditoría
  • Crear vista con historial y filtros por fecha/tipo
  • Bonus:Paginación (10 registros por página)

Pista:UsaLIMITyOFFSETen SQL para paginación

De Monolito (JSP) a Microservicios (FastAPI + React)

Hasta ahora has construido un monolito JSP: todo en un solo WAR (lógica, vistas, auth con sesiones). Ahora evolucionamos a una arquitectura de microservicios donde cada componente es independiente, escalable y desplegable por separado. La base de datos y las reglas de negocio se mantienen — lo que cambia es cómo se comunican las piezas.

Comparación Detallada: Monolito vs Microservicios

Dimensión 🏢 Monolito JSP 🔗 Microservicios FastAPI + React
Empaquetado 1 solo archivo WAR desplegado en Tomcat N contenedores Docker independientes
Comunicación Llamadas directas entre clases (Java methods) HTTP/REST entre servicios (JSON sobre la red)
Estado de sesión Stateful: HttpSession en memoria del servidor Stateless: JWT en localStorage del cliente
Frontend Server-side: JSP + JSTL renderiza HTML en Tomcat Client-side: React renderiza componentes en el navegador
API No hay API explícita (Servlet + JSP directo) API REST documentada con Swagger/OpenAPI
Base de datos Compartida: todos los DAOs apuntan a la misma BD Por servicio: cada microservicio puede tener su propia BD
Escalabilidad Vertical: escalas todo el WAR o nada Horizontal: escalas solo el servicio con demanda
Despliegue Reemplazar WAR completo → reiniciar Tomcat Actualizar solo el contenedor del servicio afectado
Tecnología Java obligatorio para todo el stack Cada servicio elige su tecnología óptima
Testing Manual (abrir navegador, probar CRUD) Automatizado: pytest para API, Vitest para React
Equipo Equipo full-stack Java (perfil único) Equipos especializados: backend, frontend, DevOps
Tiempo de inicio 30-60s (iniciar Tomcat + cargar WAR) 2-5s por servicio (contenedor ligero)

Mapa de Equivalencias: Concepto JSP → Microservicio

Concepto JSP + MVC Equivalente en Microservicios Lo que cambia
Producto.java (Record) schemas/producto.py (Pydantic) Java → Python, validación automática
ProductoDAO.java models/producto.py (SQLAlchemy) JDBC manual → ORM automático
ProductoServlet.java routers/producto.py (FastAPI) @WebServlet → @router.get(), JSON response
lista.jsp (JSP + JSTL) components/ProductoLista.jsx Render en servidor → render en navegador
LoginServlet.java routers/auth.py (JWT) HttpSession stateful → JWT stateless
ConexionDB.java database.py + get_db() try-with-resources → context manager
db.properties .env + pydantic-settings Archivo fijo → variables de entorno
pom.xml requirements.txt + pyproject.toml XML → texto plano
Tomcat + WAR Uvicorn + contenedor Docker Servidor pesado → proceso ligero
Flyway / SQL scripts Alembic Java → Python, autogenerate

Estrategia de Migración: Strangler Fig Pattern

No migres todo de golpe. Usa el Patron Strangler Fig (Higuera Estranguladora): reemplaza el monolito pieza por pieza mientras ambas versiones coexisten.

1
Identificar

Encuentra el módulo con menos dependencias (ej. Catálogo de productos)

2
Extraer

Crea el microservicio con su propia API y BD. Ambos sistemas apuntan a la misma BD inicialmente

3
Redirigir

El frontend (React) consume el nuevo microservicio. El monolito deja de recibir tráfico para ese módulo

4
Remover

Elimina el código antiguo del monolito. Repite con el siguiente módulo hasta que el monolito desaparezca

Lo que SE MANTIENE igual
  • PostgreSQL — misma BD, mismas tablas, mismos datos
  • Reglas de negocio — validaciones, cálculos, lógica de inventario
  • Modelo de datos — roles, usuarios, productos (PK/FK iguales)
  • JWT Secret Key — misma clave firmada, mismo token portable
  • Frontend React — puede consumir TANTO el JSP como FastAPI
Lo que CAMBIA
  • Comunicación: métodos Java internos → HTTP/REST
  • Estado: HttpSession en servidor → JWT en cliente
  • Vistas: JSP renderiza en servidor → React renderiza en navegador
  • API: implícita (Servlet forward) → explícita (Swagger docs)
  • Despliegue: 1 WAR → N contenedores Docker
  • Escalamiento: vertical (más RAM a Tomcat) → horizontal (más réplicas del servicio)

Evolución de la Arquitectura: 3 Fases

┌──────────────────────────────────────────────────────────────────────┐ │ 🏢 FASE 1: MONOLITO JSP (todo en un WAR, todo en Java) │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ TOMCAT (1 WAR) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ Login │ │Productos │ │ Usuarios │ │ │ │ │ │ Servlet │←→│ Servlet │←→│ Servlet │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ │ │ ┌────▼──────────────▼─────────────▼────┐ │ │ │ │ │ DAOs (JDBC manual) │ │ │ │ │ └────────────────┬─────────────────────┘ │ │ │ │ │ │ │ │ │ ┌────▼────┐ │ │ │ │ │PostgreSQL│ │ │ │ │ └─────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ 🔗 FASE 2: COEXISTENCIA (Strangler Fig - migración gradual) │ │ ┌──────────┐ ┌────────────────────┐ ┌──────────────────────┐ │ │ │ │ │ M O N O L I T O │ │ M I C R O S E R V. │ │ │ │ React │ │ (sigue corriendo │ │ (nuevo: un módulo │ │ │ │ SPA │←→│ para módulos no │←→│ extraído, ej. │ │ │ │ :5173 │ │ migrados aún) │ │ Productos API) │ │ │ └──────────┘ └────────┬───────────┘ └──────────┬───────────┘ │ │ │ │ │ │ ┌────▼─────────────────────────▼────┐ │ │ │ PostgreSQL (COMPARTIDA) │ │ │ └──────────────────────────────────┘ │ │ │ │ 🚀 FASE 3: MICROSERVICIOS PUROS (cada servicio independiente) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ React │ │ Auth │ │ Product │ │ Usuario │ │ │ │ SPA │←→│ Service │←→│ Service │←→│ Service │ │ │ │ :5173 │ │ :8001 │ │ :8002 │ │ :8003 │ │ │ └──────────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ ┌────▼─────────────▼─────────────▼────┐ │ │ │ PostgreSQL o BD por servicio │ │ │ └────────────────────────────────────┘ │ │ │ │ Beneficios: Escalabilidad individual · Aislamiento de fallos │ │ Tecnología óptima por servicio · Despliegue │ │ independiente · Equipos autónomos │ └──────────────────────────────────────────────────────────────────────┘

¿Por qué esta guía enseña ambos enfoques?

El monolito JSP te enseña los fundamentos: cómo funciona realmente el HTTP, las sesiones, el MVC, el JDBC. El stack moderno FastAPI + React te muestra la evolución natural: APIs REST, JWT, SPA, contenedores. No son enfoques rivales — el primero es la base conceptual del segundo. Los ingenieros que entienden el monolito construyen mejores microservicios porque saben qué problema resuelve cada abstracción.

10

API REST con FastAPI + SQLAlchemy + JWT

Construye el backend moderno en Python — CRUD con 3 tablas relacionadas (PK/FK), autenticación JWT y control de roles

Transición desde JSP + MVC

Así como construiste el backend con Jakarta EE (Servlets + DAO + JSP), ahora lo harás con FastAPI: losModelos Pydanticreemplazan los Records,SQLAlchemyreemplaza los DAOs, y losRoutersreemplazan los Servlets. La diferencia principal: FastAPI retorna JSON en lugar de renderizar JSP, y React (en la siguiente guía) consumirá ese JSON.

🧪

Primer Ejemplo: Hola Mundo con FastAPI

Tu primer endpoint API REST — ¡en 2 minutos!

Objetivo: Ver JSON en el navegador

Crea tu primera API REST con FastAPI. Al final verás{"mensaje": "Hola Mundo desde FastAPI!"}en http://localhost:8000.Nota:Así como el Servlet "Hola Mundo" fue tu primer ejemplo en Java, este es tu primer endpoint en Python.

Paso 1: Crear el proyecto

Terminal — Crear y configurar
# 1. Crear carpeta del proyecto
mkdir hola-mundo-fastapi
cd hola-mundo-fastapi
# 2. Crear y activar entorno virtual
python -m venv venv
venv\Scripts\activate
# Windows
# source venv/bin/activate  # Linux/Mac
# 3. Instalar FastAPI + servidor
pip install fastapi uvicorn
# Resultado:
# Successfully installed fastapi-0.115.0 uvicorn-0.31.0

Paso 2: Crear main.py (Equivalente al Servlet "Hola Mundo")

hola-mundo-fastapi/main.py
from fastapi import FastAPI

# Creamos la aplicación (como @WebServlet)
app = FastAPI(title="Mi Primera API", version="1.0.0")


@app.get("/")
async def root():
    """GET / → Retorna JSON (como doGet pero sin HTML)"""
    return {
        "mensaje": "Hola Mundo desde FastAPI!",
        "framework": "FastAPI + Python",
        "equivalente": "Servlet Hola Mundo en Jakarta EE"
    }


@app.get("/health")
async def health():
    """GET / → Verificar que funciona"""
    return {"status": "ok", "version": "1.0.0"}


@app.get("/sumar")
async def sumar(a: int = 5, b: int = 3):
    """GET /sumar?a=10&b=20 → JSON con resultado"""
    resultado = a + b
    return {
        "operacion": f"{a} + {b}",
        "resultado": resultado
    }

Paso 3: Ejecutar y probar

🐳 Con Docker:
Terminal — Docker
# Construir y ejecutar con Docker
docker build -t hola-fastapi .
docker run -d -p 8000:8000 --name hola-fastapi hola-fastapi
# Ver logs
docker logs hola-fastapi
# Probar
curl http://localhost:8000
# Detener
docker stop hola-fastapi && docker rm hola-fastapi
🪟 Nativo Windows:
Terminal — Nativo
# Activar entorno virtual (si no lo está)
venv\Scripts\activate
# Iniciar FastAPI (recarga automática con --reload)
uvicorn main:app --reload --port 8000
# Resultado esperado:
# INFO: Uvicorn running on http://127.0.0.1:8000
# INFO: Application startup complete.

Paso 4: Probar los endpoints

Navegador / curl — Probar API
# Abrir en el navegador o usar curl:
# 1. Endpoint raíz
curl http://localhost:8000
# Resultado: {"mensaje":"Hola Mundo desde FastAPI!","framework":"FastAPI + Python","equivalente":"Servlet Hola Mundo en Jakarta EE"}
# 2. Health check
curl http://localhost:8000/health
# Resultado: {"status":"ok","version":"1.0.0"}
# 3. Sumar con parámetros
curl "http://localhost:8000/sumar?a=10&b=20"
# Resultado: {"operacion":"10 + 20","resultado":30}
# 4. Documentación Swagger automática
# Abrir en el navegador: http://localhost:8000/docs
# Verás una UI interactiva donde puedes probar los endpoints
# ┌────────────────────────────────────────────┐
# │ Swagger UI                                 │
# │ ──────────────────────────                 │
# │ GET /          Root                        │
# │ GET /health    Health Check                │
# │ GET /sumar     Sumar dos números           │
# │                                            │
# │ [Try it out] → Ejecuta desde el navegador  │
# └────────────────────────────────────────────┘
# 5. Documentación ReDoc alternativa
# Abrir: http://localhost:8000/redoc

¡Ya tienes tu primera API REST funcionando!

Así como tu primer Servlet retornaba HTML, FastAPI retorna JSON. La diferencia clave: el JSON puede ser consumido por cualquier cliente (React, móvil, otros servicios), mientras que el HTML solo sirve para navegadores. Has hecho la transición de server-side rendering a API-first — un salto conceptual enorme. Ahora que entiendes el mecanismo, construyamos la API completa de inventario con base de datos.

Proyecto FastAPI listo para ejecutar

Descarga el proyecto FastAPI completo con: Modelos SQLAlchemy, Schemas Pydantic, Routers (CRUD + Auth JWT) y Migraciones Alembic. Ya incluye requirements.txt y .env.

Solo necesitas: pip install -r requirements.txt && uvicorn main:app --reload --port 8000

Descargar proyecto FastAPI (39 KB)
1

Configuración del Proyecto FastAPI

Estructura y dependencias

Estructura de directorios

Estructura del Proyecto FastAPI
fastapi-inventario/
├── main.py                  # Punto de entrada (como index.jsp)
├── config.py                # Config con pydantic-settings (lee .env)
├── database.py              # Conexión SQLAlchemy (como ConexionDB.java)
├── alembic.ini              # Migraciones (como Flyway en Java)
├── migrations/
│   ├── env.py               # Config de Alembic
│   ├── script.py.mako       # Template para nuevas migraciones
│   └── versions/            # Migraciones generadas
├── .env / .env.example      # Variables de entorno (DB, JWT, CORS)
├── tests/
│   └── test_api.py          # 8 tests: health, auth, CRUD, paginación
├── models/
│   ├── producto.py          # Producto — FK → usuario_creador_id
│   ├── rol.py               # Rol — PK referenciada por Usuario
│   └── usuario.py           # Usuario — FK → roles.id
├── schemas/
│   ├── producto.py          # Schema request/response + PaginatedResponse
│   └── usuario.py           # Schema + Rol + Login + Token
├── routers/
│   ├── producto.py          # CRUD productos con paginación (JWT)
│   ├── auth.py              # Login/Register + JWT + verificación de roles
│   ├── usuario.py           # CRUD usuarios (solo Admin)
│   └── rol.py               # CRUD roles (Admin)
└── requirements.txt         # Dependencias (como pom.xml)

Migraciones de base de datos (Alembic)

Terminal — Migraciones
# Equivalente a Flyway/Liquibase en Java
# pero nativo de Python con Alembic

# 1. Crear migración automática (detecta cambios en modelos/)
alembic revision --autogenerate -m "agregar tabla categorias"

# 2. Aplicar migraciones pendientes
alembic upgrade head

# 3. Revertir última migración
alembic downgrade -1

# 4. Ver historial
alembic history

🗺️ JSP (Flyway) vs FastAPI (Alembic)

En JSP se usabaFlywayo scripts SQL manuales para versionar la BD. En FastAPI usamosAlembicque funciona igual: detecta cambios en los modelos Python y genera automáticamente los scripts SQL de migración. Ambos permitenupgrade(avanzar) ydowngrade(retrocedir).

Configuración con variables de entorno

.env — Configuración centralizada
# ===== .env (copiar de .env.example) =====
# PostgreSQL
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/inventario_db
# JWT
SECRET_KEY=mi-clave-secreta-segura-2026
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60
# CORS
CORS_ORIGINS=http://localhost:5173,http://localhost:3000

Buenas prácticas: .env vs configuración hardcodeada

En JSP usábamosdb.propertiescon valores fijos. Aquí usamospydantic-settingsque lee de.envo variables de entorno del sistema. Esto permite:

  • Seguridad:contraseñas y claves fuera del código
  • Portabilidad:mismo código, distinta configuración por entorno
  • Docker-ready:las variables se pasan al contenedor con-e

FastAPI en Docker:docker run -e DATABASE_URL=... -e SECRET_KEY=... mi-api

Tabla de relaciones (PK/FK)

Tabla PK FK Relación
roles id (SERIAL) Tabla padre
usuarios id (SERIAL) rol_id → roles.id Muchos usuarios → 1 rol
productos id (SERIAL) usuario_creador_id → usuarios.id Muchos productos → 1 usuario creador

Autenticación JWT — de sesiones a tokens

Mientras que en JSP usábamosHttpSessionpara mantener la sesión del usuario, aquí usamosJWT (JSON Web Tokens). El usuario envía su usuario/contraseña, el servidor valida y retorna un token firmado. El frontend guarda ese token y lo envía en cada petición vía headerAuthorization: Bearer <token>. Esto permite que la API seastateless(sin sesiones en servidor), escalable y preparada para microservicios.

Archivo requirements.txt

requirements.txt
fastapi==0.115.0
uvicorn==0.31.0
sqlalchemy==2.0.35
psycopg2-binary==2.9.9
pydantic==2.9.0
pydantic-settings==2.5.0      # Config con variables de entorno
python-jose[cryptography]==3.3.0  # JWT
passlib[bcrypt]==1.7.4         # Hash de contraseñas
alembic==1.13.0                # Migraciones de BD
httpx==0.27.0                  # Testing HTTP
pytest==8.3.0                  # Testing unitario
Terminal — Instalación
# Crear y activar entorno virtual
python -m venv venv
venv\Scripts\activate
# Windows
# source venv/bin/activate  # Linux/Mac
# Instalar dependencias
pip install -r requirements.txt
# Resultado:
# Successfully installed fastapi-0.115.0 uvicorn-0.31.0
# sqlalchemy-2.0.35 psycopg2-binary-2.9.9 pydantic-2.9.0
2

Modelo SQLAlchemy + Esquema Pydantic

Equivalente a los Records de JDK 21

Modelo de Base de Datos (SQLAlchemy)

models/producto.py
from sqlalchemy import Column, Integer, String, Numeric, Boolean, DateTime, text
from database import Base


class ProductoDB(Base):
    """Modelo SQLAlchemy — mapea la tabla 'productos' de PostgreSQL.
    Equivalente al Record Producto.java en JSP MVC."""
    __tablename__ = "productos"

    id = Column(Integer, primary_key=True, index=True)
    codigo = Column(String(50), unique=True, nullable=False)
    nombre = Column(String(100), nullable=False)
    descripcion = Column(String(500), nullable=True)
    precio = Column(Numeric(10, 2), nullable=False)
    stock = Column(Integer, default=0)
    categoria = Column(String(50), nullable=True)
    estado = Column(Boolean, default=True)
    fecha_registro = Column(DateTime, server_default=text("CURRENT_TIMESTAMP"))

Esquema Pydantic (Validación)

schemas/producto.py
from pydantic import BaseModel, Field
from decimal import Decimal
from datetime import datetime
from typing import Optional


class ProductoBase(BaseModel):
    """Esquema base — validación de tipos automática.
    Equivalente a los parámetros del Servlet."""
    codigo: str = Field(..., min_length=3, max_length=50)
    nombre: str = Field(..., min_length=2, max_length=100)
    descripcion: Optional[str] = None
    precio: Decimal = Field(..., gt=0, decimal_places=2)
    stock: int = Field(default=0, ge=0)
    categoria: Optional[str] = None


class ProductoCreate(ProductoBase):
    """Para crear: hereda de ProductoBase (sin id)"""
    pass


class Producto(ProductoBase):
    """Para responder: incluye id y metadatos"""
    id: int
    estado: bool = True
    fecha_registro: Optional[datetime] = None

    class Config:
        from_attributes = True  # Permite crear desde SQLAlchemy

Conexión a Base de Datos

database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase

# URL de conexión — como db.properties en JSP
# Formato: postgresql://usuario:password@host:puerto/bd
DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/inventario_db"

engine = create_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


class Base(DeclarativeBase):
    """Clase base para modelos SQLAlchemy"""
    pass


def get_db():
    """Dependencia que provee una sesión de BD por petición.
    Como ConexionDB.conectar() pero automático."""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
3

CRUD Endpoints — Productos

Equivalente a ProductoServlet.java

Router de Productos (GET, POST, PUT, DELETE)

routers/producto.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from database import get_db
from models.producto import ProductoDB
from schemas.producto import Producto, ProductoCreate

# Prefijo /api/productos — como @WebServlet("/productos")
router = APIRouter(prefix="/api/productos", tags=["Productos"])


@router.get("/", response_model=List[Producto])
async def listar_productos(
    categoria: str | None = None,
    db: Session = Depends(get_db)
):
    """GET /api/productos — Listar todos (con filtro opcional).
    Equivalente a ProductoDAO.listarTodos()"""
    query = db.query(ProductoDB)
    if categoria:
        query = query.filter(ProductoDB.categoria == categoria)
    productos = query.all()
    return productos
    # SQLAlchemy → Pydantic automático


@router.get("/{producto_id}", response_model=Producto)
async def obtener_producto(producto_id: int, db: Session = Depends(get_db)):
    """GET /api/productos/5 — Obtener por ID.
    Equivalente a ProductoDAO.buscarPorId()"""
    producto = db.query(ProductoDB).filter(ProductoDB.id == producto_id).first()
    if not producto:
        raise HTTPException(status_code=404, detail="Producto no encontrado")
    return producto


@router.post("/", response_model=Producto, status_code=status.HTTP_201_CREATED)
async def crear_producto(producto: ProductoCreate, db: Session = Depends(get_db)):
    """POST /api/productos — Crear producto.
    Equivalente a doPost en ProductoServlet"""
    db_producto = ProductoDB(**producto.model_dump())
    db.add(db_producto)
    db.commit()
    db.refresh(db_producto)
    return db_producto


@router.put("/{producto_id}", response_model=Producto)
async def actualizar_producto(
    producto_id: int,
    producto: ProductoCreate,
    db: Session = Depends(get_db)
):
    """PUT /api/productos/5 — Actualizar producto.
    Equivalente a ProductoDAO.actualizar()"""
    db_producto = db.query(ProductoDB).filter(ProductoDB.id == producto_id).first()
    if not db_producto:
        raise HTTPException(status_code=404, detail="Producto no encontrado")
    for key, value in producto.model_dump().items():
        setattr(db_producto, key, value)
    db.commit()
    db.refresh(db_producto)
    return db_producto


@router.delete("/{producto_id}", status_code=status.HTTP_204_NO_CONTENT)
async def eliminar_producto(producto_id: int, db: Session = Depends(get_db)):
    """DELETE /api/productos/5 — Eliminar producto.
    Equivalente a ProductoDAO.eliminar()"""
    db_producto = db.query(ProductoDB).filter(ProductoDB.id == producto_id).first()
    if not db_producto:
        raise HTTPException(status_code=404, detail="Producto no encontrado")
    db.delete(db_producto)
    db.commit()
    return None

Punto de entrada (main.py)

main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from database import engine, Base
from routers import producto as producto_router

# Crear tablas en BD si no existen (como ejecutar inventario_db.sql)
Base.metadata.create_all(bind=engine)

app = FastAPI(
    title="API Inventario - SENA ADSO",
    description="API REST del sistema de inventario. Continuación de la guía JSP + MVC",
    version="1.0.0",
    docs_url="/docs",     # Swagger UI interactivo
    redoc_url="/redoc",   # Documentación alternativa
)

# CORS: permite que React (http://localhost:5173) consuma la API
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://localhost:3000"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# Registrar rutas (como @WebServlet, pero automático)
app.include_router(producto_router.router)


@app.get("/")
async def root():
    """Endpoint de bienvenida"""
    return {
        "mensaje": "API Inventario - SENA ADSO",
        "docs": "/docs",
        "version": "1.0.0"
    }


@app.get("/health")
async def health_check():
    """Verificar que la API funciona"""
    return {"status": "ok", "database": "conectada"}
4

Ejecutar y Probar la API

Inicia el servidor y prueba los endpoints

Iniciar el servidor

Terminal — Iniciar FastAPI
# Activar entorno virtual
venv\Scripts\activate
# Iniciar servidor (recarga automática con --reload)
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# Resultado esperado:
# INFO: Uvicorn running on http://0.0.0.0:8000
# INFO: Application startup complete.
# INFO: Swagger UI disponible en http://localhost:8000/docs

Probar con Swagger UI

Swagger UI — http://localhost:8000/docs
┌─────────────────────────────────────────────────────────────┐│  Swagger UI — API Inventario SENA ADSO                      ││  ─────────────────────────────────────────────────          ││                                                             ││  ▸ GET    /api/productos/       Listar productos            ││  ▸ POST   /api/productos/       Crear producto              ││  ▸ GET    /api/productos/{id}   Obtener producto por ID     ││  ▸ PUT    /api/productos/{id}   Actualizar producto         ││  ▸ DELETE /api/productos/{id}   Eliminar producto           ││                                                             ││  [Try it out] → Ejecuta desde el navegador sin Postman      │└─────────────────────────────────────────────────────────────┘

Probar con curl (o PowerShell)

Terminal — Pruebas con curl
# 1. Health check
curl http://localhost:8000/health
# Salida:
# {"status":"ok","database":"conectada"}

# 2. Listar productos (GET)
curl http://localhost:8000/api/productos/
# Salida (vacío inicialmente):
# []

# 3. Crear un producto (POST)
curl -X POST http://localhost:8000/api/productos/ \
  -H "Content-Type: application/json" \
  -d "{\"codigo\":\"PROD-001\",\"nombre\":\"Laptop HP\",\"precio\":2500000.00,\"stock\":15,\"categoria\":\"Tecnologia\"}"
# Salida:
# {"id":1,"codigo":"PROD-001","nombre":"Laptop HP","descripcion":null,
#  "precio":2500000.00,"stock":15,"categoria":"Tecnologia",
#  "estado":true,"fecha_registro":"2026-06-28T10:30:00"}

# 4. Listar productos (ahora con datos)
curl http://localhost:8000/api/productos/
# Salida:
# [{"id":1,"codigo":"PROD-001","nombre":"Laptop HP","precio":2500000.00,"stock":15,...}]

# 5. Obtener producto por ID
curl http://localhost:8000/api/productos/1
# Salida:
# {"id":1,"codigo":"PROD-001","nombre":"Laptop HP","precio":2500000.00,"stock":15,...}

# 6. Actualizar producto (PUT)
curl -X PUT http://localhost:8000/api/productos/1 \
  -H "Content-Type: application/json" \
  -d "{\"codigo\":\"PROD-001\",\"nombre\":\"Laptop HP Pavilion\",\"precio\":2600000.00,\"stock\":12,\"categoria\":\"Tecnologia\"}"
# Salida:
# {"id":1,"codigo":"PROD-001","nombre":"Laptop HP Pavilion","precio":2600000.00,"stock":12,...}

# 7. Eliminar producto (DELETE)
curl -X DELETE http://localhost:8000/api/productos/1
# Salida: (vacío - código 204)
# (No Content)

🐳 vs 🪟 Ejecución: Docker vs Nativo Windows

Terminal — Comparativa de ejecución
# ─────────────────────────────────────────────
# 🐳 OPCIÓN A: DOCKER (reproducible, producción)
# ─────────────────────────────────────────────
docker compose -f docker-compose.fullstack.yml up -d
# Resultado: PostgreSQL + FastAPI + React en 30s

# ─────────────────────────────────────────────
# 🪟 OPCIÓN B: NATIVO WINDOWS (desarrollo, debugging)
# ─────────────────────────────────────────────
# Terminal 1: FastAPI
cd recursos\fastapi-inventario
venv\Scripts\activate
uvicorn main:app --reload --port 8000
# Terminal 2: React (opcional, solo frontend)
cd recursos\frontend-inventario
npm run dev
# ⚡ Script automático (inicia todo):
.\start-fullstack.ps1

# ─────────────────────────────────────────────
# 📊 COMPARACIÓN:
# ─────────────────────────────────────────────
# Característica      Docker          Nativo
# ────────────        ──────          ─────
# Instalación previa  Docker          PG + Python + Node
# Puerto PostgreSQL   5432            5432 (servicio Win)
# Puerto FastAPI      8000            8000
# Puerto React        80              5173
# Recarga automática  rebuild         --reload / npm dev
# Tiempo inicio       30s             10s
# Aislamiento         ✅              ❌

Tabla Resumen: JSP/Servlet vs FastAPI

Concepto JSP + MVC Equivalente FastAPI Propósito
Record Producto.java Schema Pydantic Validación de datos
ProductoDAO.java Modelo SQLAlchemy Acceso a base de datos
ProductoServlet.java Router (routers/producto.py) Endpoints de la API
ConexionDB.java database.py + get_db() Conexión a PostgreSQL
db.properties DATABASE_URL Configuración de BD
pom.xml requirements.txt Gestión de dependencias
Tomcat (WAR) Uvicorn (uvicorn main:app) Servidor de aplicaciones
JSP/HTML (Server-side) JSON (API REST) Formato de respuesta
LoginServlet + HttpSession routers/auth.py + JWT Autenticación y sesión
Sesión en servidor (stateful) Token JWT (stateless) Microservicios ready

Bonus: Testing, Paginación y Logging

Profesionaliza tu API con buenas prácticas

🧪 Pruebas automatizadas con pytest

Así como en JSP usabas JUnit, aquí usamospytestconTestClientde FastAPI:

Terminal — Ejecutar tests
# Instalar pytest
pip install pytest httpx
# Ejecutar todas las pruebas
pytest tests/ -v
# Resultado esperado:
# tests/test_api.py::test_health_check PASSED
# tests/test_api.py::test_login_credenciales_invalidas PASSED
# tests/test_api.py::test_register_datos_invalidos PASSED
# tests/test_api.py::test_listar_productos PASSED
# tests/test_api.py::test_listar_productos_con_paginacion PASSED
# tests/test_api.py::test_obtener_producto_no_existe PASSED
# tests/test_api.py::test_ruta_no_existe PASSED
# tests/test_api.py::test_docs_disponible PASSED
# ───────────────────────────────────────────────
# 8 passed in 2.34s

📄 Logging automático de peticiones

FastAPI registra cada petición con método, ruta, status y duración:

api.log — Logs en tiempo real
2026-06-28 12:00:01 [INFO] api: GET /api/productos/ → 200 (0.032s)
2026-06-28 12:00:02 [INFO] api.productos: Producto creado ID=10 por usuario=admin
2026-06-28 12:00:03 [INFO] api: POST /api/productos/ → 201 (0.045s)
2026-06-28 12:00:04 [WARNING] api: ValueError: ... → /api/productos/abc
JSP (sin logs automáticos)
// En JSP tocaba agregar logger manual
// en cada Servlet:
Logger.getLogger("ProductoServlet")
    .info("Producto creado");
FastAPI (middleware global)
# Un solo middleware logea TODAS las rutas
@app.middleware("http")
async def log_requests(request, call_next):
    start = datetime.now()
    response = await call_next(request)
    elapsed = (datetime.now() - start).total_seconds()
    logger.info(f"{request.method} {request.url.path} → "
                f"{response.status_code} ({elapsed:.3f}s)")
    return response

📊 Paginación en la API

El endpointGET /api/productos/ahora soporta paginación:

curl — Probar paginación
# Página 1, 5 items por página
curl "http://localhost:8000/api/productos/?pagina=1&tamano=5"
# Resultado:
# {
#   "items": [...],
#   "total": 25,
#   "pagina": 1,
#   "tamano": 5,
#   "total_paginas": 5
# }
# Página 2
curl "http://localhost:8000/api/productos/?pagina=2&tamano=5"
# Equivalente en JSP:
# SELECT * FROM productos LIMIT 5 OFFSET 5
# En FastAPI: query.offset(5).limit(5).all()

🧪 JSP vs FastAPI: Testing

JSP:Pruebas manuales (abrir navegador, probar CRUD uno por uno).

FastAPI:pytest tests/ -v— 8 pruebas automatizadas en 2 segundos.

El archivotests/test_api.pyincluye tests para health, auth, productos, paginación y errores.

¡Siguiente paso!

Ahora que tienes la API REST funcionando con FastAPI, continúa con la guía deReactpara construir el frontend moderno que consuma estos endpoints. Es el equivalente a tener los Servlets listos y ahora construir las vistas JSP, pero con tecnología moderna.

Ejercicios de FastAPI

Practica lo aprendido creando nuevas funcionalidades en tu API.

1
Fácil

Endpoint de Búsqueda

Agrega un endpointGET /api/productos/buscar?q=textoque busque productos por nombre usando SQLILIKE.

Pista:UsaProductoDB.nombre.ilike(f"%{q}%")en SQLAlchemy

2
Intermedio

Endpoint de Estadísticas

CreaGET /api/productos/estadisticasque retorne: total productos, precio promedio, stock total, productos por categoría.

Pista:Usadb.query(func.count(), func.avg())de SQLAlchemy

3
Reto

CRUD de Usuarios

Crea un router completo/api/usuarioscon GET, POST, PUT, DELETE. Usa el modelousuariosde PostgreSQL con password hasheado (bcrypt).

Pista:Usafrom passlib.hash import bcryptpara hashear passwords

Transición: De FastAPI (Backend) a React (Frontend)

Ya tienes tu API REST con JWT funcionando con FastAPI. Ahora necesitas una interfaz de usuario que consuma esos endpoints protegidos. Así como en JSP usabas JSTL + Expression Language y session.getAttribute("usuario"), en React usarás Componentes + Hooks + AuthContext y el JWT se guarda en localStorage y se envía automáticamente con cada petición vía Axios Interceptor.

El Mismo CRUD: Código JSP vs Código React

La mejor forma de entender la transición es ver el mismo caso de uso implementado en ambos stacks:

JSP + Servlet (Monolito)
// ── Servlet: recibe petición ──
@WebServlet("/productos")
public class ProductoServlet {

    ProductoDAO dao = new ProductoDAO();

    protected void doGet(...) {
        // Obtener datos
        List<Producto> lista = dao.listarTodos();
        // Poner en request
        req.setAttribute("productos", lista);
        // Forward a JSP
        req.getRequestDispatcher(
            "/lista.jsp").forward(req, resp);
    }
}

<%-- ── JSP: renderiza HTML ──  --%>
<c:forEach var="p" items="${productos}">
    <tr>
        <td>${p.nombre}</td>
        <td>${p.precio}</td>
    </tr>
</c:forEach>
FastAPI + React (Microservicios)
# ── Router FastAPI: sirve JSON ──
@router.get("/productos/")
async def listar(db: Session = Depends(get_db)):
    productos = db.query(ProductoDB).all()
    return productos
    # FastAPI convierte automáticamente
    # SQLAlchemy → Pydantic → JSON

// ── Componente React: consume API ──
function ProductoLista() {
    const [productos, setProductos]
        = useState([]);

    useEffect(() => {
        fetch("/api/productos/")
            .then(r => r.json())
            .then(setProductos);
    }, []);

    return (
        <table>
            {productos.map(p =>
                <tr key={p.id}>
                    <td>{p.nombre}</td>
                    <td>{p.precio}</td>
                </tr>
            )}
        </table>
    );
}
Login JSP (HttpSession)
// Servlet login
String user = req.getParameter("user");
String pass = req.getParameter("pass");
Usuario u = dao.login(user, pass);
if (u != null) {
    // Guarda en sesión del servidor
    HttpSession sesion = req.getSession();
    sesion.setAttribute("usuario", u);
    sesion.setMaxInactiveInterval(1800);
    resp.sendRedirect("productos");
} else {
    resp.sendRedirect("login.jsp?error=1");
}
Login React (JWT)
// AuthContext (React)
async function login(username, password) {
    const res = await axios.post(
        "/api/auth/login",
        { username, password }
    );
    // Guarda token en el navegador
    const token = res.data.access_token;
    localStorage.setItem("token", token);

    // Configura header automático
    axios.defaults.headers.common[
        "Authorization"
    ] = `Bearer ${token}`;

    navigate("/productos");
}

CORS: El Problema que Aparece al Separar Frontend y Backend

Cuando el frontend (React en :5173) y el backend (FastAPI en :8000) están en distintos puertos, el navegador bloquea las peticiones por seguridad. Esto se llama CORS (Cross-Origin Resource Sharing). En JSP no existía porque el frontend y backend estaban en el mismo servidor.

❌ Sin CORS: Error en consola
Access to fetch at 'http://localhost:8000/'
from origin 'http://localhost:5173'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header
✅ Con CORS: FastAPI lo resuelve
# FastAPI: un middleware lo soluciona
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",
        "http://localhost:3000",
    ],
    allow_methods=["*"],
    allow_headers=["*"],
)

JSP vs CORS: ¿Por qué en JSP no existía este problema?

En el monolito JSP, el frontend (JSP) y el backend (Servlet) están en el mismo WAR, el mismo Tomcat, el mismo puerto (:8080). El navegador ve todo como un mismo origen. No hay CORS. Al separar frontend y backend en distintos servidores y puertos, el navegador exige que el backend declare explícitamente qué orígenes están permitidos.

Flujo de Datos: PostgreSQL → FastAPI → React

┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ PostgreSQL │ │ FastAPI │ │ React │ │ Usuario │ │ (datos) │ │ (API) │ │ (SPA) │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ │ │ 1. GET /api/productos/ │ │ │←─────────────────────────────────────│ │ │ │ useEffect() │ │ │ │ llama a Axios │ │ 2. SELECT * │ │ │ │ FROM productos │ │ │ │←──────────────────│ │ │ │ │ │ │ │ 3. Filas como │ │ │ │ tuplas raw │ │ │ │───────────────────→│ 4. SQLAlchemy │ │ │ │ mapea a objetos │ │ │ │ Pydantic valida │ │ │ │ │ │ │ │ 5. JSON array │ │ │ │ [{"id":1, ...}] │ │ │ │──────────────────→│ │ │ │ │ 6. setState() │ │ │ │ React renderiza │ │ │ │ ProductoCard[] │ │ │ │ │ │ │ │ 7. HTML en │ │ │ │ el navegador │ │ │ │──────────────────→│

Flujo Completo de Autenticación: Login → JWT → Petición Protegida

┌────────────────────────────────────────────────────────────────────────────────────┐ │ LOGIN: usuario envía credenciales, recibe JWT │ │ │ │ React FastAPI PostgreSQL │ │ │ │ │ │ │ │ POST /api/auth/login │ │ │ │ │ { │ │ │ │ │ "username": "admin", │ │ │ │ │ "password": "1234" │ │ │ │ │ } │ │ │ │ │─────────────────────────→│ │ │ │ │ │ SELECT * FROM usuarios │ │ │ │ │ WHERE username='admin' │ │ │ │ │───────────────────────────→│ │ │ │ │←───────────────────────────│ │ │ │ │ Verifica password con │ │ │ │ │ bcrypt (hash) │ │ │ │ │ Genera JWT: │ │ │ │ │ jwt.encode({ │ │ │ │ │ "sub": "admin", │ │ │ │ │ "rol": "Administrador", │ │ │ │ │ "exp": 1719561600 │ │ │ │ │ }, SECRET_KEY) │ │ │ │ │ │ │ │ │ 200 OK │ │ │ │ │ { │ │ │ │ │ "access_token": │ │ │ │ │ "eyJhbGciOiJ...", │ │ │ │ │ "token_type": "bearer",│ │ │ │ │ "usuario": { │ │ │ │ │ "username": "admin", │ │ │ │ │ "rol": "Admin" │ │ │ │ │ } │ │ │ │ │ } │ │ │ │ │←─────────────────────────│ │ │ │ │ │ │ │ │ │ localStorage.setItem( │ │ │ │ │ "token", token) │ │ │ │ │ AuthContext.setUsuario()│ │ │ │ │ │ │ │ │ │ ───────────────────────────────────────────────────────────────────────── │ │ │ │ │ │ │ │ PETICIÓN PROTEGIDA: Axios envía JWT automáticamente │ │ │ │ │ │ │ │ GET /api/productos/ │ │ │ │ │ Headers: { │ │ │ │ │ Authorization: │ │ │ │ │ "Bearer eyJhbGci..." │ │ │ │ │ } │ │ │ │ │─────────────────────────→│ │ │ │ │ │ Valida JWT: │ │ │ │ │ jwt.decode(token, │ │ │ │ │ SECRET_KEY, │ │ │ │ │ algorithms=["HS256"]) │ │ │ │ │ Extrae: "sub":"admin" │ │ │ │ │ Extrae: "rol":"Admin" │ │ │ │ │ │ │ │ │ │ ¿El rol tiene permiso │ │ │ │ │ para este endpoint? │ │ │ │ │ ✅ Sí → ejecuta consulta │ │ │ │ │ │ │ │ │ │ SELECT * FROM productos │ │ │ │ │ (solo si el rol tiene │ │ │ │ │ permisos) │ │ │ │ │───────────────────────────→│ │ │ │ │←───────────────────────────│ │ │ │ 200 OK │ │ │ │ │ [{"id":1,"nombre":...}] │ │ │ │ │←─────────────────────────│ │ │ └────────────────────────────────────────────────────────────────────────────────────┘

Comparación: Server-Side Rendering vs Client-Side Rendering

Dimensión 🏢 SSR — JSP (Server-Side) 🔗 CSR — React (Client-Side)
Renderizado Tomcat genera HTML completo y lo envía al navegador El navegador descarga JS, ejecuta React, genera HTML localmente
Estado de página Desaparece al recargar (hay que volver a consultar la BD) Persiste en memoria (React state) mientras la SPA esté abierta
Navegación Cada clic = recarga completa de página + viaje al servidor Transiciones instantáneas: solo viaja el JSON, no el HTML
SEO ✅ Excelente: el HTML ya viene con contenido indexable ⚠️ Requiere SSR (Next.js) o prerendering para contenido público
Carga inicial Rápida: el servidor envía HTML listo para mostrar Lenta: descarga JS (+250KB), ejecuta, luego renderiza
Carga siguientes Lenta: cada página recarga todo el HTML (más ancho de banda) Rápida: solo viaja JSON (datos puros, sin HTML duplicado)
UX / Interactividad Limitada: cada acción recarga la página (parpadeo blanco) Excelente: animaciones, transiciones, feedback instantáneo
Offline / PWA ❌ No posible: depende del servidor para todo ✅ Posible: Service Workers, cache de datos, modo offline

Estado de Sesión: HttpSession vs JWT vs Context API

HttpSession (JSP)

El servidor guarda el usuario en RAM. Stateful: si el servidor se cae, pierdes la sesión. No escalable horizontalmente sin sticky sessions.

// Guardar en servidor
req.getSession()
  .setAttribute("usuario", u);

// Leer en JSP
${sessionScope.usuario.nombre}

// Cerrar sesión
req.getSession().invalidate();
JWT (FastAPI)

Token firmado que el cliente guarda. Stateless: cualquier servidor puede validarlo sin compartir sesión. Escalable horizontalmente.

// Generar en backend
token = jwt.encode({
    "sub": user,
    "rol": "Admin",
    "exp": time + 3600
}, SECRET_KEY)

// Validar en backend
payload = jwt.decode(
    token, SECRET_KEY,
    algorithms=["HS256"]
)
Context API (React)

Estado global en memoria del navegador. El AuthContext provee usuario y token a toda la SPA sin pasarlos por props.

// Proveer contexto
<AuthContext.Provider
 value={{user, token, login, logout}}>
  <App />
</AuthContext.Provider>

// Consumir en cualquier
// componente hijo
const { user, login }
  = useContext(AuthContext);

Pipeline de Build y Despliegue: JSP vs React

Paso 🏢 JSP (WAR) 🔗 React (Vite)
Código fuente .java, .jsp, pom.xml .jsx, .tsx, package.json
Dependencias mvn dependency:resolve (descarga .jar) npm install (descarga node_modules)
Compilación javac compila .java → .class vite build compila JSX → JS estático
Output .war (Web Archive, ~10-30MB) Carpeta dist/ (archivos estáticos, ~200KB)
Servidor Tomcat despliega el WAR (interpreta .class) Nginx sirve archivos estáticos (no necesita ejecución)
Hot reload Requiere reiniciar Tomcat o usar JRebel Vite HMR: los cambios se reflejan en milisegundos
Comando mvn clean package cargo:run npm run dev (desarrollo) / npm run build (producción)

Manejo de Estado en React: ¿Cuándo usar qué?

En JSP el estado era simple: request, session, application. En React hay más opciones. Aquí una guía de decisión:

┌─────────────────────────────────────────────────────────────────────────────┐ │ ¿El dato lo usa solo un componente? │ │ │ │ │ ├─ Sí → ¿Cambia con el tiempo? │ │ │ │ │ │ │ ├─ Sí → useState (ej: valor de input, toggle, contador) │ │ │ │ const [nombre, setNombre] = useState("") │ │ │ │ │ │ │ └─ No → variable normal o useRef (ej: intervalo, timeout) │ │ │ const intervalRef = useRef(null) │ │ │ │ │ ├─ No → ¿Lo necesitan varios componentes hermanos? │ │ │ │ │ │ │ ├─ Sí → Lifting state up (elevar estado al padre) │ │ │ │ <Padre> │ │ │ │ <HijoA valor={estado} /> │ │ │ │ <HijoB onChange={setEstado} /> │ │ │ │ </Padre> │ │ │ │ │ │ │ └─ No → ¿Lo necesitan componentes en distintas ramas? │ │ │ │ │ │ │ ├─ Sí → Context API (ej: auth, tema, idioma) │ │ │ │ const AuthContext = createContext() │ │ │ │ <AuthContext.Provider value={user}> │ │ │ │ <App /> │ │ │ │ </AuthContext.Provider> │ │ │ │ │ │ │ └─ No y el estado es complejo → useReducer │ │ │ (ej: carrito de compras, formularios grandes) │ │ │ │ │ └── ¿Miles de componentes y estado muy complejo? → Redux / Zustand │ │ (solo cuando Context API + useReducer se quedan cortos) │ └─────────────────────────────────────────────────────────────────────────────┘

Resumen: Del viaje completo al viaje ligero

JSP: cada clic = viaje redondo al servidor (request → Servlet → DAO → BD → DAO → Servlet → JSP → HTML → navegador). Todo el HTML se regenera aunque solo cambie un número. La página parpadea al recargar.

React + FastAPI: carga inicial más pesada (descargar React), pero después solo viaja JSON. El navegador actualiza solo la parte de la pantalla que cambia. Sin recargas, sin parpadeos. Menos ancho de banda, más velocidad percibida.

La API de FastAPI no solo sirve a React — puede servir a apps móviles (Flutter), otros servicios, o integraciones de terceros. El JSP solo servía HTML para navegadores.

11

Frontend con React 19 + Vite + Auth JWT

Consume la API de FastAPI con autenticación JWT, rutas protegidas y control de roles

Continuación directa de FastAPI

Esta guía asume que tienes la API de FastAPI corriendo enhttp://localhost:8000con los endpoints de auth JWT. React será nuestra nueva "Vista" (V de MVC), consumiendo los endpoints JSON protegidos. Así como JSP usabasession.getAttribute("usuario")para saber quién está logueado, React guarda elJWT en localStoragey lo envía en cada petición víaAxios Interceptor.

🧪

Primer Ejemplo: Hola Mundo con React

Tu primer componente React — ¡en 2 minutos!

Objetivo: Ver "Hola Mundo" renderizado por React

Crea tu primer componente React. Al final verás un mensaje"¡Hola Mundo desde React 19!"en http://localhost:5173.Nota:Así como JSP renderizaba HTML en el servidor, React renderiza componentes en el navegador.

Paso 1: Crear proyecto con Vite

🪟 Nativo Windows:
Terminal — Crear proyecto (Nativo)
# Crear proyecto con Vite + React
npm create vite@latest hola-mundo-react -- --template react
# Resultado:
# ✔ Project name: hola-mundo-react
# ✔ Template: react
# Done.
# Entrar e instalar
cd hola-mundo-react
npm install
# Iniciar servidor de desarrollo
npm run dev
# Resultado esperado:
# VITE v5.4.0  ready in 250ms
# ➜  Local:   http://localhost:5173/
# ➜  presiona h + enter para ayuda
# Abre http://localhost:5173 — verás la página default de Vite + React
🐳 Con Docker (producción):
Terminal — Docker
# Construir imagen de producción
docker build -t hola-react .
docker run -d -p 80:80 --name hola-react hola-react
# Abrir: http://localhost
# Detener
docker stop hola-react && docker rm hola-react

Paso 2: Reemplazar App.jsx con nuestro componente

src/App.jsx
// Equivalente a un archivo JSP, pero en JavaScript
// En lugar de renderizar en el servidor (Tomcat),
// renderiza en el navegador del cliente
function App() {
    const nombre = "Miguel";
    const fecha = new Date().toLocaleString("es-CO");
    return (
        <div style={{
            fontFamily: "Arial, sans-serif",
            textAlign: "center",
            padding: "4rem 2rem",
            background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
            minHeight: "100vh",
            color: "white"
        }}>
            <h1 style={{ fontSize: "3rem", marginBottom: "1rem" }}>
                ¡Hola Mundo desde React 19!
            </h1>
            <p style={{ fontSize: "1.2rem", opacity: 0.9 }}>
                Mi primer componente en React 🚀
            </p>
            <p style={{ fontSize: "1rem", opacity: 0.7 }}>
                Bienvenido, {nombre}
            </p>
            <div style={{
                marginTop: "2rem",
                padding: "1rem",
                background: "rgba(255,255,255,0.1)",
                borderRadius: "12px",
                display: "inline-block"
            }}>
                Fecha actual: {fecha}
            </div>
            <hr style={{
                margin: "2rem auto",
                width: "60%",
                border: "1px solid rgba(255,255,255,0.2)"
            }} />
            <h2>Equivalencias con JSP</h2>
            <ul style={{ listStyle: "none", padding: 0 }}>
                <li>✅ JSP → JSX (JavaScript XML)</li>
                <li>✅ ${variable} → {variable}</li>
                <li>✅ Servidor (Tomcat) → Navegador (Cliente)</li>
                <li>✅ HTML estático → Componentes dinámicos</li>
            </ul>
        </div>
    );
}
export default App;

Paso 3: Ver el resultado en el navegador

Resultado en http://localhost:5173
┌──────────────────────────────────────────────────────────────┐
│                                                              │
│           ¡Hola Mundo desde React 19!                       │
│           Mi primer componente en React                      │
│                                                              │
│           Bienvenido, Miguel                                 │
│                                                              │
│           ┌────────────────────────────────────┐             │
│           │ Fecha actual: 28/6/2026, 12:00 PM  │             │
│           └────────────────────────────────────┘             │
│                                                              │
│           ─────────────────────────────                       │
│           Equivalencias con JSP                              │
│           ✅ JSP → JSX (JavaScript XML)                      │
│           ✅ ${variable} → {variable}                        │
│           ✅ Servidor (Tomcat) → Navegador (Cliente)         │
│           ✅ HTML estático → Componentes dinámicos           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Paso 4: Consumir datos desde la API de FastAPI

src/App.jsx (con API)
import { useState, useEffect } from "react";
function App() {
    const [mensaje, setMensaje] = useState("Cargando...");
    // useEffect = como doGet() del Servlet, pero del lado del cliente
    useEffect(() => {
        fetch("http://localhost:8000")
            .then(res => res.json())
            .then(data => setMensaje(data.mensaje))
            .catch(err => setMensaje("Error: " + err.message));
    }, []);
    return (
        <div style={{ textAlign: "center", padding: "4rem" }}>
            <h1>Conectado a FastAPI</h1>
            <p>Mensaje desde la API: <strong>{mensaje}</strong></p>
            <p>(FastAPI debe estar corriendo en :8000)</p>
        </div>
    );
}
export default App;
// Resultado esperado:
// ┌──────────────────────────────────────┐
// │                                      │
// │   Conectado a FastAPI                │
// │                                      │
// │   Mensaje desde la API:              │
// │   Hola Mundo desde FastAPI!          │
// │                                      │
// │   (FastAPI debe estar corriendo      │
// │    en :8000)                         │
// │                                      │
// └──────────────────────────────────────┘

¡Has conectado React + FastAPI!

Acabas de crear un componente React que consume datos de la API FastAPI. Este es el mismo patrón que usarás en el sistema de inventario completo: React muestra datos, FastAPI los sirve, PostgreSQL los almacena. Ahora construyamos la aplicación completa.

Proyecto React 19 listo para ejecutar

Descarga el proyecto React completo con: Vite + Axios + React Router. Ya incluye AuthContext para JWT, ProtectedRoute y componentes CRUD listos para conectar a FastAPI.

Solo necesitas: npm install && npm run dev y abre http://localhost:5173

Descargar proyecto React (32 KB)
1

Crear Proyecto React con Vite

Configuración inicial y estructura

Crear proyecto

Terminal — Crear proyecto Vite
# Crear proyecto con Vite + React template
npm create vite@latest frontend-inventario -- --template react
# Resultado:
# ✔ Project name: frontend-inventario
# ✔ Template: react
# Done. Now run:
#   cd frontend-inventario
#   npm install
#   npm run dev
# Instalar dependencias adicionales
cd frontend-inventario
npm install
npm install axios react-router-dom
# Iniciar en desarrollo
npm run dev
# Resultado esperado:
#   VITE v5.4.0  ready in 250ms
#   ➜  Local:   http://localhost:5173/
#   ➜  Network: http://192.168.1.10:5173/
#   ➜  press h + enter to show help

Estructura del proyecto

Estructura del proyecto React
frontend-inventario/
├── .env / .env.example         # VITE_API_URL=http://localhost:8000/api
├── src/
│   ├── contexts/
│   │   └── AuthContext.jsx       # Estado global de auth + JWT
│   ├── services/
│   │   └── api.js                # Axios + Interceptor JWT + env vars
│   ├── components/
│   │   ├── Navbar.jsx            # Barra con user/login/logout
│   │   ├── ProtectedRoute.jsx    # Guard de rutas (roles)
│   │   ├── ProductoCard.jsx      # Tarjeta (como fragmento JSP)
│   │   ├── ProductoLista.jsx     # Lista (como lista.jsp)
│   │   └── ProductoForm.jsx      # Formulario crear/editar
│   ├── pages/
│   │   ├── Login.jsx             # Login con JWT
│   │   ├── Register.jsx          # Registro de usuarios
│   │   └── AdminUsuarios.jsx     # Admin: CRUD usuarios
│   ├── App.jsx                   # Raíz + AuthProvider + Rutas
│   ├── App.css                   # Estilos globales
│   └── main.jsx                  # Punto de entrada
├── index.html
├── vite.config.js
└── package.json

Variables de entorno

.env — Configurar URL de la API
# Las variables VITE_ son expuestas automáticamente por Vite
# .env (copiar de .env.example)
VITE_API_URL=http://localhost:8000/api

# En el código:
# const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'

Dev vs Prod: cambio de URL automático

En desarrollo:.envconVITE_API_URL=http://localhost:8000/api

En producción (Docker): elnginx.confhace proxy reverso a la API, o se cambia la variable en el build.

En JSP: la URL era fija porque el frontend estaba en el mismo servidor. Con React + FastAPI, pueden estar en servidores diferentes.

Flujo de autenticación

Login Flow
┌──────────┐    POST /api/auth/login    ┌──────────┐
│  React   │ ──── {username, password} ──→│ FastAPI  │
│  Login   │                              │  Auth    │
│  Page    │ ←── {access_token, usuario}──│  Router  │
└──────────┘                              └──────────┘
     │                                         │
     │  localStorage.setItem(                   │  jwt.encode(
     │    "access_token", token)                │    {"sub": user, "rol": "Admin"}
     │  localStorage.setItem(                   │  )
     │    "usuario", JSON.stringify(user))      │
     ▼                                         ▼
┌──────────┐    GET /api/productos/       ┌──────────┐
│  React   │ ──── Authorization: Bearer ──→│ FastAPI  │
│  Axios   │        <token>               │  Router  │
│  Interceptor│                            │  valida  │
│  (automático)│ ←── JSON(productos) ──────│  JWT     │
└──────────┘                              └──────────┘

Limpieza inicial

Terminal — Limpiar archivos base
# Eliminar archivos de ejemplo que Vite crea
# Windows: del src\App.css src\index.css src\assets\react.svg
# Linux/Mac: rm src/App.css src/index.css src/assets/react.svg
Remove-Item src/App.css, src/index.css, src/assets/react.svg
# Crear directorios para nuestros componentes
mkdir src/services, src/components
# Resultado final:
# src/
# ├── App.jsx        # Vamos a editar
# ├── main.jsx       # Se mantiene igual
# ├── services/
# │   └── api.js     # Servicio API
# └── components/
#     ├── Navbar.jsx
#     ├── ProductoCard.jsx
#     ├── ProductoLista.jsx
#     └── ProductoForm.jsx
2

Servicio API con Axios

Equivalente a ProductoDAO.java pero en JavaScript

api.js — Capa de servicio

src/services/api.js
import axios from 'axios';

// URL base de nuestra API FastAPI
// Equivalente a db.properties → URL de conexión
const API_URL = 'http://localhost:8000/api';
const api = axios.create({
    baseURL: API_URL,
    headers: {
        'Content-Type': 'application/json'
    },
});

// ===== CRUD Productos =====
// Equivalente a ProductoDAO.java pero con promesas
export const listarProductos = async (categoria = null) => {
    // GET /api/productos/?categoria=Tecnologia
    // Equivalente a: ProductoDAO.listarTodos()
    const params = categoria ? { categoria } : {};
    const response = await api.get('/productos/', {
        params
    });
    return response.data;
    // Retorna array de productos
};
export const obtenerProducto = async (id) => {
    // GET /api/productos/5    // Equivalente a: ProductoDAO.buscarPorId(id)
    const response = await api.get(`/productos/${id}`);
    return response.data;
};
export const crearProducto = async (producto) => {
    // POST /api/productos/ body: { codigo, nombre, precio, ... }
    // Equivalente a: ProductoDAO.insertar(producto)
    const response = await api.post('/productos/', producto);
    return response.data;
};
export const actualizarProducto = async (id, producto) => {
    // PUT /api/productos/5 body: { codigo, nombre, precio, ... }
    // Equivalente a: ProductoDAO.actualizar(producto)
    const response = await api.put(`/productos/${id}`, producto);
    return response.data;
};
export const eliminarProducto = async (id) => {
    // DELETE /api/productos/5    // Equivalente a: ProductoDAO.eliminar(id)
    await api.delete(`/productos/${id}`);
};
3

Componente ProductoCard

Renderiza cada producto — como un fragmento JSP con JSTL

ProductoCard.jsx

src/components/ProductoCard.jsx
import './ProductoCard.css';

export default function ProductoCard({ producto, onEliminar }) {
    const formatoPrecio = new Intl.NumberFormat('es-CO', {
        style: 'currency',
        currency: 'COP',
        minimumFractionDigits: 0,
    });
    const stockClase = producto.stock < 10
        ? 'stock-bajo'
        : 'stock-normal';

    return (
        <div className="producto-card">
            <div className="producto-header">
                <span className="producto-codigo">{producto.codigo}</span>
                <span className={producto.estado ? 'activo' : 'inactivo'}>
                    {producto.estado ? 'Activo' : 'Inactivo'}
                </span>
            </div>

            <h3 className="producto-nombre">{producto.nombre}</h3>

            {producto.descripcion && (
                <p className="producto-descripcion">{producto.descripcion}</p>
            )}

            <div className="producto-detalles">
                <div className="producto-precio">
                    <span className="label">Precio:</span>
                    <span className="valor">
                        {formatoPrecio.format(producto.precio)}
                    </span>
                </div>

                <div className="producto-stock">
                    <span className="label">Stock:</span>
                    <span className={stockClase}>
                        {producto.stock} unidades
                    </span>
                </div>

                <div className="producto-categoria">
                    <span className="label">Categoría:</span>
                    <span className="valor">
                        {producto.categoria || 'Sin categoría'}
                    </span>
                </div>
            </div>

            <div className="producto-acciones">
                <a href={/productos/${producto.id}/editar}
                    className="btn-editar">
                    <i className="fas fa-edit"></i> Editar
                </a>
                <button onClick={() => onEliminar(producto.id)}
                    className="btn-eliminar">
                    <i className="fas fa-trash"></i> Eliminar
                </button>
            </div>
        </div>
    );
}

ProductoCard.css — Estilos del componente

src/components/ProductoCard.css
.producto-card {
    background: var(--card-bg);
    border-radius: 12px;
    padding: 1.5rem;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    transition: transform 0.2s;
}
.producto-card:hover {
    transform: translateY(-2px);
}
.producto-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.75rem;
}
.producto-codigo {
    font-family: monospace;
    font-size: 0.85rem;
    color: var(--text-muted);
    background: var(--bg-muted);
    padding: 0.2rem 0.5rem;
    border-radius: 4px;
}
.producto-estado {
    font-size: 0.75rem;
    padding: 0.2rem 0.6rem;
    border-radius: 10px;
}
.producto-estado.activo {
    background: #d4edda;
    color: #155724;
}
.producto-estado.inactivo {
    background: #f8d7da;
    color: #721c24;
}
.producto-nombre {
    margin: 0 0 0.5rem 0;
    font-size: 1.1rem;
}
.producto-detalles {
    display: grid;
    gap: 0.4rem;
    margin: 0.75rem 0;
}
.producto-detalles .label {
    font-size: 0.8rem;
    color: var(--text-muted);
}
.producto-detalles .valor {
    font-weight: 600;
}
.stock-bajo { color: #dc3545; }
.stock-normal { color: #28a745; }
.producto-acciones {
    display: flex;
    gap: 0.5rem;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}
.btn-editar, .btn-eliminar {
    padding: 0.4rem 1rem;
    border-radius: 6px;
    font-size: 0.85rem;
    cursor: pointer;
    text-decoration: none;
}
.btn-editar {
    background: #007bff;
    color: white;
    border: none;
}
.btn-eliminar {
    background: #dc3545;
    color: white;
    border: none;
}

Resultado visual del componente

Vista renderizada en el navegador
┌──────────────────────────────────────────────┐
│  PROD-001                          [ Activo ] │
│                                               │
│  Laptop HP Pavilion                           │
│  Laptop 15.6" Intel Core i5 8GB RAM          │
│                                               │
│  Precio:     $ 2.500.000                      │
│  Stock:      15 unidades  ✓                   │
│  Categoría:  Tecnología                       │
│                                               │
│  ────────────────────────────────────────     │
│  [✏ Editar]    [🗑 Eliminar]                   │
└──────────────────────────────────────────────┘
4

Componente ProductoLista

Lista todos los productos desde la API — como lista.jsp

ProductoLista.jsx — Consume GET /api/productos/

src/components/ProductoLista.jsx
import { useState, useEffect } from 'react';
import { listarProductos, eliminarProducto } from '../services/api';
import ProductoCard from './ProductoCard';
import Mensaje from './Mensaje';

export default function ProductoLista() {
    const [productos, setProductos] = useState([]);
    const [cargando, setCargando] = useState(true);
    const [error, setError] = useState(null);
    const [filtro, setFiltro] = useState('');
    const [mensaje, setMensaje] = useState(null);

    useEffect(() => {
        cargarProductos();
    }, []);

    async function cargarProductos() {
        try {
            setCargando(true);
            const data = await listarProductos();
            setProductos(data);
            setError(null);
        } catch (err) {
            setError('Error al cargar productos: ' + err.message);
        } finally {
            setCargando(false);
        }
    }

    async function handleEliminar(id) {
        if (!confirm('¿Eliminar este producto?')) return;
        try {
            await eliminarProducto(id);
            setMensaje({ tipo: 'exito', texto: 'Producto eliminado' });
            cargarProductos();
        } catch (err) {
            setMensaje({ tipo: 'error', texto: 'Error al eliminar' });
        }
    }

    const productosFiltrados = filtro
        ? productos.filter(p =>
            p.nombre.toLowerCase().includes(filtro.toLowerCase()) ||
            p.codigo.toLowerCase().includes(filtro.toLowerCase()))
        : productos;

    if (cargando) {
        return (
            <div className="estado-container">
                <div className="spinner"></div>
                <p>Cargando productos...</p>
            </div>
        );
    }

    if (error) {
        return (
            <div className="estado-container error">
                <i className="fas fa-exclamation-triangle"></i>
                <p>{error}</p>
                <button onClick={cargarProductos} className="btn-reintentar">
                    Reintentar
                </button>
            </div>
        );
    }

    return (
        <div className="producto-lista">
            {mensaje && (
                <Mensaje
                    tipo={mensaje.tipo}
                    texto={mensaje.texto}
                    onCerrar={() => setMensaje(null)}
                />
            )}

            <div className="lista-header">
                <h2>Productos ({productosFiltrados.length})</h2>
                <div className="lista-acciones">
                    <input
                        type="text"
                        placeholder="Buscar producto..."
                        value={filtro}
                        onChange={e => setFiltro(e.target.value)}
                        className="buscador"
                    />
                    <a href="/productos/nuevo" className="btn-crear">
                        + Nuevo Producto
                    </a>
                </div>
            </div>

            {productosFiltrados.length === 0 ? (
                <div className="vacio">
                    <i className="fas fa-box-open"></i>
                    <p>{filtro
                        ? 'Sin resultados para "' + filtro + '"'
                        : 'No hay productos'}
                    </p>
                </div>
            ) : (
                <div className="productos-grid">
                    {productosFiltrados.map(producto => (
                        <ProductoCard
                            key={producto.id}
                            producto={producto}
                            onEliminar={handleEliminar}
                        />
                    ))}
                </div>
            )}
        </div>
    );
}

Resultado visual: Lista de Productos

Vista renderizada: Lista de productos desde la API
┌──────────────────────────────────────────────────────────────┐
│  Productos (5)              [🔍 Buscar producto...] [+ Nuevo] │
├──────────────────────────────────────────────────────────────┤
│  ┌──────────────────────┐  ┌──────────────────────┐          │
│  │ PROD-001   [Activo]  │  │ PROD-002   [Activo]  │          │
│  │ Laptop HP Pavilion   │  │ Mouse Logitech M185  │          │
│  │ $ 2.500.000          │  │ $ 45.000             │          │
│  │ 15 unidades  ✓       │  │ 50 unidades  ✓       │          │
│  │ [Editar] [Eliminar]  │  │ [Editar] [Eliminar]  │          │
│  └──────────────────────┘  └──────────────────────┘          │
│  ┌──────────────────────┐  ┌──────────────────────┐          │
│  │ PROD-003   [Activo]  │  │ PROD-004   [Activo]  │          │
│  │ Teclado Mecánico     │  │ Monitor Samsung 24"  │          │
│  │ $ 180.000            │  │ $ 650.000            │          │
│  │ 30 unidades  ✓       │  │ 20 unidades  ✓       │          │
│  │ [Editar] [Eliminar]  │  │ [Editar] [Eliminar]  │          │
│  └──────────────────────┘  └──────────────────────┘          │
│  ┌──────────────────────┐                                    │
│  │ PROD-005   [Activo]  │                                    │
│  │ Audífonos Sony WH-…  │                                    │
│  │ $ 1.200.000          │                                    │
│  │ 25 unidades  ✓       │                                    │
│  │ [Editar] [Eliminar]  │                                    │
│  └──────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘
5

Componente ProductoForm

Crear y editar productos — como formulario.jsp

ProductoForm.jsx — POST y PUT a FastAPI

src/components/ProductoForm.jsx
import { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { crearProducto, actualizarProducto, obtenerProducto } from '../services/api';

export default function ProductoForm() {
    const navigate = useNavigate();
    const { id } = useParams();
    const esEdicion = Boolean(id);

    const [form, setForm] = useState({
        codigo: '', nombre: '', descripcion: '',
        precio: '', stock: '0', categoria: '',
    });
    const [enviando, setEnviando] = useState(false);
    const [error, setError] = useState(null);

    useEffect(() => {
        if (id) {
            obtenerProducto(id)
                .then(data => setForm({
                    codigo: data.codigo,
                    nombre: data.nombre,
                    descripcion: data.descripcion || '',
                    precio: data.precio.toString(),
                    stock: data.stock.toString(),
                    categoria: data.categoria || '',
                }))
                .catch(err => setError('Error al cargar producto'));
        }
    }, [id]);

    const handleChange = (e) => {
        setForm({ ...form, [e.target.name]: e.target.value });
    };

    const handleSubmit = async (e) => {
        e.preventDefault();
        setEnviando(true);
        setError(null);
        try {
            const datos = {
                ...form,
                precio: parseFloat(form.precio),
                stock: parseInt(form.stock),
            };
            if (esEdicion) {
                await actualizarProducto(id, datos);
                alert('Producto actualizado exitosamente');
            } else {
                await crearProducto(datos);
                alert('Producto creado exitosamente');
            }
            navigate('/');
        } catch (err) {
            setError(err.response?.data?.detail || 'Error al guardar producto');
        } finally {
            setEnviando(false);
        }
    };

    return (
        <div className="producto-form-container">
            <h2>{esEdicion ? 'Editar Producto' : 'Nuevo Producto'}</h2>

            {error && (
                <div className="error-banner">
                    <i className="fas fa-exclamation-circle"></i> {error}
                </div>
            )}

            <form onSubmit={handleSubmit} className="producto-form">
                <div className="form-grid">
                    <div className="form-group">
                        <label>Código *</label>
                        <input name="codigo" value={form.codigo}
                            onChange={handleChange} required minLength={3} />
                    </div>

                    <div className="form-group">
                        <label>Nombre *</label>
                        <input name="nombre" value={form.nombre}
                            onChange={handleChange} required />
                    </div>

                    <div className="form-group full-width">
                        <label>Descripción</label>
                        <textarea name="descripcion" value={form.descripcion}
                            onChange={handleChange} rows={3} />
                    </div>

                    <div className="form-group">
                        <label>Precio *</label>
                        <input name="precio" type="number" step="0.01"
                            value={form.precio} onChange={handleChange} required />
                    </div>

                    <div className="form-group">
                        <label>Stock *</label>
                        <input name="stock" type="number"
                            value={form.stock} onChange={handleChange} required />
                    </div>

                    <div className="form-group">
                        <label>Categoría</label>
                        <select name="categoria" value={form.categoria}
                            onChange={handleChange}>
                            <option value="">Seleccionar...</option>
                            <option value="Tecnología">Tecnología</option>
                            <option value="Accesorios">Accesorios</option>
                            <option value="Audio">Audio</option>
                        </select>
                    </div>
                </div>

                <div className="form-acciones">
                    <button type="button" onClick={() => navigate('/')}
                        className="btn-cancelar">
                        Cancelar
                    </button>
                    <button type="submit" disabled={enviando}
                        className="btn-guardar">
                        {enviando ? 'Guardando...'
                            : (esEdicion ? 'Actualizar' : 'Crear Producto')}
                    </button>
                </div>
            </form>
        </div>
    );
}

Resultado visual: Formulario de Producto

Vista renderizada: Nuevo Producto
┌──────────────────────────────────────────────────┐
│  Nuevo Producto                                  │
├──────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────────────┐     │
│  │ Código *     │  │ Nombre *             │     │
│  │ [PROD-009    ]  │ [Tablet Samsung      │     │
│  └──────────────┘  └──────────────────────┘     │
│  ┌──────────────────────────────────────┐       │
│  │ Descripción                          │       │
│  │ [Tablet Samsung Galaxy Tab S9 256GB ]       │
│  └──────────────────────────────────────┘       │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐    │
│  │ Precio * │  │ Stock *  │  │ Categoría │    │
│  │ [2800000 ]  │ [12      ]  │ [Tecnología▼]    │
│  └──────────┘  └──────────┘  └───────────┘    │
│                                                 │
│  [ Cancelar ]        [ Crear Producto ]         │
└──────────────────────────────────────────────────┘
6

App.jsx — Rutas y Navegación

Componente raíz con React Router — como index.jsp + web.xml

App.jsx — Enrutador principal

src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import ProductoLista from './components/ProductoLista';
import ProductoForm from './components/ProductoForm';

function Inicio() {
    return (
        <div className="inicio">
            <h1>Sistema de Inventario</h1>
            <p>API REST con FastAPI + Frontend React</p>
            <div className="inicio-stats">
                <div className="stat-card">
                    <i className="fas fa-boxes"></i>
                    <h3>Gestión de Productos</h3>
                    <p>CRUD completo desde React consumiendo FastAPI</p>
                </div>
                <div className="stat-card">
                    <i className="fas fa-bolt"></i>
                    <h3>Rendimiento</h3>
                    <p>FastAPI + React: respuesta en milisegundos</p>
                </div>
            </div>
        </div>
    );
}

export default function App() {
    return (
        <BrowserRouter>
            <Navbar />
            <main className="contenido">
                <Routes>
                    <Route path="/" element={<Inicio />} />
                    <Route path="/productos" element={<ProductoLista />} />
                    <Route path="/productos/nuevo" element={<ProductoForm />} />
                    <Route path="/productos/:id/editar" element={<ProductoForm />} />
                </Routes>
            </main>
        </BrowserRouter>
    );
}

Navbar.jsx — Navegación

src/components/Navbar.jsx
import { Link } from 'react-router-dom';
import './Navbar.css';

export default function Navbar() {
    return (
        <nav className="navbar">
            <div className="navbar-brand">
                <i className="fas fa-boxes"></i>
                <span>Inventario SENA</span>
            </div>
            <div className="navbar-links">
                <Link to="/">Inicio</Link>
                <Link to="/productos">Productos</Link>
                <Link to="/productos/nuevo" className="btn-nav-crear">
                    + Nuevo
                </Link>
            </div>
        </nav>
    );
}

/* ===== ESTILOS (Navbar.css) ===== */
.navbar {
    background: var(--primary);
    color: white;
    padding: 0 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 60px;
}
.navbar-brand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1.2rem;
    font-weight: 700;
}
.navbar-links {
    display: flex;
    gap: 1rem;
    align-items: center;
}
.navbar-links a {
    color: white;
    text-decoration: none;
    padding: 0.5rem 1rem;
    border-radius: 6px;
    transition: background 0.2s;
    }.navbar-links a:hover {
    background: rgba(255,255,255,0.1);
    }.btn-nav-crear {
    background: rgba(255,255,255,0.2);
    font-weight: 600;
    }*/

Resultado visual: App completa funcionando

Aplicación completa: FastAPI + React
┌──────────────────────────────────────────────────────────────┐
│  [■ Inventario SENA]     Inicio  Productos  [+ Nuevo]       │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Sistema de Inventario                                       │
│  API REST con FastAPI + Frontend React                      │
│                                                              │
│  ┌─────────────────────────┐  ┌─────────────────────────┐   │
│  │  📦 Gestión de          │  │  ⚡ Rendimiento         │   │
│  │     Productos           │  │                         │   │
│  │  CRUD completo desde    │  │  FastAPI + React        │   │
│  │  React consumiendo      │  │  respuesta en           │   │
│  │  FastAPI                │  │  milisegundos           │   │
│  └─────────────────────────┘  └─────────────────────────┘   │
│                                                              │
│  ──────────── FastAPI (backend) ────────────                 │
│  http://localhost:8000/docs                                  │
│  http://localhost:8000/api/                                  │
│  uvicorn main:app --reload                                   │
│                                                              │
│  ──────────── React (frontend) ─────────────                 │
│  http://localhost:5173                                       │
│  npm run dev                                                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘
7

Ejecutar Todo el Stack

FastAPI + React trabajando juntos

Terminal 1: Iniciar FastAPI (Backend)

Terminal 1 — Backend
# En el directorio fastapi-inventario
cd fastapi-inventario
.\venv\Scripts\activate
# Iniciar FastAPI (recarga automática)
uvicorn main:app --reload --port 8000
# Resultado:
# INFO: Uvicorn running on http://127.0.0.1:8000
# INFO: Swagger docs: http://localhost:8000/docs

Terminal 2: Iniciar React (Frontend)

Terminal 2 — Frontend
# En el directorio frontend-inventario
cd frontend-inventario
npm run dev
# Resultado:
# VITE v5.4.0  ready in 250ms
# ➜  Local:   http://localhost:5173/
# Abrir http://localhost:5173 en el navegador
# La app React consume http://localhost:8000/api/productos/

Verificar conexión Frontend ↔ Backend

Navegador — http://localhost:5173
┌─────────────────────────────────────────────────────────────┐
│  En la consola del navegador (F12 → Console):              │
│                                                             │
│  ✓ Productos cargados desde FastAPI: 3 productos            │
│  ✓ GET /api/productos/ → 200 OK (320ms)                    │
│  ✓ CORS configurado correctamente                           │
│                                                             │
│  Network tab:                                               │
│  ┌──────────┬──────────┬────────┬──────────┐               │
│  │ Method   │ Endpoint │ Status │ Time     │               │
│  ├──────────┼──────────┼────────┼──────────┤               │
│  │ GET      │ /api/pro │ 200    │ 320ms    │               │
│  │          │ ductos/  │        │          │               │
│  │ GET      │ /api/pro │ 200    │ 15ms     │               │
│  │          │ ductos/1 │        │          │               │
│  └──────────┴──────────┴────────┴──────────┘               │
└─────────────────────────────────────────────────────────────┘

🐳 vs 🪟 Ejecución del stack completo

Terminal — Docker vs Nativo Windows
# ===========================================================
# 🐳 DOCKER (un solo comando, todo encapsulado)
# ===========================================================
docker compose -f docker-compose.fullstack.yml up -d
# Prueba:  http://localhost

# ===========================================================
# 🪟 NATIVO WINDOWS (tres terminales, máximo control)
# ===========================================================
# Terminal 1: PostgreSQL (servicio Windows) + FastAPI
# Terminal 2: React
# ⚡ O usa el script automático:
.\start-fullstack.ps1
# Prueba:  http://localhost:5173

Ejercicios de React

Practica lo aprendido mejorando el frontend con nuevas funcionalidades.

1
Fácil

Filtro por Categoría

Agrega un<select>en ProductoLista para filtrar productos por categoría. Al seleccionar una categoría, llama aGET /api/productos/?categoria=X.

Pista:AgregauseEffectque se ejecute cuando cambie el filtro de categoría

2
Intermedio

Paginación de Resultados

Implementa paginación en ProductoLista. Muestra 6 productos por página con botones "Anterior / Siguiente".

Pista:UsauseStateparapaginaActualy slice el array:productos.slice(inicio, fin)

3
Reto

Modal de Confirmación

Reemplaza elconfirm()nativo por un modal personalizado con animación CSS y botones estilizados.

Pista:Crea un componenteConfirmModalconcreatePortalde ReactDOM

¡Resumen Final: De Monolito a Microservicios!

Has recorrido el camino completo del desarrollo web: desde el monolito JSP (todo en un WAR, sesiones en servidor) hasta los microservicios FastAPI + React (servicios independientes, JWT stateless, SPA). Aquí tienes la evolución completa:

La psicología de este viaje de aprendizaje

Esta guía fue diseñada siguiendo principios de aprendizaje cognitivo: primero viste el problema (código espagueti), luego aprendiste la solución (MVC + JDBC), y finalmente aplicaste los mismos conceptos en un stack moderno (FastAPI + React). Este patrón de problema → solución → aplicación está respaldado por décadas de investigación en educación: cuando entiendes el por qué detrás de una tecnología, aprender el cómo se vuelve natural. No has aprendido dos stacks separados — has aprendido los fundamentos universales del desarrollo web y cómo se manifiestan en distintas tecnologías.

🏢 FASE 1: Monolito JSP + MVC

ProductoServlet.java@WebServlet → doGet/doPost
ProductoDAO.javaJDBC + PreparedStatement
lista.jspJSTL + Expression Language
LoginServlet.javaHttpSession (stateful)
http://localhost:8080/inventario-mvc-1.0/
🔗 Evolución a Microservicios

🔧 FASE 2: FastAPI Microservices

routers/auth.pyJWT + BCrypt + roles
routers/producto.pyCRUD con JWT
routers/usuario.pyCRUD solo Admin
models/*.pySQLAlchemy con FK
http://localhost:8000/docs (Swagger + Auth)
Consume API con JWT

🎨 FASE 3: React SPA + Auth

contexts/AuthContext.jsxEstado global + JWT
pages/Login.jsxLogin → JWT → localStorage
components/ProtectedRoute.jsxGuard por roles
services/api.jsAxios + Interceptor JWT
http://localhost:5173/ (SPA con auth)

🗺️ Mapa de relaciones entre tablas (PK/FK)

┌───────────┐       ┌──────────────┐       ┌──────────────┐
│   roles   │       │   usuarios   │       │  productos   │
├───────────┤       ├──────────────┤       ├──────────────┤
│ PK: id    │←──────│ FK: rol_id   │       │ PK: id       │
│ nombre    │  1:N  │ username     │       │ codigo       │
│ descrip.  │       │ password(bc) │       │ nombre       │
│ estado    │       │ email        │       │ precio       │
└───────────┘       │ estado       │       │ stock        │
                    │ fecha_creac. │       │ categoria    │
                    └──────────────┘       │ fecha_reg.   │
                       │ FK: usu_crea │←─┐  └──────────────┘
                       │                 │     (opcional)
                       │                 │     1 usuario crea
                       │                 │     muchos productos
                       │                 └────────────────────┘

✅ Para ejecutar tu proyecto completo:

1
PostgreSQL

Asegura que PostgreSQL esté corriendo con la BDinventario_db

psql -U postgres -c "CREATE DATABASE inventario_db;"
2
FastAPI (Backend) — Nativo o Docker

🐳 Docker:docker compose -f docker-compose.fullstack.yml up -d

🪟 Nativo: Terminal 1 inicia API en:8000

cd recursos/fastapi-inventario
venv\Scripts\activate
uvicorn main:app --reload --port 8000
3
React (Frontend) — Nativo o Docker

🐳 Docker: incluido en docker-compose (puerto 80)

🪟 Nativo: Terminal 2 inicia frontend en:5173

cd recursos/frontend-inventario
npm install
npm run dev
4
¡Abrir el navegador!

🐳 Docker:http://localhost

🪟 Nativo:http://localhost:5173

⚡ Script automático:.\start-fullstack.ps1

# Deberías ver el sistema de inventario
# con datos provenientes de FastAPI → PostgreSQL

Has migrado del stack Java empresarial tradicional (JSP + MVC + Tomcat) al stack moderno Python + JavaScript (FastAPI + React + Vite). Los conceptos son los mismos: Modelo-Vista-Controlador, CRUD, validación de datos. Lo que cambia es la sintaxis y las herramientas.

🎉 ¡Felicidades, ahora eres desarrollador Full Stack! 🎉

Has recorrido más de 56 horas de contenido, 13 secciones, y dominas dos stacks tecnológicos completos. Esto no es un logro menor — celebra tu progreso. La próxima vez que te enfrentes a una tecnología nueva, recuerda: ya tienes la base para aprender cualquier stack que aparezca. El viaje del desarrollador nunca termina, pero hoy has llegado muy lejos.

12

4. Despliegue con Docker

Dockerización y despliegue en Coolify

1. Generar archivo WAR

Terminal — Maven Build
mvn clean package -DskipTests
# Genera: target/inventario-mvc-1.0.war

2. Crear Dockerfile

Dockerfile
FROM tomcat:10.1-jdk21-temurin-jakarta
LABEL maintainer="ADSO SENA"
RUN rm -rf /usr/local/tomcat/webapps/*
COPY target/inventario-mvc-1.0.war /usr/local/tomcat/webapps/ROOT.war
EXPOSE 8080
CMD ["catalina.sh", "run"]

3. Construir Imagen Docker

Terminal — Docker Build
docker build -t inventario-mvc:1.0 .
# Verificar imagen
docker images | findstr inventario-mvc

4. Ejecutar Contenedor

Terminal — Docker Run
docker run -d -p 8080:8080 \
  -e DB_HOST=host.docker.internal \
  -e DB_PORT=5432 \
  -e DB_NAME=inventario_db \
  -e DB_USER=postgres \
  -e DB_PASSWORD=tu_password \
  --name inventario-app \
  inventario-mvc:1.0

# Ver logs
docker logs -f inventario-app

# Acceder: http://localhost:8080

7. Docker Compose — Microservicios

Arquitectura completa de microservicios donde cada servicio es independiente:Auth(:8001),Productos(:8002),Usuarios(:8003) yReact(:80).

docker-compose.microservices.yml
# Levantar 5 servicios (PostgreSQL + Auth + Products + Users + React)
docker compose -f docker-compose.microservices.yml up -d

# Verificar todos los servicios
docker compose -f docker-compose.microservices.yml ps

# Probar cada microservicio:
# Auth Service:     http://localhost:8001/docs
# Products Service: http://localhost:8002/docs
# Users Service:    http://localhost:8003/docs
# React Frontend:   http://localhost

# Detener todo
docker compose -f docker-compose.microservices.yml down

5. Ejecución Nativa en Windows

Antes de ejecutar, conoce elMakefile.ps1— un task runner con comandos comunes. Equivalente a scripts Maven pero para PowerShell.

PowerShell — Comandos disponibles
# Instalar todas las dependencias (Python + Node)
.\Makefile.ps1 install

# Iniciar FastAPI (desarrollo)
.\Makefile.ps1 api

# Iniciar React (desarrollo)
.\Makefile.ps1 frontend

# Ejecutar pruebas
.\Makefile.ps1 test

# Migraciones de base de datos
.\Makefile.ps1 db-create    # Crear BD
.\Makefile.ps1 db-migrate   # Aplicar migraciones
.\Makefile.ps1 db-revision  # Crear nueva migración

# Docker
.\Makefile.ps1 docker-up    # docker compose up -d
.\Makefile.ps1 docker-down  # docker compose down

# Estado de servicios
.\Makefile.ps1 status

# Ayuda completa
.\Makefile.ps1 help

5.2 Ejecución Manual (2 terminales)

Alternativa a Docker: ejecuta PostgreSQL, FastAPI y React directamente en Windows. Ideal para desarrollo y aprendizaje.

Componente 🐳 Docker (contenedores) 🪟 Windows Nativo
PostgreSQL docker compose up db Servicio de Windowspostgresql-x64-16
FastAPI docker compose up api uvicorn main:app --reload(PowerShell)
React docker compose up frontend npm run dev(PowerShell)
Comando único docker compose up -d .\start-fullstack.ps1

🪓 Opción 1: Script automático (recomendado)

PowerShell — Inicio rápido nativo
# ===== INICIO RÁPIDO NATIVO WINDOWS =====
# Requisitos previos (una sola vez):
#   1. PostgreSQL 16 instalado como servicio de Windows
#   2. Python 3.10+ instalado y en PATH
#   3. Node.js 18+ instalado
# --- Iniciar todo con un solo comando ---
.\start-fullstack.ps1
# --- Resultado esperado ---
# ✅ PostgreSQL is RUNNING (port 5432)
# ✅ FastAPI running on http://localhost:8000
# ✅ React running on http://localhost:5173
# 🌐 Open in browser: http://localhost:5173
# --- Detener todo ---
.\start-fullstack.ps1 -Stop
# --- Ver estado ---
.\start-fullstack.ps1 -Status

🖐️ Opción 2: Manual (tres terminales separadas)

Terminal 1: PostgreSQL + FastAPI
# ===== TERMINAL 1: BACKEND =====
# 1. Asegurar que PostgreSQL esté corriendo
Get-Service postgresql* | Start-Service
# 2. Verificar base de datos
psql -U postgres -c "CREATE DATABASE inventario_db;" 2>$null
# 3. Activar entorno virtual e iniciar FastAPI
cd recursos\fastapi-inventario
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r requirements.txt
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# ✅ FastAPI en: http://localhost:8000
# 📘 Swagger:    http://localhost:8000/docs
Terminal 2: React Frontend
# ===== TERMINAL 2: FRONTEND =====
cd recursos\frontend-inventario
npm install
npm run dev
# ✅ React en: http://localhost:5173
# Nota: Abre automáticamente el navegador

Tu aplicación está lista para el mundo real

Has pasado por todo el ciclo de vida: desarrollo local, contenerización con Docker, Docker Compose multi-servicio y despliegue en Coolify. Esto es exactamente cómo se despliega el software en la industria. La próxima vez que veas un docker compose up en un trabajo o proyecto open source, sabrás exactamente qué está pasando. Eso es poder.

🐳 vs 🪟: ¿Cuándo usar cada uno?

Usa Docker Producción, equipo, reproducibilidad, CI/CD, microservicios
Usa Nativo Desarrollo local, aprendizaje, debugging rápido, sin Docker instalado

6. Docker Compose — Full Stack

Levanta todo el stack moderno con un solo comando: PostgreSQL + FastAPI + React.

docker-compose.fullstack.yml
version: '3.8'

services:
  # ===== Base de datos =====
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: inventario_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: sena_adso_2026
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./recursos/sql/inventario_db.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - fullstack-net

  # ===== Backend FastAPI =====
  api:
    build:
      context: ./recursos/fastapi-inventario
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://postgres:sena_adso_2026@db:5432/inventario_db
    depends_on:
      db:
        condition: service_healthy
    networks:
      - fullstack-net

  # ===== Frontend React (producción) =====
  frontend:
    build:
      context: ./recursos/frontend-inventario
      dockerfile: Dockerfile
    ports:
      - "80:80"
    depends_on:
      - api
    networks:
      - fullstack-net

volumes:
  pgdata:

networks:
  fullstack-net:
    driver: bridge

Dockerfiles necesarios:

recursos/fastapi-inventario/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
recursos/frontend-inventario/Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Verificar despliegue

Terminal — Estado de servicios
# NAME                                      STATUS      PORTS
# fullstack-db-1                            healthy     5432/tcp
# fullstack-api-1                           running     0.0.0.0:8000->8000/tcp
# fullstack-frontend-1                      running     0.0.0.0:80->80/tcp

# Probar endpoints:
# FastAPI docs:   http://localhost:8000/docs
# React frontend: http://localhost
# Health check:   curl http://localhost:8000/health

# Detener todo
docker compose -f docker-compose.fullstack.yml down

# Detener y eliminar volúmenes (borra datos BD)
docker compose -f docker-compose.fullstack.yml down -v

8. Desplegar en Coolify

  1. Sube tu código a GitHub (incluye Dockerfile)
  2. Instala Coolify en tu VPS (coolify.io)
  3. Crea una nueva "Application" en Coolify
  4. Conecta tu repositorio de GitHub
  5. Coolify detecta automáticamente el Dockerfile
  6. Configura variables de entorno (DB_HOST, etc.)
  7. Haz clic en "Deploy" y espera
  8. Accede a tu aplicación en la URL proporcionada
13

5. Evidencias de Aprendizaje

Demuestra que has alcanzado los resultados de aprendizaje

Evidencia de Conocimiento

Cuestionario escrito individual

  • Explicar el patrón MVC
  • Diferencia entre javax y jakarta
  • Ventajas de records de JDK 21
  • Prevención de inyección SQL
  • Flujo completo de una petición
  • Función de JSTL en JSP
  • Dockerización de aplicaciones
  • Arquitectura REST con FastAPI
  • Componentes y Hooks de React
20% de la nota final

Evidencia de Desempeño

Video de sustentación (10 min)

  • Mostrar aplicación funcionando
  • Explicar arquitectura MVC
  • Demostrar uso de JSTL
  • Explicar conexión JDBC
  • Mostrar script SQL
  • Explicar 2 ejercicios resueltos
  • Demostrar despliegue Docker
30% de la nota final

Evidencia de Producto

Repositorio en GitHub

  • Código fuente completo Maven
  • Archivo pom.xml configurado
  • Script SQL de base de datos
  • Dockerfile funcional
  • README.md con instrucciones
  • Archivo WAR compilado
  • Capturas de pantalla
  • Ejercicios de transferencia
50% de la nota final

Rúbrica de Evaluación

Criterio Excelente (100%) Aceptable (70%) Insuficiente (40%)
Estructura MVC Separación clara de capas Separación parcial Capas mezcladas
Uso de JSTL Sin scriptlets, uso correcto Algunos scriptlets menores Uso extensivo de scriptlets
CRUD Completo 3 tablas con CRUD funcional 2 tablas con CRUD CRUD incompleto
Seguridad PreparedStatement + BCrypt PreparedStatement sin BCrypt SQL vulnerable
Dockerización Dockerfile funcional + compose Solo Dockerfile Sin Docker
API FastAPI CRUD completo con SQLAlchemy Endpoints básicos API incompleta
Frontend React Componentes + Axios + Router Solo componentes básicos Sin React
Ejercicios 3 ejercicios completos 2 ejercicios 1 o ninguno

Has llegado al final de esta guía, pero esto no es un adiós — es un hito en tu carrera como desarrollador. Has construido aplicaciones web completas en dos stacks tecnológicos distintos, entendiendo no solo el código sino los principios fundamentales que los sustentan.

🚀 Gracias por el esfuerzo y la dedicación. Esto es solo el comienzo.

Recuerda: los mejores desarrolladores no son los que nunca fallan, sino los que persisten, experimentan y aprenden de cada error. Lleva contigo esa mentalidad a tu próximo proyecto, tu próximo trabajo, tu próximo desafío. El mundo de la tecnología está lleno de problemas esperando ser resueltos — y ahora tienes las herramientas para resolverlos.

★ SENA ADSO — Generación de desarrolladores que transforman Colombia ★