#2757: rest.js
projectforge-webapp/src/utilities/rest.js Utilidad JavaScript / React — cliente central de la API REST para el frontend React de ProjectForge. Fuente: projectforge-webapp/src/utilities/rest.js 88 líneas · 68 código · 7 comentarios · 13 en blanco
Cada componente React que se comunica con el backend de ProjectForge pasa por este módulo. Proporciona una envoltura ligera alrededor de la API fetch con construcción de URL consciente del entorno, autenticación mediante cookies de sesión (credentials: 'include'), debouncing adaptativo para conexiones lentas y manejo de solicitudes/respuestas JSON. Este es el punto único de contacto entre la SPA React y la capa REST de Spring Boot — no hay otro cliente HTTP en el frontend.
Arquitectura
Pipeline de construcción de URL
El módulo construye las URL REST a través de un pipeline de tres etapas, cada una manejando un aspecto:
Etapa 1 — baseURL: Resuelve el origen del servidor. En desarrollo (import.meta.env.DEV), apunta a http://localhost:8080 — el servidor de desarrollo de Spring Boot. En producción, devuelve una cadena vacía, haciendo que todas las URL sean relativas al origen de la página (sin necesidad de CORS). La guarda MODE !== 'test' evita que la URL del servidor de pruebas se filtre en las pruebas de Jest.
Etapa 2 — getServiceURL / baseRestURL: Antepone el prefijo REST. Si la URL del servicio comienza con / (absoluta desde la raíz), usa baseURL directamente. De lo contrario, usa baseRestURL que añade /rs — el prefijo estándar de endpoint REST de ProjectForge. Esto permite tanto getServiceURL('/some/path') (absoluta) como getServiceURL('user/list') (relativa a /rs).
Etapa 3 — evalServiceURL / createQueryParams: Añade parámetros de consulta. Filtra valores undefined, codifica cada valor y usa correctamente ? vs & dependiendo de si la URL ya contiene una cadena de consulta.
Estrategia de debounce — Conciencia de saveData
La constante
debouncedWaitTime se adapta según la sugerencia del cliente
Save-Data (
MDN). Cuando el usuario ha activado el modo de ahorro de datos (común en móviles), el tiempo de espera aumenta de 250ms a 1000ms — reduciendo la frecuencia de solicitudes en conexiones medidas. Esto es utilizado por los componentes de autocompletado (búsqueda de direcciones, búsqueda de usuarios, búsqueda de tareas) para limitar las llamadas API mientras se escribe.
El diseño es defensivo:
navigator && navigator.connection && navigator.connection.saveData — tres guardas antes de acceder a la propiedad, porque
navigator.connection no está disponible en todos los navegadores (Firefox, Safari).
API exportada
| Exportación | Tipo | Propósito |
debouncedWaitTime | const (250 o 1000) | Debounce adaptativo para entradas de autocompletado basado en el encabezado Save-Data |
baseURL | const (cadena) | Origen del servidor — localhost:8080 en desarrollo, vacío en producción |
baseRestURL | const (cadena) | baseURL + "/rs" — prefijo estándar de endpoint REST |
createQueryParams(obj) | función → cadena | Construye key=val&key=val a partir de un objeto, filtra valores undefined |
evalServiceURL(url, params) | función → cadena | Añade parámetros de consulta a la URL, maneja ? existente vs & |
getServiceURL(url, params) | función → cadena | Constructor de URL completo — antepone base + prefijo REST, añade parámetros |
handleHTTPErrors(res) | función → Response | Lanza un Error en respuestas no 2xx, pasa a través en éxito |
fetchJsonGet(url, params, cb) | función → Promise | GET con respuesta JSON, analizada y pasada al callback |
fetchJsonPost(url, body, cb) | función → Promise | POST con cuerpo JSON-stringified y respuesta JSON |
fetchGet(url, params, cb) | función → Promise | GET simple con callback opcional (sin análisis JSON) |
getObjectFromQuery(str) | función → objeto | Inverso de createQueryParams — analiza ?key=val&... en un objeto |
Manejo de errores
Las tres funciones fetch utilizan el mismo patrón de manejo de errores: .catch((error) => alert('Error interno: ' + error)). La directiva /* eslint-disable no-alert */ al principio reconoce que alert() normalmente está prohibido por ESLint, pero se usa intencionalmente aquí como notificación de último recurso al usuario.
Esta es una decisión deliberada: un sistema de notificación de errores adecuado (basado en Redux, con soporte i18n) sería mejor UX, pero rest.js está en la base del gráfico de dependencias — no puede importar Redux ni componentes React. Usar alert() significa que el manejador de errores tiene cero dependencias y funciona en cualquier contexto (incluso fuera del ciclo de vida de React). La directiva /* eslint-disable no-alert */ se añadió en el commit ac30e55f7 para suprimir la advertencia de linting sobre esta elección intencional.
Autenticación
Todas las llamadas fetch usan credentials: 'include' — instruye al navegador a enviar cookies de sesión HTTP-only con cada solicitud. Este es el mecanismo de autenticación estándar para SPAs servidas desde el mismo origen que la API. La cookie de sesión es establecida por el backend Spring Boot al iniciar sesión e incluida automáticamente por el navegador. No hay gestión de tokens, ni encabezado Authorization, ni lógica de sesión del lado del cliente — el navegador lo maneja de forma transparente a través de la bandera credentials.
Uso en el frontend
Este módulo es importado por prácticamente todos los componentes React que obtienen datos. Patrones de uso típicos:
// En un componente React:
import { fetchJsonGet, fetchJsonPost } from '../utilities/rest';
// Cargar una lista
fetchJsonGet('user/list', { search: 'kai' }, (data) => setUsers(data));
// Guardar una entidad
fetchJsonPost('user/save', { id: 1, username: 'kai' }, (data) => console.log('guardado', data));
// GET simple para descarga de archivo
fetchGet('export/excel', { id: 123 }, () => setDownloading(false));
Consumidores
El módulo se importa en todo el frontend React — libreta de direcciones, calendario, hojas de tiempo, gestión de usuarios, encuestas, scripts y todas las interfaces de plugin. En el momento de escribir esto, aproximadamente 40+ componentes React importan desde este archivo. Cualquier cambio en la lógica de construcción de URL (por ejemplo, cambiar el prefijo REST de /rs a otra cosa) afectaría a cada llamada API en la aplicación.
Historial Git
| Commit | Qué cambió en este archivo |
bf988bc6d | Migración React-scripts→Vite. La detección de entorno cambió de process.env.NODE_ENV === 'development' (convención CRA) a import.meta.env.DEV && import.meta.env.MODE !== 'test' (convención Vite). La guarda añadida MODE !== 'test' evita que Jest apunte a localhost:8080 durante las ejecuciones de prueba — un error común al migrar de CRA a Vite donde el modo del entorno de prueba es 'test', no 'development'. |
823ef0992 | Guarda defensiva de callback en fetchGet. Cambió de .then(() => callback()) a .then(() => { if (typeof callback === 'function' && callback()); }). La función fetchGet tiene un callback opcional — cuando se llama sin uno (por ejemplo, solicitudes de disparar y olvidar), el código antiguo lanzaba TypeError: callback is not a function. La guarda omite silenciosamente el callback cuando no es una función. |
253b9f38b | Pase de formateo de código. Se aplanaron cadenas de llamadas fetch multilínea en llamadas de una sola línea. Antes: fetch(\n getServiceURL(...), {\n method: 'GET',\n .... Después: fetch(getServiceURL(...), { method: 'GET', .... Se redujo el ruido visual, sin cambio de comportamiento. |
ac30e55f7 | Se añadió supresión de ESLint. Se insertó /* eslint-disable no-alert */ al principio del archivo. Los manejadores de error usan alert() para errores visibles al usuario, lo cual la configuración recomendada de ESLint prohíbe. La supresión reconoce que esto es intencional — un mecanismo de notificación de errores sin dependencias para un módulo en la base del gráfico de dependencias. |
bbd81edc3 | Formateo impulsado por ESLint. Las funciones flecha cambiaron de params => ... a (params) => ... (flechas de un solo parámetro entre paréntesis). Se eliminó el cuerpo de función redundante en getServiceURL — la función se definía anteriormente con function getServiceURL(...) { ... } y la reescritura fue un cambio solo de formato. |
Decisiones de diseño
| Decisión | Por qué | Compensación |
Sin Axios, fetch simple | Cero dependencias. El módulo tiene ~80 líneas y cubre todas las necesidades del frontend. Añadir Axios (~14KB comprimido) para interceptores y análisis JSON automático no está justificado para tres métodos HTTP. | Sin interceptores de solicitud/respuesta — la renovación del token de autenticación requeriría código personalizado. Actualmente no es necesario porque la autenticación se basa en cookies. |
| Basado en callbacks en lugar de devolver promesas | Histórico. Cuando se escribió este módulo (2019), el frontend usaba componentes de clase con this.setState en callbacks. Devolver promesas requeriría .then(data => this.setState(...)) en cada consumidor — el patrón de callback era más simple en ese momento. | Más difícil de encadenar, más difícil de usar con async/await. Los consumidores deben pasar callbacks incluso cuando solo quieren los datos. Una reescritura moderna devolvería promesas. |
alert() para manejo de errores | Cero dependencias, funciona en cualquier contexto. Un sistema de notificaciones adecuado requeriría importar React/Redux — creando un riesgo de dependencia circular. | Feo, no compatible con i18n, bloquea el hilo de la UI. Un emisor de eventos de error diferido sería mejor: window.dispatchEvent(new CustomEvent('rest:error', {detail: error})) — consumido por un límite de error de React o un componente de notificación. |
Debounce consciente de saveData | Mejora progresiva. Los usuarios con conexiones medidas (móvil, países en desarrollo) obtienen un debounce más lento — menos llamadas API, menos uso de datos. Los usuarios con conexiones no medidas obtienen un debounce más rápido — autocompletado más ágil. | El soporte del navegador es limitado — navigator.connection es solo de Chromium. Los usuarios de Firefox/Safari siempre obtienen el valor predeterminado de 250ms. La degradación gradual funciona correctamente. |