Votre application est prête. Vous avez un backend qui fait des choses magiques puis expose certaines données via une API. Vous avez un frontend qui consomme cette API et affiche les données à l'utilisateur. Vous utilisez l'API Fetch pour faire des demandes à votre backend, puis traiter la réponse et mettre à jour l'UI. Simple et simple, n'est-ce pas? Eh bien, dans le développement, oui. Ensuite, vous déployez votre application pour la production. Et des choses étranges commencent à se produire. La plupart du temps, tout semble bien, mais parfois, les demandes échouent. L'interface utilisateur se brise. Les utilisateurs se plaignent. Vous vous demandez ce qui s'est passé mal. Le réseau est imprévisible, et vous devez être prêt à le faire. Vous préférez avoir des réponses à ces questions: Que se passe-t-il lorsque le réseau est lent ou peu fiable? Que se passe-t-il lorsque le backend est en panne ou renvoie une erreur? Si vous consommez des API externes, que se passe-t-il lorsque vous atteignez la limite de taux et que vous êtes bloqué? Honnêtement, Vanilla Fetch n'est pas suffisant pour gérer ces scénarios. Vous devez ajouter beaucoup de code de boilerplate pour gérer les erreurs, les retries, les délais, le caching, etc. Cela peut rapidement devenir gênant et difficile à entretenir. Dans cet article, nous allons explorer comment rendre vos demandes de collecte prêtes à la production en utilisant une bibliothèque appelée . We will: ffetch build a backend with Node.js and Express with some endpoints Créer un frontend qui sondera ces endpoints avec vanille JavaScript en utilisant l'API Fetch rendre le backend flaky pour simuler des scénarios du monde réel voir comment les demandes de collecte peuvent échouer et comment gérer ces échecs introduce to simplify and enhance the fetch request handling ffetch Le Boilerplateau We will build the backbones of a simple multi-user task list. The backend will expose RESTful endpoints to create, read, update, and delete users and tasks, and assign tasks to users. The frontend will poll the backend to get the latest data and display it to the user. en arrière Nous utiliserons Node.js et Express pour construire le backend. Nous utiliserons également un simple stockage de données en mémoire pour garder les choses 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 } Nous créerons les endpoints suivants : GET /users : Obtenez tous les utilisateurs (Retourne un ensemble d'identifiants d'utilisateur) POST /users : Créer un nouvel utilisateur (Retourne l'identifiant de l'utilisateur créé) GET /users/:id : Obtenir un utilisateur par id (Retourne l'objet utilisateur) PUT /users/:id : Mettre à jour un utilisateur (retourne un message de succès) : Delete a user (returns a success message) DELETE /users/:id GET /tasks : Obtenez toutes les tâches (retourne un ensemble d'id de tâche) GET /tasks/:id : Obtenir une tâche par id (Retourne l'objet de tâche) : Create a new task (returns the created task's id) POST /tasks : Update a task (returns a success message) PUT /tasks/:id : Delete a task DELETE /tasks/:id : Get all tasks assigned to a user (returns an array of task ids) GET /users/:userId/tasks POST /users/:userId/tasks/:taskId : Assigner une tâche à un utilisateur (retourne un message de succès) : Remove a task from a user (returns a success message) DELETE /users/:userId/tasks/:taskId frontale Comme les cadres ajoutent généralement leurs propres abstractions et façons de faire les choses, nous utiliserons Vanilla TypeScript pour garder les choses simples et non-framework. Nous créerons un SPA avec deux vues: une pour la liste des utilisateurs et une pour un utilisateur spécifique. La liste des utilisateurs affiche le nom de l'utilisateur et le nombre de tâches qui lui sont attribuées. Le clic sur un utilisateur vous mènera à la vue de l'utilisateur, qui montre les détails de l'utilisateur et de leurs tâches. Et de la vue de l'utilisateur, nous pouvons retourner à la liste des utilisateurs. Pour simplifier les choses, nous utiliserons les sondages pour obtenir les dernières données du backend. Tous les 3 secondes, nous ferons des demandes au backend pour obtenir les dernières données pour la vue actuelle et mettre à jour l'interface utilisateur en conséquence. Pour l'affichage de la liste utilisateur, nous ferons une demande à pour obtenir tous les identifiants d'utilisateur, puis pour chaque utilisateur, nous ferons une demande à pour retrouver leurs détails, et Calculer le nombre de tâches qui leur sont assignées. GET /users GET /users/:id GET /users/:id/tasks Pour la vue utilisateur, nous ferons une demande de pour obtenir les détails de l'utilisateur, et pour obtenir les identifiants de tâches qui leur sont attribués. puis pour chaque identifiant de tâche, nous ferons une demande à Pour retrouver les détails de la tâche. GET /users/:id GET /users/:id/tasks GET /tasks/:id Le GitHub Repo Vous pouvez trouver le code complet pour cet exemple dans l'accompagnement . GitHub Repo Because of the amount of boilerplate, refer to the repo for the complete code. Every stage of the article will reference a branch in the repo. The repo contains both the backend and frontend code. The backend is in the l’enveloppe, et le fronton se trouve dans le Lorsque vous clonez le repo, exécutez in both folders to install the dependencies. Then you can run the backend with in the folder, and the frontend with in the Le front-end sera servi à Le backend à . backend frontend npm install npm run dev backend npm run dev frontend http://localhost:5173 http://localhost:3000 Une fois que vous avez fait toutes les tâches et que votre backend et votre frontend sont en cours d'exécution, vous pouvez ouvrir votre navigateur et aller à Voir l’application en action : http://localhost:5173 dans le développement Si vous naviguez à , you should see everything working just fine. If you add a new user with http://localhost:5173 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' you should see the user appear in the userlist view within 3 seconds. Feel free to play around with the app and add more users and tasks. Most likely, everything will work just fine. Well, this is where we finally arrive at the point of this article. Our backend works just fine. Our frontend, despite the horrible boilerplate, also works just fine. But between the frontend and backend, there is the network. And the network is unreliable. So, let's see what happens if we add a bit of flakiness to our backend. Simulation des erreurs de réseau Ajoutez un middleware à notre backend qui échoue aléatoirement les demandes avec une chance de 20% et ajoute également un retard aléatoire jusqu'à 1 seconde. Vous pouvez trouver le middleware dans le Le code : voici le code : 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); } Ensuite, nous pouvons utiliser ce middleware dans notre application Express. Il suffit d'importer le middleware et de l'utiliser avant vos itinéraires: backend/src/index.ts ... import { flaky } from './middleware/flaky'; ... app.use(cors()); app.use(express.json()); app.use(flaky); // Use the flaky middleware Ce code est dans la branch of the repo, so you can check it out with . network-errors git checkout network-errors Maintenant, si vous redémarrez votre backend et rafraîchissez le frontend, vous devriez commencer à voir des choses étranges. La console sera remplie d'erreurs. Et c’est alors, si vous ne l’avez pas déjà fait, que vous devez commencer à penser à la façon de gérer ces erreurs gracieusement. undefined Error Scenarios Tout d’abord, identifions ce qui peut aller mal et comment nous pouvons le gérer: Intermittent network failures: requests can fail randomly, so on certain errors, we need to retry them a few times before giving up. Lors du sondage, nous n'envoyons pas une seule demande, mais plusieurs demandes de manière asynchrone. Et 3 secondes plus tard, nous envoyons un autre lot de demandes. Si une demande du lot précédent est toujours en attente lorsque le lot suivant est envoyé, nous pouvons obtenir une réponse plus tôt après un dernier. Cela peut conduire à un état d'interface utilisateur incohérent. Nous devons nous assurer que seule la dernière réponse est utilisée pour mettre à jour l'interface utilisateur, de sorte que lorsque un nouveau cycle de sondage commence, nous devons annuler toutes les demandes en attente du cycle précédent. De même, si l'utilisateur navigue vers une autre vue alors que les demandes de la vue précédente sont toujours en attente, nous pouvons recevoir des réponses pour la vue précédente après que nous ayons déjà navigué. Cela peut également conduire à un état d'interface incohérent. Nous devons nous assurer que seules les réponses de la vue actuelle sont utilisées pour mettre à jour l'interface utilisateur, de sorte que lorsque nous naviguons vers une autre vue, nous devons annuler toutes les demandes en attente de la vue précédente. If a request was successful at some point, but then fails in a subsequent polling cycle, we don't want to immediately show an error state to the user. We can cache successful responses so users don't notice every little hiccup in the network. Nous devons gérer des scénarios où, par exemple, nous regardons un utilisateur qui a été supprimé dans le backend. Nous devons gérer gracieusement les erreurs 404 et retourner à la vue de la liste d'utilisateurs, ou au moins afficher un message non trouvé. En outre, nous devons gérer des scénarios où le backend est complètement en panne ou inaccessible. Nous devons montrer un message d'erreur global à l'utilisateur et peut-être réexaminer les demandes après un certain temps. And the list goes on, especially if the UI allows creating, updating, or deleting data. But for now, let's focus on the read operations and how to handle errors when fetching data. Les erreurs de manipulation avec Vanilla Fetch Ici, comme avec beaucoup de choses dans JavaScript (ou TypeScript), vous avez deux options pour gérer ces scénarios. Vous pouvez écrire vos propres fonctions d'utilitaire pour envelopper l'API Fetch et ajouter la logique de gestion des erreurs nécessaire, ou vous pouvez choisir une bibliothèque qui fait cela pour vous. Let's start with implementing everything ourselves. The code is on the branche du repo, de sorte que vous pouvez le vérifier avec . native-fetch git checkout native-fetch Ce qu'il faut faire Centralisez toute la logique de capture dans poller.ts. Pour chaque sondage, créez un nouveau AbortController et annulez le précédent. Wrap capture les appels dans une fonction retry-and-timeout. En cas de succès, mettez à jour un cache et utilisez-le pour le rendu. En cas d'échec, réagissez au besoin et traitez gracieusement les délais d'expiration/annulation. notre 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; } } Nous avons supprimé Toute la logique est maintenant en . In our simplified use case, it's bearable, but even here, we needed to add a lot of boilerplate code to handle errors, retries, timeouts, cancellations, and caching. And this is just for read operations. Imagine how much more code you would need to handle create, update, and delete users, tasks, and assignments. api.ts poller.ts If you run the app now, you should see that it works much better. The UI is more stable and doesn't break as often. You can still see some errors in the console, but they are handled gracefully and don't affect the user experience as much. Les inconvénients de cette approche Plus de code de boilerplate: Nous avons dû écrire beaucoup de code pour gérer les erreurs, les retries, les délais, les annulations et le caching. Pas très réutilisable: Le code est étroitement lié à notre cas d'utilisation spécifique et pas très réutilisable pour d'autres projets ou scénarios. Caractéristiques limitées: Le code ne traite que des scénarios d'erreur de base. Des scénarios plus complexes tels que le retour exponentiel, les interrupteurs de circuit ou la gestion globale des erreurs nécessiteraient encore plus de code. Utiliser for Better Fetch Handling ffetch ffetch Pour aborder les inconvénients de notre traitement de collecte personnalisé, j'ai écrit une bibliothèque appelée C'est une bibliothèque petite et légère qui enveloppe l'API Fetch et fournit un moyen simple et déclaratif de gérer les erreurs, les retries, les délais, les annulations et quelques autres fonctionnalités. ffetch Let's rewrite our fetch logic using Vous pouvez trouver le code sur le branche du repo, de sorte que vous pouvez le vérifier avec . ffetch ffetch git checkout ffetch First, install Dans le Le Follet : ffetch frontend npm install @gkoos/ffetch Nous pouvons réécrire notre Fichier utilisé : 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; } } The code is much cleaner and easier to read. We don't have to worry about retries, timeouts, or cancellations anymore. Nous créons simplement un client avec les options souhaitées et l'utilisons pour faire des demandes. ffetch Other Benefits of Using ffetch Interrupteur de circuit : refroidissement automatique des endpoints après des défaillances répétées Backoff exponentiel automatique pour les retraites : augmentation des temps d’attente entre les retraites Gestion globale des erreurs: hooks pour le journalisation, la modification des demandes / réponses, etc. We can fine-tune which failures should trigger a retry, and which should not. For example, we can choose to retry on network errors and 5xx server errors, but not on 4xx client errors. doesn't do anything magical you couldn't build yourself, but it saves you from writing, testing, and maintaining all that boilerplate. It's a convenience wrapper that bakes in production-grade patterns (like circuit breaker and backoff) so you can focus on your app, not your fetch logic. It also stops at the fetch layer, so you can still use your own caching, state management, and UI libraries as you see fit. ffetch Conclusion The main takeaway of this article is not that you should use spécifiquement, mais que vous ne devriez pas compter sur vanille Fetch pour les applications prêtes à la production. Le réseau est peu fiable, et vous devez être préparé pour cela. Vous devez gérer les erreurs, les retries, les délais, les annulations et le caching gracieusement pour fournir une bonne expérience utilisateur. ffetch What you exactly need to do depends on your specific use case and requirements, but you can't go to production handling the happy path only. Things can and will go wrong, and your app needs to handle at least the most common failure scenarios. And peut aider avec cela. ffetch