Tu aplicación está lista.Tienes un backend que hace algunas cosas mágicas y luego expone algunos datos a través de una API.Tienes un frontend que consume esa API y muestra los datos al usuario.Estás utilizando la API de Fetch para hacer solicitudes a tu backend, luego procesar la respuesta y actualizar la UI. Bueno, en el desarrollo, sí. Luego, desplegas tu aplicación en la producción. Y cosas extrañas comienzan a suceder. La mayoría del tiempo, todo parece bien, pero a veces, las solicitudes fallan. La interfaz de usuario se rompe. Los usuarios se quejan. Te preguntas qué pasó mal. La red es impredecible, y tienes que estar preparado para ello.Más vale tener respuestas a estas preguntas: ¿Qué sucede cuando la red es lenta o poco fiable? ¿Qué sucede cuando el backend está abajo o devuelve un error? Si consumes API externas, ¿qué sucede cuando alcanzas el límite de tasa y te bloqueas? ¿Cómo gestionas estos escenarios graciosamente y proporcionas una buena experiencia de usuario? Honestamente, vanilla Fetch no es suficiente para manejar estos escenarios.Tienes que añadir un montón de código de boilerplate para manejar errores, retries, temporadas, caché, etc. Esto puede convertirse rápidamente en un desorden y difícil de mantener. En este artículo, exploraremos cómo hacer que sus solicitudes de recogida estén listas para la producción utilizando una biblioteca llamada Nosotros vamos a: ffetch Construir un backend con Node.js y Express con algunos endpoints Construir un frontend que encuentre esos endpoints con vanilla JavaScript usando la API de Fetch Hacer que el backend sea flaky para simular escenarios del mundo real Ver cómo pueden fallar las solicitudes de recogida y cómo manejar esos fallos introducir ffetch para simplificar y mejorar el manejo de solicitudes de fetch El Boilerplate Construiremos la columna vertebral de una simple lista de tareas multiusuario. El backend expondrá endpoints RESTful para crear, leer, actualizar y eliminar usuarios y tareas, y asignar tareas a los usuarios. Atrás Usaremos Node.js y Express para construir el backend. También usaremos un simple almacenamiento de datos en memoria para mantener las cosas simples. interface User { id: number; // Unique identifier name: string; // Full name email: string; // Email address } export interface Task { id: number; // Unique identifier title: string; // Short task title description?: string; // Optional detailed description priority: "low" | "medium" | "high"; // Task priority } Crearemos los siguientes puntos finales: GET /users: Obtenga a todos los usuarios (devuelve un conjunto de identificadores de usuario) POST /users: Crear un nuevo usuario (devuelve la identificación del usuario creado) GET /users/:id: Obtener un usuario por id (devuelve el objeto del usuario) PUT /users/:id: Actualizar a un usuario (devuelve un mensaje de éxito) DELETE /users/:id: Eliminar un usuario (devuelve un mensaje de éxito) GET /tasks: Obtenga todas las tareas (devuelve una serie de ids de tareas) GET /tasks/:id: Obtener una tarea por id (devuelve el objeto de la tarea) POST /tasks: Crear una nueva tarea (devuelve la identificación de la tarea creada) PUT /tasks/:id: Actualiza una tarea (devuelve un mensaje de éxito) Delete /tasks/:id: Eliminar una tarea GET /users/:userId/tasks: Obtenga todas las tareas asignadas a un usuario (devuelve un conjunto de id de tareas) POST /users/:userId/tasks/:taskId: Asigna una tarea a un usuario (devuelve un mensaje de éxito) DELETE /users/:userId/tasks/:taskId: Eliminar una tarea de un usuario (devuelve un mensaje de éxito) Frente Como los frameworks suelen agregar sus propias abstracciones y maneras de hacer las cosas, usaremos vanilla TypeScript para mantener las cosas simples y anónimas al framework. Crearemos un SPA con dos vistas: una para la lista de usuarios y una para un usuario específico. La lista de usuarios muestra el nombre del usuario y el número de tareas asignadas a ellos. Hacer clic en un usuario le llevará a la vista del usuario, que muestra los detalles del usuario y sus tareas. Para mantener las cosas sencillas, usaremos las encuestas para obtener los datos más recientes del backend. Cada 3 segundos, haremos solicitudes al backend para obtener los datos más recientes para la vista actual y actualizar la interfaz de usuario en consecuencia. Para la vista de la lista de usuarios, haremos una solicitud a para obtener todos los identificadores de usuario, entonces para cada usuario, haremos una solicitud a para recuperar sus detalles, y Calcular el número de tareas asignadas. GET /users GET /users/:id GET /users/:id/tasks Para la vista del usuario, haremos una solicitud de para obtener los detalles del usuario, y para obtener los ids de tareas asignados a ellos. Luego, para cada id de tareas, haremos una solicitud a Para recuperar los detalles de la tarea. GET /users/:id GET /users/:id/tasks GET /tasks/:id El GitHub Repo Puedes encontrar el código completo para este ejemplo en el . GitHub Repo Debido a la cantidad de boilerplate, consulte el repo para el código completo. Cada etapa del artículo se referirá a una rama en el repo. El repo contiene tanto el código backend como el código frontend. La carpeta, y el frontend está en el Cuando clone el repo, ejecute en ambas carpetas para instalar las dependencias. Luego puede ejecutar el backend con En la La carpeta, y el frontón con En la El frontend será servido en Y el retroceso en . backend frontend npm install npm run dev backend npm run dev frontend http://localhost:5173 http://localhost:3000 Una vez que haya hecho todas las tareas y tanto su backend como su frontend estén en funcionamiento, puede abrir su navegador y ir a Para ver la app en acción: http://localhost:5173 En el desarrollo Si usted navega por , debe ver que todo funciona bien. Si agrega un nuevo usuario con http://localhost:5173 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' Deberías ver que el usuario aparece en la vista de la lista de usuarios dentro de 3 segundos. siéntate libre de jugar con la aplicación y añadir más usuarios y tareas. Bueno, aquí es donde finalmente llegamos al punto de este artículo. Nuestro backend funciona muy bien. Nuestro frontend, a pesar de la horrible boilerplate, también funciona muy bien. Pero entre el frontend y el backend, hay la red. Y la red es poco fiable. Así que veamos qué sucede si añadimos un poco de flakiness a nuestro backend. Simulación de errores de red Añadamos un middleware a nuestro backend que falla aleatoriamente las solicitudes con una probabilidad del 20% y también añade algún retraso aleatorio de hasta 1 segundo. Usted puede encontrar el flaky middleware en Archivo: Aquí está el código: backend/src/middleware/flaky.ts import { Request, Response, NextFunction } from 'express'; export function flaky(req: Request, res: Response, next: NextFunction) { // Randomly fail requests with a 20% chance if (Math.random() < 0.2) { return res.status(500).json({ error: 'Random failure' }); } // Add random delay up to 2 seconds const delay = Math.random() * 2000; setTimeout(next, delay); } Luego, podemos utilizar este middleware en nuestra aplicación Express. Simplemente importa el middleware y lo utiliza antes de sus rutas: backend/src/index.ts ... import { flaky } from './middleware/flaky'; ... app.use(cors()); app.use(express.json()); app.use(flaky); // Use the flaky middleware Este código se encuentra en la rama del repo, por lo que puede comprobarlo con . network-errors git checkout network-errors Ahora, si reinicia su backend y refresca el frontend, debe empezar a ver algunas cosas extrañas.La consola estará llena de errores. Y esto es cuando, si ya no lo tienes, necesitas empezar a pensar en cómo manejar estos errores graciosamente. undefined Escenarios erróneos En primer lugar, identificemos qué puede ir mal y cómo podemos manejarlo: Errores intermitentes de la red: las solicitudes pueden fallar aleatoriamente, por lo que en ciertos errores, necesitamos revisarlas varias veces antes de renunciar. Cuando se realiza una encuesta, no se envía solo una solicitud, sino múltiples solicitudes de forma asíncrona. Y 3 segundos más tarde, se envía otro lote de solicitudes. Si una solicitud del lote anterior aún está pendiente cuando se envía el lote siguiente, podríamos obtener una respuesta más temprana después de una posterior. Esto puede conducir a un estado de UI inconsistente. Debemos asegurarnos de que sólo se utilice la última respuesta para actualizar la interfaz, por lo que cuando comienza un nuevo ciclo de encuesta, necesitamos cancelar cualquier solicitud pendiente del ciclo anterior. Del mismo modo, si el usuario navega a una vista diferente mientras que las solicitudes de la vista anterior todavía están pendentes, podríamos obtener respuestas para la vista anterior después de que ya hayamos navegado lejos. Esto también puede conducir a un estado de interfaz de usuario inconsistente. Debemos asegurarnos de que sólo las respuestas para la vista actual se utilizan para actualizar la interfaz de usuario, por lo que cuando navegamos a una vista diferente, necesitamos cancelar cualquier solicitud pendiente de la vista anterior. Si una solicitud fue exitosa en algún momento, pero luego fracasó en un ciclo de encuestas posterior, no queremos mostrar inmediatamente un estado de error al usuario. Tenemos que manejar escenarios donde, por ejemplo, estamos viendo a un usuario que ha sido eliminado en el backend.Tenemos que manejar errores 404 graciosamente y navegar de vuelta a la vista de lista de usuarios, o al menos mostrar un mensaje no encontrado. También necesitamos manejar escenarios en los que el backend está completamente abajo o inaccesible.Tenemos que mostrar un mensaje de error global al usuario y tal vez retomar las solicitudes después de algún tiempo. Y la lista continúa, especialmente si la interfaz de usuario permite crear, actualizar o borrar datos.Pero por ahora, centrémonos en las operaciones de lectura y cómo manejar errores al recoger datos. Errores de manejo con vanilla fetch Aquí, como con muchas cosas en JavaScript (o TypeScript), tiene dos opciones para manejar estos escenarios.Puede escribir sus propias funciones de utilidad para envolver la API de Fetch y agregar la lógica de manejo de errores necesaria, o puede elegir una biblioteca que haga esto para usted. Comencemos por implementar todo nosotros mismos.El código está en el rama del repo, por lo que puede comprobarlo con . native-fetch git checkout native-fetch Lo que hay que hacer Centralizar toda la lógica de captación en poller.ts. Para cada encuesta, crea un nuevo AbortController y cancela el anterior. Wrap recibe llamadas en una función de retry-and-timeout. En caso de éxito, actualice una caché y utilícela para renderizar. En caso de fracaso, retraiga según sea necesario y maneje los horarios / cancelaciones con gracia. Nuestro file now looks like this: poller.ts // Cache for responses const cache: Record<string, any> = {}; // AbortController for cancelling requests let currentController: AbortController | undefined; // Helper: fetch with retries and timeout async function fetchWithRetry(url: string, options: RequestInit = {}, retries = 2, timeout = 3000): Promise<any> { for (let attempt = 0; attempt <= retries; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timer); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); return data; } catch (err) { clearTimeout(timer); if (attempt === retries) throw err; } } } // Cancel all previous requests function cancelRequests() { if (currentController) currentController.abort(); currentController = new AbortController(); } export async function fetchUserListData() { cancelRequests(); // Use cache if available if (cache.userList) return cache.userList; try { if (!currentController) throw new Error('AbortController not initialized'); const userIds = await fetchWithRetry('http://localhost:3000/users', { signal: currentController!.signal }); const users = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}`, { signal: currentController!.signal }))); const taskCounts = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}/tasks`, { signal: currentController!.signal }).then((tasks: any[]) => tasks.length))); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { // fallback to cache if available if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { if (!currentController) throw new Error('AbortController not initialized'); const user = await fetchWithRetry(`http://localhost:3000/users/${userId}`, { signal: currentController!.signal }); const taskIds = await fetchWithRetry(`http://localhost:3000/users/${userId}/tasks`, { signal: currentController!.signal }); const tasks = await Promise.all(taskIds.map((id: number) => fetchWithRetry(`http://localhost:3000/tasks/${id}`, { signal: currentController!.signal }))); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } Nosotros eliminamos Como toda la lógica fetch está ahora en En nuestro caso de uso simplificado, es soportable, pero incluso aquí, necesitábamos añadir un montón de código de boilerplate para manejar errores, retries, temporadas, cancelaciones y caché.Y esto es sólo para operaciones de lectura.Imagine cuánto más código necesitaría para manejar crear, actualizar y borrar usuarios, tareas y asignaciones. api.ts poller.ts Si ejecuta la aplicación ahora, debe ver que funciona mucho mejor.La interfaz de usuario es más estable y no se rompe tan a menudo.Todavía puede ver algunos errores en la consola, pero se manejan graciosamente y no afectan tanto a la experiencia del usuario. Desventajas de este enfoque Más código de boilerplate: tuvimos que escribir un montón de código para manejar errores, retries, temporadas, cancelaciones y caché. No muy reutilizable: El código está estrechamente ligado a nuestro caso de uso específico y no es muy reutilizable para otros proyectos o escenarios. Características limitadas: El código solo maneja escenarios de error básicos.Scenarios más complejos como retrocesos exponenciales, interruptores de circuitos o manejo de errores globales requerirían aún más código. Uso for Better Fetch Handling ffetch FETCH Para abordar las desventajas de nuestro manejo personalizado, escribí una biblioteca llamada Es una biblioteca pequeña y ligera que envuelve la API de Fetch y proporciona una manera simple y declarativa de manejar errores, retries, timeouts, cancelaciones y algunas más características. ffetch Reescribamos nuestra lógica fetch usando Puedes encontrar el código en el rama del repo, por lo que puede comprobarlo con . ffetch ffetch git checkout ffetch En primer lugar, la instalación En la El folleto: ffetch frontend npm install @gkoos/ffetch Entonces podemos reescribir nuestro El archivo que utiliza : poller.ts ffetch import createClient from '@gkoos/ffetch'; // Cache for responses const cache: Record<string, any> = {}; // Create ffetch client const api = createClient({ timeout: 3000, retries: 2, }); function cancelRequests() { api.abortAll(); } export async function fetchUserListData() { cancelRequests(); if (cache.userList) return cache.userList; try { const userIds = await api('http://localhost:3000/users').then(r => r.json()); const users = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}`).then(r => r.json())) ); const taskCounts = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}/tasks`).then(r => r.json()).then((tasks: any[]) => tasks.length)) ); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { const user = await api(`http://localhost:3000/users/${userId}`).then(r => r.json()); const taskIds = await api(`http://localhost:3000/users/${userId}/tasks`).then(r => r.json()); const tasks = await Promise.all( taskIds.map((id: number) => api(`http://localhost:3000/tasks/${id}`).then(r => r.json())) ); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } El código es mucho más limpio y más fácil de leer. ya no tenemos que preocuparnos por retiros, temporadas o cancelaciones. Sólo creamos un cliente con las opciones deseadas y lo usamos para hacer solicitudes. ffetch Otros beneficios del uso ffetch Interruptor de circuito: descarga automática del punto final después de fallas repetidas Backoff automático exponencial para retiros: aumento de los tiempos de espera entre retiros Gestión global de errores: ganchos para el registro, modificación de solicitudes / respuestas, etc. Por ejemplo, podemos elegir reiniciar los errores de red y los errores de servidor 5xx, pero no los errores de cliente 4xx. No hace nada mágico que no puedas construir tú mismo, pero te ahorra de escribir, probar y mantener toda esa placa de calentamiento. Es un envasador de conveniencia que se envuelve en patrones de grado de producción (como el interruptor de circuitos y el retroceso) para que puedas centrarte en tu aplicación, no en tu lógica de recogida. También se detiene en la capa de recogida, por lo que todavía puedes usar tu propio cache, gestión de estado y bibliotecas de UI según lo encuentres apropiado. ffetch Conclusión La principal ventaja de este artículo no es que usted debe usar específicamente, pero que no debe confiar en vanilla Fetch para aplicaciones listas para la producción. La red es poco fiable, y usted necesita estar preparado para ello. ffetch Lo que exactamente necesita hacer depende de su caso y requisitos de uso específicos, pero no puede ir a la producción que solo maneje el camino feliz. Puede ayudar con eso. ffetch