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.
@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
Jakarta EE 10
Estándar empresarial con Servlet 6.0 y JSP 3.1
PostgreSQL 16
Base de datos relacional robusta y de código abierto
Tomcat 10.1
Servidor de aplicaciones compatible con Jakarta EE 10
Maven 3.9
Gestión de dependencias y automatización de builds
Docker
Contenedores para despliegue portable y reproducible
FastAPI
Framework moderno para APIs REST con Python 3.10+
React 19
Biblioteca moderna para interfaces de usuario SPA
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.
Actualizaciones de seguridad por 8 años
Hasta 40% más rápido que JDK 11
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
Visitaadoptium.nety descarga Eclipse Temurin JDK 21 para tu sistema operativo
# 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
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- Usa
requestpara 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.
Transacciones seguras y confiables
Soporta terabytes de datos
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
Visitapostgresql.org/downloady descarga el instalador para tu sistema
Durante la instalación, establece una contraseña para el usuariopostgres. Recuerda esta contraseña para la conexión JDBC.
# 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.
Soporta Servlet 6.0, JSP 3.1, WebSocket 2.1
Inicio en segundos, bajo consumo de memoria
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.
Descarga librerías automáticamente desde Maven Central
Convención sobre configuración
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
Visitamaven.apache.org/downloady descarga la versión binaria
# 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
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.
"Funciona en mi máquina" = funciona en todas
Cada aplicación en su propio contenedor
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
Visitadocker.com/products/docker-desktopy descarga para tu sistema operativo
Ejecuta el instalador y reinicia tu computador. Docker Desktop incluye Docker Engine, Docker CLI y Docker Compose.
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 comandos
RUNcon&& - Usa .dockerignore- Excluye
node_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.
Tan rápido como Node.js o Go (gracias a Starlette + Uvicorn)
Pydantic valida tipos y datos automáticamente
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
Descarga desdepython.orgy verifica:python --version
# 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
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.
UI encapsulada y reutilizable (como tags JSP personalizados)
useState, useEffect para datos dinámicos
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
Descarga desdenodejs.orgy verifica:node --version
# 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
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
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
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 + DAOControlador
Procesa peticiones HTTP
ServletsVista
Interfaz de usuario
JSP + JSTLObjetivos 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
3.1 Reflexión Inicial
El problema del código espagueti y por qué MVC lo soluciona
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í:
<%@ 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
¿Puedes identificar al menos 3 problemas de seguridad en este código?
¿Qué pasaría si necesitas cambiar la base de datos de PostgreSQL a MySQL? ¿Cuántos archivos tendrías que modificar?
¿Cómo harías para probar la lógica de negocio sin tener un servidor web funcionando?
Si dos desarrolladores necesitan trabajar en este archivo al mismo tiempo, ¿qué problemas podrían surgir?
¿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.
3.2 Fundamentos Teóricos
Conceptos clave antes de programar — MVC, JSP, Jakarta EE, JDBC, Records, CDI
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.javaVista
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.jspControlador
Recibe las peticiones HTTP, interpreta los parámetros, invoca al modelo y selecciona la vista. Es el orquestador del flujo. Implementado con Servlets.
ProductoServlet.javaFlujo de una Petición MVC (Crear Producto)
Usuario llena el formulario y hace clic en "Guardar"
POST /productos con accion=guardar → ProductoServlet.doPost()
Servlet extrae parámetros (codigo, nombre, precio) del request
Servlet invoca ProductoDAO.insertar(producto) → PreparedStatement
DAO ejecuta INSERT en PostgreSQL y retorna resultado
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:
Traducción: JSP → Servlet (.java)
Compilación: Servlet.java → .class
Carga: ClassLoader carga el Servlet
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:
<%@ 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)
<%@ 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:
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:
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:
// 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
Servlet / DAO
Connection, Statement, ResultSet
org.postgresql.Driver
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 |
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:
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: 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):
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:
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:
// 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
Configuración de PostgreSQL
Conecta tu aplicación a la base de datos existente
Verificar que la BD existe
# 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
Configurar credenciales en db.properties
Edita el archivosrc/main/resources/db.propertiescon tus credenciales reales:
# 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
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)
Probar la conexión
Crea una clase de prueba para verificar que todo funciona:
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());
}
}
}
Variables de entorno (Producción)
Para producción, usa variables de entorno en lugar de hardcodear credenciales:
# 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
¡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
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;
}
}
}
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
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)
<!-- 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 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") |
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
# 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()
@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
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.
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
Simuladores Interactivos
Experimenta con la arquitectura MVC de forma visual
Simulador de Flujo MVC
Observa cómo viaja una petición HTTP a través de las capas: Vista → Controlador → Modelo → Base de Datos
Simulador CRUD Interactivo
Practica Crear, Leer, Actualizar y Eliminar productos en una tabla simulada con logs SQL
Simulador de Inyección SQL
Aprende cómo prevenir ataques de inyección SQL usando PreparedStatement. Ve la diferencia entre código vulnerable y seguro.
Ciclo de Vida del Servlet
Visualiza las fases del ciclo de vida de un Servlet: init(), service(), destroy()
Comparador JDBC vs ORM
Compara lado a lado el código JDBC puro con JPA/Hibernate y entiende las diferencias
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 usuarioFlujo 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 │ │
│←───────────────────────────│ │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!
Paso 1: Crear la estructura del 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)
<?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)
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
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
# 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.
Configuración del Entorno
Instalación de herramientas necesarias
Herramientas Requeridas
Verificar Instalaciones
# 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
# Navegar al directorio de Tomcat
cd C:\apache-tomcat-10.1.x\bin
# Iniciar servidor
startup.bat
# Verificar en: http://localhost:8080
Base de Datos PostgreSQL
Crear tablas y datos de prueba
Crear Base de Datos
-- 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:
-- 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');
Estructura del Proyecto Maven
Organización estándar de directorios
Estructura de Directorios
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
<?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>
Modelo - Records de JDK 21
Entidades inmutables con sintaxis moderna
Producto.java - Record Principal
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());
}
}
DAO - Acceso a Datos con JDBC
Patrón DAO con PreparedStatement
ProductoDAO.java - CRUD Completo
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;
}
}
}
Controlador - Servlets Jakarta EE
Manejo de peticiones HTTP con @WebServlet
ProductoServlet.java - CRUD Completo
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");
}
}
Vista - JSP con JSTL
Páginas dinámicas sin código Java
lista.jsp - Tabla de Productos con JSTL
<%@ 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>
Compilación y Despliegue
Arrancar con Maven Cargo o Script Automatizado
Opción A: Ejecutar usando el script interactivo (Recomendado)
# 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
# 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:
# 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
3.4 Ejercicios de Transferencia
Ejercicios retadores para demostrar tu dominio
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étodo
buscarPorNombre(String)en ProductoDAO - Usar
LIKEen 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 + "%")
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 tabla
categoriasen PostgreSQL - Crear record
Categoria.java - Crear
CategoriaDAO.javacon CRUD completo - Modificar tabla productos: agregar
categoria_idFK - Crear
CategoriaServlet.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
Sistema de Auditoría con Filtros
Implementa un sistema de auditoría que registre todas las operaciones CRUD con filtros avanzados.
Requisitos:
- Crear tabla
auditoria(tabla, operacion, usuario, fecha, datos) - Crear record
Auditoria.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
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
Primer Ejemplo: Hola Mundo con FastAPI
Tu primer endpoint API REST — ¡en 2 minutos!
Paso 1: Crear el proyecto
# 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")
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:
# 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:
# 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
# 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.
Configuración del Proyecto FastAPI
Estructura y dependencias
Estructura de directorios
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)
# 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 (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
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
# 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
Modelo SQLAlchemy + Esquema Pydantic
Equivalente a los Records de JDK 21
Modelo de Base de Datos (SQLAlchemy)
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)
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
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()
CRUD Endpoints — Productos
Equivalente a ProductoServlet.java
Router de Productos (GET, POST, PUT, DELETE)
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)
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"}
Ejecutar y Probar la API
Inicia el servidor y prueba los endpoints
Iniciar el servidor
# 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 — 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)
# 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
# ─────────────────────────────────────────────
# 🐳 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:
# 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:
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:
# 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()
¡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.
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
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
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
Frontend con React 19 + Vite + Auth JWT
Consume la API de FastAPI con autenticación JWT, rutas protegidas y control de roles
Primer Ejemplo: Hola Mundo con React
Tu primer componente React — ¡en 2 minutos!
Paso 1: Crear proyecto con Vite
🪟 Nativo Windows:
# 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):
# 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
// 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
┌──────────────────────────────────────────────────────────────┐
│ │
│ ¡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
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.
Crear Proyecto React con Vite
Configuración inicial y estructura
Crear proyecto
# 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
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
# 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
┌──────────┐ 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
# 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
Servicio API con Axios
Equivalente a ProductoDAO.java pero en JavaScript
api.js — Capa de servicio
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}`);
};
Componente ProductoCard
Renderiza cada producto — como un fragmento JSP con JSTL
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
.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
┌──────────────────────────────────────────────┐
│ 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] │
└──────────────────────────────────────────────┘
Componente ProductoLista
Lista todos los productos desde la API — como lista.jsp
ProductoLista.jsx — Consume GET /api/productos/
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
┌──────────────────────────────────────────────────────────────┐
│ 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] │ │
│ └──────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Componente ProductoForm
Crear y editar productos — como formulario.jsp
ProductoForm.jsx — POST y PUT a FastAPI
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
┌──────────────────────────────────────────────────┐
│ 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 ] │
└──────────────────────────────────────────────────┘
App.jsx — Rutas y Navegación
Componente raíz con React Router — como index.jsp + web.xml
App.jsx — Enrutador principal
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
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
┌──────────────────────────────────────────────────────────────┐
│ [■ 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 │
│ │
└──────────────────────────────────────────────────────────────┘
Ejecutar Todo el Stack
FastAPI + React trabajando juntos
Terminal 1: Iniciar FastAPI (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)
# 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
┌─────────────────────────────────────────────────────────────┐
│ 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
# ===========================================================
# 🐳 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.
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
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)
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:
🏢 FASE 1: Monolito JSP + MVC
ProductoServlet.java@WebServlet → doGet/doPost
ProductoDAO.javaJDBC + PreparedStatement
lista.jspJSTL + Expression Language
LoginServlet.javaHttpSession (stateful)
🔧 FASE 2: FastAPI Microservices
routers/auth.pyJWT + BCrypt + roles
routers/producto.pyCRUD con JWT
routers/usuario.pyCRUD solo Admin
models/*.pySQLAlchemy con FK
🎨 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
🗺️ 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:
Asegura que PostgreSQL esté corriendo con la BDinventario_db
psql -U postgres -c "CREATE DATABASE inventario_db;"
🐳 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
🐳 Docker: incluido en docker-compose (puerto 80)
🪟 Nativo: Terminal 2 inicia frontend en:5173
cd recursos/frontend-inventario
npm install
npm run dev
🐳 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.
4. Despliegue con Docker
Dockerización y despliegue en Coolify
1. Generar archivo WAR
mvn clean package -DskipTests
# Genera: target/inventario-mvc-1.0.war
2. Crear 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
docker build -t inventario-mvc:1.0 .
# Verificar imagen
docker images | findstr inventario-mvc
4. 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
# 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).
# 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.
# 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)
# ===== 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: 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: 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.
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:
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"]
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
# 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
- Sube tu código a GitHub (incluye Dockerfile)
- Instala Coolify en tu VPS (coolify.io)
- Crea una nueva "Application" en Coolify
- Conecta tu repositorio de GitHub
- Coolify detecta automáticamente el Dockerfile
- Configura variables de entorno (DB_HOST, etc.)
- Haz clic en "Deploy" y espera
- Accede a tu aplicación en la URL proporcionada
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
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
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
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 ★