Usar fetch con Servlets para enviar parámetros POST y devolver HTML

 

En el artículo anterior vimos que es posible construir aplicaciones web dinámicas con Java, Servlets, Thymeleaf y JavaScript sin necesidad de incorporar obligatoriamente un framework frontend como React o Angular.

En esta entrada vamos a llevar esa idea a un ejemplo práctico.

Si estamos trabajando con Servlets, Thymeleaf y JavaScript, podemos utilizar fetch() para enviar datos al servidor y hacer que el Servlet responda directamente con HTML.

En este ejemplo vamos a implementar una búsqueda dinámica de productos.

La idea es sencilla:

Usuario escribe el nombre
        ↓
JavaScript ejecuta fetch()
        ↓
Servlet recibe parámetros POST
        ↓
ProductoService realiza la búsqueda
        ↓
Servlet genera HTML
        ↓
fetch recibe el HTML
        ↓
Se reemplaza una sección de la página

La página inicial

La página de administración de productos se genera inicialmente utilizando Thymeleaf.

Tenemos un campo de búsqueda:

<input
    oninput="cargarSeccion('BUSCAR_NOMBRE', this.value);"
    type="text"
    class="form-control"
    name="nombre"
    placeholder="Buscar por nombre">

Aquí estamos utilizando el evento:

oninput

Esto significa que cada vez que cambia el contenido del campo se ejecuta:

cargarSeccion('BUSCAR_NOMBRE', this.value);

El primer parámetro indica la operación que queremos realizar:

BUSCAR_NOMBRE

El segundo corresponde al texto que está escribiendo el usuario:

this.value

Por ejemplo, si el usuario escribe:

monitor

la llamada será equivalente a:

cargarSeccion("BUSCAR_NOMBRE", "monitor");

La función JavaScript

La función encargada de enviar los datos al Servlet es la siguiente:

function cargarSeccion(parametro1, parametro2) {

    const datos = new URLSearchParams();

    datos.append("ACC", parametro1);
    datos.append("productoNom", parametro2);

    fetch("adminProducto", {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        body: datos
    })
    .then(response => {

        if (!response.ok) {
            throw new Error(
                "Error HTTP: " + response.status
            );
        }

        return response.text();
    })
    .then(html => {

        document
            .getElementById("contenido")
            .innerHTML = html;

    })
    .catch(error => {

        console.error(
            "Error:",
            error
        );
    });
}

Vamos a revisar qué está haciendo.

Crear los parámetros POST

Primero creamos un objeto:

const datos = new URLSearchParams();

URLSearchParams nos permite construir los parámetros que enviaremos al servidor.

Agregamos dos parámetros:

datos.append("ACC", parametro1);
datos.append("productoNom", parametro2);

La solicitud resultante conceptualmente contiene algo similar a:

ACC=BUSCAR_NOMBRE&productoNom=monitor

Es importante que estos nombres coincidan con los utilizados posteriormente por el Servlet.

Enviar la petición con fetch

La petición se realiza contra:

fetch("adminProducto", {

y especificamos que será mediante POST:

method: "POST"

También indicamos el tipo de contenido:

headers: {
    "Content-Type":
        "application/x-www-form-urlencoded"
}

Finalmente enviamos los parámetros:

body: datos

Por lo tanto, JavaScript está enviando una petición similar a un formulario tradicional, pero sin recargar completamente la página.

Recibir los parámetros en el Servlet

En el método doPost() del Servlet recuperamos primero la empresa almacenada en sesión:

int empresaid =
    (int) request
        .getSession()
        .getAttribute("empresaid");

Luego creamos el servicio correspondiente:

ProductoService productoService =
        new ProductoService(empresaid);

Y obtenemos la operación enviada desde JavaScript:

String acc =
        request.getParameter("ACC");

El nombre:

ACC

es exactamente el mismo que utilizamos aquí:

datos.append("ACC", parametro1);

Utilizar un switch para determinar la operación

Ahora podemos decidir qué acción ejecutar:

ArrayList<Producto> listaProducto =
        new ArrayList<>();

switch (acc) {

    case "BUSCAR_NOMBRE" ->
        listaProducto =
            productoService.searchNom(
                request.getParameter(
                    "productoNom"));
}

También podemos observar la correspondencia del segundo parámetro.

JavaScript envía:

datos.append(
    "productoNom",
    parametro2
);

y Java recupera:

request.getParameter(
    "productoNom"
)

Por lo tanto tenemos:

JavaScript                     Servlet

ACC              -----------> getParameter("ACC")

productoNom      -----------> getParameter("productoNom")

Esta correspondencia es fundamental.

Buscar los productos

La búsqueda se delega a:

productoService.searchNom(...)

De esta manera el Servlet no necesita conocer cómo se realiza directamente la consulta SQL.

El flujo queda:

AdminProducto
      ↓
ProductoService
      ↓
DAO
      ↓
Base de datos

El resultado se almacena en:

ArrayList<Producto> listaProducto

Construir HTML desde Java

Una vez obtenidos los productos, el Servlet construye la tabla.

Primero creamos:

StringBuilder html =
        new StringBuilder();

Luego agregamos la estructura:

html.append(
    "<table class=\"table table-striped table-bordered\">"
);

html.append(
    "<thead class=\"table-dark\">"
);

html.append("""
    <tr>
        <th>ID</th>
        <th>Nombre</th>
        <th>Precio</th>
        <th>Acciones</th>
    </tr>
    """);

html.append(
    "</thead><tbody>"
);

Después recorremos los productos:

for (Producto producto : listaProducto) {

    html.append("<tr>");

    html.append("<td>")
        .append(
            String.valueOf(
                producto.getProductoid()
            )
        )
        .append("</td>");

    html.append("<td>")
        .append(
            String.valueOf(
                producto.getProductonom()
            )
        )
        .append("</td>");

    html.append("<td>")
        .append(
            String.valueOf(
                producto.getPrecioventa()
            )
        )
        .append("</td>");

    html.append("<td>");

    html.append(
        "<button type=\"button\" " +
        "class=\"btn btn-warning\">" +
        "Editar</button>"
    );

    html.append(
        "<button type=\"button\" " +
        "class=\"btn btn-danger\">" +
        "Eliminar</button>"
    );

    html.append("</td>");
    html.append("</tr>");
}

Finalmente cerramos la tabla:

html.append(
    "</tbody></table>"
);

El Servlet devuelve HTML

Aquí está uno de los puntos centrales del ejemplo.

La respuesta no será JSON.

Indicamos:

response.setContentType(
    "text/html;charset=UTF-8"
);

y enviamos:

response
    .getWriter()
    .write(
        html.toString()
    );

Por lo tanto el navegador recibe directamente algo como:

<table class="table table-striped table-bordered">

    <thead class="table-dark">
        <tr>
            <th>ID</th>
            <th>Nombre</th>
            <th>Precio</th>
            <th>Acciones</th>
        </tr>
    </thead>

    <tbody>

        <tr>
            <td>12</td>
            <td>Monitor Samsung</td>
            <td>129990</td>
            <td>
                <button
                    type="button"
                    class="btn btn-warning">
                    Editar
                </button>

                <button
                    type="button"
                    class="btn btn-danger">
                    Eliminar
                </button>
            </td>
        </tr>

    </tbody>

</table>

¿Por qué usamos response.text()?

Como el Servlet está devolviendo:

text/html

JavaScript debe interpretar la respuesta como texto.

Por eso utilizamos:

return response.text();

No utilizamos:

response.json();

porque el servidor no está enviando JSON.

El resultado pasa entonces al siguiente bloque:

.then(html => {

La variable:

html

contiene todo el HTML construido por Java.

Reemplazar solamente una parte de la página

Nuestra página tiene este contenedor:

<div id="contenido">

    <table class="table table-striped table-bordered">

        <!-- tabla inicial generada por Thymeleaf -->

    </table>

</div>

Cuando fetch() recibe el nuevo contenido, ejecutamos:

document
    .getElementById("contenido")
    .innerHTML = html;

Esto reemplaza únicamente el contenido dentro del div.

Por lo tanto:

antes
<div id="contenido">

    tabla inicial

</div>

se transforma en:

después
<div id="contenido">

    tabla devuelta por el Servlet

</div>

El resto de la página permanece intacto.

No hay una recarga completa.

La página inicial sigue utilizando Thymeleaf

Una característica interesante de esta implementación es que estamos utilizando dos formas de renderizado.

Al entrar inicialmente a la página, doGet() obtiene los productos:

ArrayList<Producto> arrayproducto =
        objProductoService
            .obtenerProductos();

Luego los agrega al contexto:

context.setVariable(
    "productos",
    arrayproducto
);

y Thymeleaf genera la vista:

String contenido =
        templateEngine.process(
            "/productoview/adminproducto",
            context
        );

En el HTML tenemos:

<tr th:each="producto : ${productos}">

Por lo tanto la primera carga funciona de forma tradicional:

GET
 ↓
Servlet
 ↓
Thymeleaf
 ↓
HTML completo
 ↓
Navegador

Pero la búsqueda funciona de forma asíncrona:

oninput
 ↓
fetch POST
 ↓
Servlet
 ↓
HTML parcial
 ↓
innerHTML

Estamos utilizando ambos modelos dentro de la misma aplicación.

No todo tiene que ser JSON

Cuando se habla de fetch(), muchas veces los ejemplos muestran inmediatamente una API que devuelve:

{
    "id": 10,
    "nombre": "Monitor",
    "precio": 129990
}

Pero fetch() no exige JSON.

Puede recibir perfectamente:

JSON
XML
HTML
texto

En este caso resulta bastante práctico retornar directamente HTML porque nuestro objetivo final precisamente es actualizar una parte del documento.

Tampoco necesitamos un framework frontend

Para este comportamiento utilizamos solamente:

HTML
Bootstrap
JavaScript
fetch
Servlets
Thymeleaf
JDBC

El navegador ya tiene las herramientas necesarias para realizar una petición asíncrona.

No necesitamos obligatoriamente introducir:

React
Angular
Vue
Node.js
npm

para implementar una búsqueda dinámica.

Esto puede ser especialmente útil en sistemas administrativos, CRUD, POS y aplicaciones internas donde queremos mantener una arquitectura sencilla.

Un detalle: oninput genera varias peticiones

Existe un aspecto que debemos considerar.

Estamos utilizando:

oninput="cargarSeccion(
    'BUSCAR_NOMBRE',
    this.value
);"

Esto ejecuta una petición cada vez que el usuario modifica el campo.

Si escribimos:

MONITOR

podrían enviarse consultas para:

M
MO
MON
MONI
MONIT
MONITO
MONITOR

Para una aplicación pequeña esto puede ser perfectamente aceptable.

Si el número de usuarios o consultas aumenta, podemos implementar posteriormente un mecanismo llamado:

debounce

para esperar algunos milisegundos después de que el usuario deje de escribir antes de realizar la consulta.

Separación de responsabilidades

El ejemplo además mantiene una estructura bastante clara.

El HTML contiene:

estructura visual

JavaScript maneja:

interacción del navegador

El Servlet maneja:

la solicitud HTTP

ProductoService maneja:

la lógica relacionada con productos

y la capa de datos maneja:

SQL y persistencia

El flujo completo queda:

HTML
 ↓
JavaScript
 ↓
fetch
 ↓
AdminProducto Servlet
 ↓
ProductoService
 ↓
DAO
 ↓
MySQL

y la respuesta vuelve:

MySQL
 ↓
Producto
 ↓
Servlet
 ↓
HTML
 ↓
fetch
 ↓
innerHTML

Conclusión

Con unas pocas líneas de JavaScript podemos agregar comportamiento dinámico a una aplicación Java tradicional.

Nuestra función central fue simplemente:

function cargarSeccion(parametro1, parametro2) {

    const datos = new URLSearchParams();

    datos.append(
        "ACC",
        parametro1
    );

    datos.append(
        "productoNom",
        parametro2
    );

    fetch("adminProducto", {

        method: "POST",

        headers: {
            "Content-Type":
                "application/x-www-form-urlencoded"
        },

        body: datos

    })
    .then(response => {

        if (!response.ok) {

            throw new Error(
                "Error HTTP: " +
                response.status
            );
        }

        return response.text();

    })
    .then(html => {

        document
            .getElementById(
                "contenido"
            )
            .innerHTML = html;

    })
    .catch(error => {

        console.error(
            "Error:",
            error
        );

    });
}

Dos parámetros entran al Servlet:

ACC
productoNom

el Servlet ejecuta la búsqueda y devuelve:

HTML

y JavaScript reemplaza:

<div id="contenido">

sin tener que recargar toda la página.

Es una forma bastante directa de agregar interactividad a una aplicación basada en Servlets sin transformar toda la arquitectura en una aplicación frontend separada.

Comentarios

Entradas populares de este blog

Boleta Electrónica en Chile: Los 3 Ambientes del SII para Desarrolladores

Configurando Servlets y JSP en Jetty

GUIA GENERAL DE GENERACION DE DOCUMENTOS TRIBUTARIOS ELECTRONICOS