← El cliente del proyecto: navegador y CORS

Sesión 34 · Semana 17

Integración del navegador antes de la seguridad

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado revisar y publicar un contrato compatible. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

El navegador ya puede consultar la API con la configuración CORS elegida. Hoy completará escrituras y mostrará errores del servidor. fetch devuelve una respuesta también para estados 400 o 500; el cliente debe comprobar su estado antes de tratarla como éxito.

El método de diagnóstico en tres capas

Cuando un sistema compuesto por un cliente web y un servidor backend falla, el impulso del programador novato es cambiar líneas de código al azar: toca el controlador, luego el JavaScript, luego el HTML, y acaba creando cinco bugs nuevos sin resolver el original.

Un ingeniero de software profesional aplica el árbol de diagnóstico de tres capas:

El árbol de diagnóstico forense de tres capas
  1. Capa 1: Cliente (Consola JS)
  2. Capa 2: Red (DevTools Network)
  3. Capa 3: Servidor (Logs Spring Boot)
  1. Capa 1 · El Cliente (Consola de JavaScript):
    • ¿Hay errores de sintaxis en el script? ¿Falló una referencia a un elemento del DOM (null is not an object)?
    • Diagnóstico: Abrir la pestaña Console. Si hay texto rojo en JavaScript, el problema está en el cliente antes de emitir la petición.
  2. Capa 2 · La Red y el Navegador (DevTools Network):
    • ¿Llegó a salir la petición HTTP? ¿Qué método, ruta y cuerpo exacto envió?
    • ¿Qué código HTTP respondió el servidor (201, 400, 404, 409, 500)?
    • Diagnóstico: Abrir la pestaña Network. Si la petición aparece en rojo o con error de CORS, el fallo está en la comunicación.
  3. Capa 3 · El Servidor (Consola de Spring Boot):
    • Si la petición llegó pero devolvió 500, ¿qué excepción se imprimió en la terminal del backend (NullPointerException, DataIntegrityViolationException)?
    • Si devolvió 400, ¿qué regla de Bean Validation se activó?

La respuesta llega antes de que termine la interfaz

Una llamada con fetch es asíncrona. La interfaz puede estar esperando, mostrar datos, mostrar una lista vacía o informar de un error. Esos estados pertenecen a la aplicación cliente y deben corresponderse con el resultado de la API. Una lista vacía con estado 200 no significa lo mismo que una petición que nunca llegó al servidor.

fetch no rechaza su promesa solo porque la API devuelva 400 o 500. Primero hay que comprobar el estado o response.ok y después interpretar el cuerpo que corresponda. Si la API devuelve el detalle de validación, el cliente puede asociarlo al campo del formulario; sustituirlo siempre por un mensaje genérico elimina información útil.

Hoy se utiliza el portfolio que ya está publicado. Tras una escritura se vuelve a consultar el listado o se actualiza su estado de manera coherente. Para comprobarlo se provoca un error de validación, una URL incorrecta y un servidor apagado, y se observa qué aparece en la pestaña de red y en pantalla. Esa base permitirá distinguir los nuevos rechazos de autenticación cuando se incorpore la seguridad.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre el cliente existente, el controlador de altas y su DTO. Ejecuta un listado desde el navegador antes de cambiar el formulario.
  2. Anota los nombres y tipos de los campos que acepta el POST. Haz que el formulario construya ese contrato exacto.
  3. Prepara tres escenarios: alta válida, datos rechazados y servidor detenido. Son resultados diferentes que el usuario debe poder distinguir.

Paso 2 · Formulario de alta con validación visual

Añade el HTML del formulario dentro de body y coloca su script después de esos elementos, o ejecútalo cuando el documento esté cargado. Conserva el listado existente. Comprueba que los ids usados por getElementById coinciden con los del HTML. En el evento submit, evita la recarga, construye un objeto con las claves del DTO, conviértelo con JSON.stringify y envía el POST. Solo después de una respuesta correcta actualiza la lista y limpia el formulario.

Añadimos un formulario con campos para el nombre y la descripción, y contenedores dedicados para mostrar mensajes de error:

<section style="margin-top: 2rem; border-top: 1px solid #e4e4e7; padding-top: 1.5rem;">
  <h2>Crear nuevo proyecto</h2>
  <form id="form-proyecto">
    <div>
      <label for="nombre">Nombre del proyecto (*):</label><br>
      <input type="text" id="nombre" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;">
      <small id="error-nombre" style="color: #ef4444; display: none;"></small>
    </div>
    <div style="margin-top: 1rem;">
      <label for="descripcion">Descripción:</label><br>
      <textarea id="descripcion" rows="3" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;"></textarea>
      <small id="error-descripcion" style="color: #ef4444; display: none;"></small>
    </div>
    <div style="margin-top: 1rem;">
      <button type="submit" id="btn-guardar" style="background: #2563eb; color: white; border: none; padding: 0.6rem 1.2rem; border-radius: 4px; cursor: pointer;">
        Guardar proyecto
      </button>
    </div>
    <p id="mensaje-global" style="margin-top: 1rem; display: none;"></p>
  </form>
</section>

En el script de index.html, escuchamos el envío del formulario, evitamos la recarga tradicional de la página con e.preventDefault() y enviamos el JSON:

const form = document.getElementById('form-proyecto');
const inputNombre = document.getElementById('nombre');
const inputDesc = document.getElementById('descripcion');
const errorNombre = document.getElementById('error-nombre');
const mensajeGlobal = document.getElementById('mensaje-global');

form.addEventListener('submit', async (e) => {
  e.preventDefault(); // Evita que el navegador recargue la página

  // Limpiamos mensajes de error previos
  errorNombre.style.display = 'none';
  mensajeGlobal.style.display = 'none';

  const nuevoProyecto = {
    nombre: inputNombre.value.trim(),
    descripcion: inputDesc.value.trim()
  };

  try {
    const res = await fetch('http://localhost:8080/api/v1/proyectos', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(nuevoProyecto)
    });

    if (res.status === 201) {
      // Caso 1: Alta exitosa
      mensajeGlobal.textContent = '¡Proyecto creado con éxito!';
      mensajeGlobal.style.color = '#16a34a';
      mensajeGlobal.style.display = 'block';
      form.reset();
      obtenerProyectos(); // Actualizamos la lista automáticamente

    } else if (res.status === 400) {
      // Caso 2: Error de validación RFC 7807
      const errorData = await res.json();
      if (errorData.invalidParams && errorData.invalidParams.nombre) {
        errorNombre.textContent = errorData.invalidParams.nombre;
        errorNombre.style.display = 'block';
      } else {
        mensajeGlobal.textContent = errorData.detail || 'Datos de entrada inválidos';
        mensajeGlobal.style.color = '#ef4444';
        mensajeGlobal.style.display = 'block';
      }

    } else if (res.status === 409) {
      // Caso 3: Conflicto de unicidad
      const errorData = await res.json();
      mensajeGlobal.textContent = errorData.detail || 'Ya existe un proyecto con ese nombre';
      mensajeGlobal.style.color = '#f59e0b';
      mensajeGlobal.style.display = 'block';

    } else {
      throw new Error(`Error inesperado del servidor: HTTP ${res.status}`);
    }

  } catch (err) {
    mensajeGlobal.textContent = 'No se pudo contactar con el servidor. Revisa tu conexión.';
    mensajeGlobal.style.color = '#dc2626';
    mensajeGlobal.style.display = 'block';
  }
});

Paso 3 · El simulacro de los tres fallos provocados

Para dominar el diagnóstico de integración, vamos a provocar intencionadamente tres errores típicos y comprobar cómo reaccionan las tres capas:

Prueba Qué hacemos en el navegador Qué debe mostrar DevTools (Red) Qué debe mostrar la UI
1 · El alta limpia Escribimos "App Clientes" y pulsamos guardar. Petición POST con código 201 Created y cabecera Location. Mensaje verde de éxito y la lista se actualiza al instante con el nuevo proyecto.
2 · La validación fallida Dejamos el nombre vacío y pulsamos guardar. Petición POST con código 400 Bad Request y JSON RFC 7807. El texto rojo “El nombre no puede estar en blanco” aparece bajo el input.
3 · El conflicto de duplicado Volvemos a escribir "App Clientes" idéntico. Petición POST con código 409 Conflict. Mensaje ámbar “Ya existe un proyecto con ese nombre”.
4 · El servidor apagado Detenemos Spring Boot y pulsamos guardar. Petición (failed) en rojo con tipo net::ERR_CONNECTION_REFUSED. Mensaje rojo “No se pudo contactar con el servidor”.

Paso 4 · Si algo no sale como dice el guion

Síntoma Dónde está el problema Qué mirar
415 Unsupported Media Type Cliente Falta la cabecera Content-Type: application/json en las opciones del fetch
400 con JSON parse error Cliente Se envía el objeto sin serializar en lugar de convertirlo a texto JSON
400 con la lista de campos inválidos Servidor, y funcionando bien Es tu Bean Validation de la UD3 haciendo su trabajo: muestra el detail en pantalla
El POST responde 201 pero la lista no cambia Cliente Has creado el recurso pero no has vuelto a pintar la lista
204 y el elemento sigue en pantalla Cliente El 204 no trae cuerpo: no intentes hacer res.json() con él, reventaría
El OPTIONS aparece y el POST no Navegador El preflight fue rechazado: revisa la configuración de la sesión 33

Paso 5 · Cerrar el ciclo completo desde el navegador

Para editar, selecciona un registro del listado, carga sus valores en el formulario y conserva su id. Al guardar, utiliza PUT con todos los campos editables o PATCH con los modificados según tu contrato; comprueba la respuesta antes de recargar el listado. Para borrar, utiliza el id del elemento y, tras 204, actualiza la pantalla sin ejecutar response.json(). Prueba el ciclo crear → editar → consultar → borrar sobre un registro recién creado.

  1. En cada proyecto de la lista, añade un botón «Eliminar» que pida confirmación antes de lanzar un DELETE.
  2. Si la API responde 204 No Content, quita el elemento de la pantalla. No intentes leer el cuerpo: un 204 no tiene, y hacerlo lanza un error de análisis que parece un fallo del servidor y no lo es.
  3. Encadena el ciclo entero sin recargar la página: crear un proyecto, verlo aparecer en la lista, añadirle una tarea y borrarlo. Cuatro verbos HTTP, una sola pantalla.
  4. Provoca a propósito estos tres errores y comprueba que cada uno se ve distinto en pantalla:
    • Nombre vacío al crear → 400, con el mensaje de validación del servidor.
    • Borrar un proyecto que ya no existe → 404.
    • Backend apagado → fallo de red, que no trae código de estado ninguno.
  5. Escribe al lado de cada uno quién tiene la culpa: el usuario, tu cliente, tu servidor o la red. Esa clasificación es la competencia real de esta sesión, y es lo que evita las tardes perdidas discutiendo de quién es el fallo.
  6. Guarda la carpeta cliente dentro del repositorio del proyecto, junto a un README de tres líneas que diga cómo servirla y contra qué puerto habla. En la UD9 vas a volver a esta página para añadirle el token.
Cómo saber que lo has terminado
Una sola pantalla ejecuta GET, POST, DELETE y el subrecurso de tareas sin recargarse; los tres tipos de error se distinguen a simple vista; y sabes decir, ante cualquiera de ellos, en qué capa está el problema y con qué evidencia lo has determinado.

Paso 6 · Comprobar y registrar el resultado del proyecto

  1. Completa alta, consulta, modificación y borrado desde el navegador y verifica los cambios en la API.
  2. Provoca validación y fallo de conexión por separado: el cliente debe explicar cada caso y no mostrar éxito ni perder innecesariamente lo escrito.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Diagnóstico forense de integración cliente-servidor

En equipos de trabajo reales, cuando una integración falla se pierde mucho tiempo discutiendo de quién es la culpa.

Analiza estas tres situaciones y determina con precisión técnica en qué capa se encuentra el problema:

  1. Situación A: El usuario pulsa el botón, en la pestaña Red de DevTools aparece una petición POST con código 400 Bad Request, y el cuerpo JSON contiene {"detail": "JSON parse error: Unexpected character"}. ¿Dónde está el error y qué archivo debe corregirse?
  2. Situación B: El usuario pulsa el botón, en la pestaña Red la petición aparece con estado (canceled) y en la consola de JavaScript salta TypeError: Failed to fetch. La terminal de Spring Boot está en silencio absoluto. ¿Qué ha ocurrido?
  3. Situación C: El usuario pulsa el botón, en la pestaña Red el POST devuelve 201 Created y el cuerpo contiene el nuevo recurso con id: 5, pero la pantalla del navegador no muestra ningún cambio y el nuevo proyecto no aparece en la lista. ¿En qué línea del cliente está el fallo?

Formato de entrega

Incluye esta explicación en el registro de la sesión dentro del repositorio de GitHub, junto al código y las comprobaciones. La entrega es el enlace al repositorio y al commit de la sesión.

Objetivo mínimoFormulario de alta conectado por POST con JSON.stringify y actualización de lista ante 201.
Si lo tienesManejo granular de errores RFC 7807 (400 y 409) con mensajes visuales en la interfaz y borrado con DELETE.
RetoTabla de diagnóstico forense de las 3 situaciones resuelta y argumentada a nivel de protocolos.
Ver respuestas

1 · El navegador ejecuta el comportamiento HTML predeterminado: envía una petición POST síncrona tradicional y recarga la página por completo, interrumpiendo cualquier llamada asíncrona de JavaScript.

2 · Porque por motivos de seguridad la API no debe exponer la traza de pila (stack trace) interna en el JSON de respuesta; la causa raíz exacta (línea de Java y excepción) solo está registrada en los logs del servidor.

3 · Código HTTP 204 No Content.

4 · JSON.stringify(objeto).

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

El CRUD completo funciona desde el navegador publicado y queda una comprobación repetible previa a autenticación.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.