Seu aplicativo está pronto. Você tem um backend que faz algumas coisas mágicas e, em seguida, expõe alguns dados através de uma API. Você tem um frontend que consome essa API e exibe os dados para o usuário. Você está usando o Fetch API para fazer solicitações para seu backend, em seguida, processar a resposta e atualizar a UI. Simples e simples, certo? Bem, no desenvolvimento, sim. Então, você implanta seu aplicativo para a produção. E coisas estranhas começam a acontecer. Na maioria das vezes, tudo parece bem, mas às vezes, os pedidos falham. A interface do usuário quebra. Usuários se queixam. Você se pergunta o que deu errado. A rede é imprevisível, e você tem que estar preparado para isso. Você melhor teria respostas para essas perguntas: O que acontece quando a rede é lenta ou não confiável? O que acontece quando o backend está abaixo ou retorna um erro? Se você consumir APIs externas, o que acontece quando você atinge o limite de taxa e fica bloqueado? Como você lida com esses cenários graciosamente e fornece uma boa experiência de usuário? Honestamente, o Vanilla Fetch não é suficiente para lidar com esses cenários.Você precisa adicionar um monte de código de boilerplate para lidar com erros, retries, timeouts, caching, etc. Isso pode rapidamente se tornar confuso e difícil de manter. Neste artigo, vamos explorar como fazer suas solicitações de recuperação produtivas prontas usando uma biblioteca chamada Nós vamos: ffetch Construa um backend com Node.js e Express com alguns endpoints Construa um frontend que pesquise esses endpoints com vanilla JavaScript usando a API Fetch Faça o backend flaky para simular cenários do mundo real Veja como os pedidos de recuperação podem falhar e como lidar com esses fracassos introduzir ffetch para simplificar e melhorar o processamento de solicitações de fetch O Boilerplate O backend exporá endpoints RESTful para criar, ler, atualizar e excluir usuários e tarefas, e atribuir tarefas aos usuários. Atrás Usaremos Node.js e Express para construir o backend. Nós também usaremos um simples armazenamento de dados na memória para manter as coisas simples. Aqui estão os modelos de usuário e tarefas: 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 } Criaremos os seguintes endpoints: GET /users: Obtenha todos os usuários (retorna uma matriz de ids de usuário) POST /users: Criar um novo usuário (retorna o ID do usuário criado) GET /users/:id: Obter um usuário por id (retorna o objeto do usuário) PUT /users/:id: Atualizar um usuário (retorna uma mensagem de sucesso) DELETE /users/:id: Excluir um usuário (retorna uma mensagem de sucesso) GET /tasks: Obter todas as tarefas (retorna um conjunto de ids de tarefas) GET /tasks/:id: Obter uma tarefa por id (retorna o objeto da tarefa) POST /tasks: Criar uma nova tarefa (retorna a ID da tarefa criada) PUT /tasks/:id: Atualizar uma tarefa (retorna uma mensagem de sucesso) Delete /tasks/:id: Excluir uma tarefa GET /users/:userId/tasks: Obter todas as tarefas atribuídas a um usuário (retorna um conjunto de ids de tarefas) POST /users/:userId/tasks/:taskId: Atribuir uma tarefa a um usuário (retorna uma mensagem de sucesso) DELETE /users/:userId/tasks/:taskId: Remover uma tarefa de um usuário (retorna uma mensagem de sucesso) Fronteiras Como os frameworks geralmente adicionam suas próprias abstrações e maneiras de fazer as coisas, usaremos Vanilla TypeScript para manter as coisas simples e agnósticas ao framework. Criaremos um SPA com duas vistas: uma para a lista de usuários e uma para um usuário específico. A lista de usuários exibe o nome do usuário e o número de tarefas atribuídas a eles. Clicando em um usuário, você será levado para a vista do usuário, que mostra os detalhes do usuário e suas tarefas. Para manter as coisas simples, usaremos sondagens para obter os dados mais recentes do backend. A cada 3 segundos, faremos solicitações ao backend para obter os dados mais recentes para a vista atual e atualizar a UI em conformidade. Para a visualização da lista de usuários, faremos uma solicitação para para obter todos os identificadores de usuário, em seguida, para cada usuário, faremos um pedido para para recuperar seus detalhes, e Calcular o número de tarefas que lhe são atribuídas. GET /users GET /users/:id GET /users/:id/tasks Para a visão do usuário, faremos uma solicitação para para obter os detalhes do usuário, e para obter os ids de tarefa atribuídos a eles. Então, para cada id de tarefa, faremos uma solicitação para Para recuperar os detalhes da tarefa. GET /users/:id GET /users/:id/tasks GET /tasks/:id Sobre o GitHub Repo Você pode encontrar o código completo para este exemplo no . GitHub Repo Devido à quantidade de caldeira, consulte o repo para o código completo. O repo contém o backend e o código frontend. O portfólio, e o front-end está no Quando clonar o repo, execute em ambas as pastas para instalar as dependências. Então você pode executar o backend com Em O O cartão e o front-end com Em O O front-end será servido em Além disso, o backend em . backend frontend npm install npm run dev backend npm run dev frontend http://localhost:5173 http://localhost:3000 Uma vez que você tenha feito todas as tarefas e seu backend e frontend estejam em execução, você pode abrir seu navegador e ir para Veja o app em ação: http://localhost:5173 em desenvolvimento Se você navegar por , você deve ver tudo funcionando bem. Se você adicionar um novo usuário com http://localhost:5173 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' Você deve ver o usuário aparecer na lista de usuários dentro de 3 segundos. Sentir-se livre para brincar com o aplicativo e adicionar mais usuários e tarefas. Bem, é aqui que finalmente chegamos ao ponto deste artigo. Nosso backend funciona bem. Nosso frontend, apesar do terrível boilerplate, também funciona bem. Mas entre o frontend e o backend, há a rede. E a rede é pouco confiável. Então, vamos ver o que acontece se adicionarmos um pouco de flakiness ao nosso backend. Simulação de erros de rede Vamos adicionar um middleware ao nosso backend que falha aleatoriamente pedidos com uma chance de 20% e também adiciona algum atraso aleatório até 1 segundo. Você pode encontrar o flaky middleware no E aqui está o 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); } Então, podemos usar este middleware em nosso aplicativo Express. Você pode encontrar o código no Basta importar o middleware e usá-lo antes de suas rotas: 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 está no ramo do repo, para que você possa verificar com . network-errors git checkout network-errors Agora, se você reiniciar seu backend e refrescar o frontend, você deve começar a ver algumas coisas estranhas. E isso é quando, se você ainda não tem, você precisa começar a pensar em como lidar com esses erros graciosamente. undefined Cenários errados Primeiro de tudo, vamos identificar o que pode dar errado e como podemos lidar com isso: Falhas intermitentes da rede: as solicitações podem falhar aleatoriamente, por isso, em certos erros, precisamos repeti-las algumas vezes antes de desistir. Ao fazer uma votação, não estamos enviando apenas um pedido, mas vários pedidos de forma assíncrona. E 3 segundos depois, enviamos outro lote de pedidos. Se um pedido do lote anterior ainda está pendente quando o próximo lote é enviado, podemos obter uma resposta mais antiga após um posterior. Isso pode levar a um estado de UI inconsistente. Precisamos garantir que apenas a última resposta seja usada para atualizar o UI, por isso, quando um novo ciclo de votação começa, precisamos cancelar quaisquer pedidos pendentes do ciclo anterior. Da mesma forma, se o usuário navegar para uma visão diferente enquanto as solicitações da visão anterior ainda estão pendentes, poderemos obter respostas para a visão anterior depois de já ter navegado fora. Isso também pode levar a um estado de UI inconsistente.Precisamos garantir que apenas as respostas para a visão atual são usadas para atualizar a interface, por isso, ao navegar para uma visão diferente, precisamos cancelar quaisquer solicitações pendentes da visão anterior. Se um pedido tiver sido bem-sucedido em algum momento, mas falhar em um ciclo de pesquisa subsequente, não queremos mostrar imediatamente um estado de erro ao usuário. Temos que lidar com cenários onde, digamos, estamos visualizando um usuário que foi excluído no backend.Precisamos lidar com erros 404 graciosamente e navegar de volta para a vista de lista de usuários, ou pelo menos mostrar uma mensagem não encontrada. Além disso, precisamos lidar com cenários em que o backend está completamente abaixo ou inacessível.Precisamos mostrar uma mensagem de erro global ao usuário e talvez retomar as solicitações depois de algum tempo. E a lista continua, especialmente se a UI permite criar, atualizar ou excluir dados.Mas por enquanto, vamos nos concentrar nas operações de leitura e como lidar com erros ao coletar dados. Erros de manuseio com Vanilla Fetch Aqui, como com muitas coisas em JavaScript (ou TypeScript), você tem duas opções para lidar com esses cenários.Você pode escrever suas próprias funções de utilidade para envolver a API do Fetch e adicionar a lógica de gerenciamento de erros necessária, ou você pode escolher uma biblioteca que faça isso para você. Vamos começar por implementar tudo nós mesmos.O código está no ramo do repo, para que você possa verificar com . native-fetch git checkout native-fetch O que precisa ser feito Centralize toda a lógica de captura em poller.ts. Para cada pesquisa, crie um novo AbortController e cancele o anterior. Envolva chamadas em uma função de retry-and-timeout. No sucesso, atualize uma cache e use-a para renderização. Em caso de falha, retire conforme necessário e gerencie os timeouts/cancelamentos graciosamente. Nossa O arquivo agora parece assim: 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; } } Nós apagamos Como toda a lógica está agora em Em nosso caso de uso simplificado, é suportável, mas mesmo aqui, precisávamos adicionar um monte de código de boilerplate para lidar com erros, retries, timeouts, cancelamentos e caching.E isso é apenas para operações de leitura.Imagine quanto mais código você precisaria para lidar com criar, atualizar e excluir usuários, tarefas e atribuições. api.ts poller.ts Se você executar o aplicativo agora, você deve ver que ele funciona muito melhor.A interface do usuário é mais estável e não quebra tão frequentemente.Você ainda pode ver alguns erros no console, mas eles são tratados graciosamente e não afetam a experiência do usuário tanto. Desvantagens desta abordagem Mais código de boilerplate: Tivemos que escrever um monte de código para lidar com erros, retrias, timeouts, cancelamentos e cache. Não muito reutilizável: o código está estreitamente ligado ao nosso caso específico de uso e não é muito reutilizável para outros projetos ou cenários. Características limitadas: O código só lida com cenários de erro básicos. Cenários mais complexos, como retrocesso exponencial, interruptores de circuito ou gerenciamento de erros globais, exigiriam ainda mais código. Usando Para melhor manuseio Fetch Fetch Para abordar as desvantagens do nosso processamento personalizado, escrevi uma biblioteca chamada É uma biblioteca pequena e leve que envolve a API Fetch e fornece uma maneira simples e declarativa de lidar com erros, retries, timeouts, cancelamentos e alguns outros recursos. ffetch Vamos reescrever nossa lógica fetch usando Você pode encontrar o código no ramo do repo, para que você possa verificar com . ffetch ffetch git checkout ffetch Primeiro, a instalação Em O Folha : ffetch frontend npm install @gkoos/ffetch Podemos reescrever o nosso Arquivo Usando : 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; } } O código é muito mais limpo e mais fácil de ler.Não precisamos nos preocupar com retrias, timeouts ou cancelamentos mais. Nós apenas criamos um cliente com as opções desejadas e usá-lo para fazer solicitações. ffetch Outros benefícios do uso ffetch Interruptor de circuito: descarga automática do endpoint após falhas repetidas Backoff exponencial automático para retrias: aumento dos tempos de espera entre retrias Gerenciamento de erros globais: ganchos para logging, modificação de solicitações / respostas, etc. Podemos ajustar quais falhas devem desencadear um retry e quais não devem.Por exemplo, podemos optar por retry em erros de rede e erros de servidor 5xx, mas não em erros de cliente 4xx. Não faz nada mágico que você não pudesse construir sozinho, mas economiza você de escrever, testar e manter toda essa placa de caldeira. É um embrulhador de conveniência que cozinha em padrões de nível de produção (como circuit breaker e backoff) para que você possa se concentrar em seu aplicativo, não em sua lógica de recolha. Também pára na camada de recolha, para que você ainda possa usar sua própria memória cache, gerenciamento de estado e bibliotecas de UI conforme você achar apropriado. ffetch CONCLUSÃO A principal vantagem deste artigo não é que você deve usar especificamente, mas que você não deve confiar em vanilha Fetch para aplicações prontas para a produção. A rede é pouco confiável, e você precisa estar preparado para isso. Você precisa lidar com erros, retrias, timeouts, cancelamentos e cache graciosamente para fornecer uma boa experiência do usuário. ffetch O que você precisa fazer exatamente depende do seu caso de uso específico e requisitos, mas você não pode ir para a produção lidando apenas com o caminho feliz. as coisas podem e vão dar errado, e seu aplicativo precisa lidar com pelo menos os cenários de falha mais comuns. pode ajudar com isso. ffetch