Migrando uma Aplicação React do Axios para o Fetch
Migre um app React de Axios para fetch com wrapper seguro, tratamento de erros, timeouts e um cliente pronto para copiar sem regressões.
Migrar uma aplicação React do Axios para o fetch é uma mudança de baixo risco se você substituir a biblioteca por trás de um wrapper em vez de reescrever os pontos de chamada — e um campo minado de regressões se não o fizer. Os dois clientes resolvem o mesmo problema de maneiras diferentes: o fetch é o padrão nativo do browser, sem dependências, que entrega os ingredientes brutos, enquanto o Axios empacota ergonomias — base URL, JSON automático, rejeição automática em respostas de erro, response.data, interceptors e timeouts — que você precisa recriar por conta própria. Este guia resolve primeiro a questão “devo migrar?”, e depois fornece um caminho de migração sequenciado e seguro contra regressões para uma aplicação React real, incluindo um cliente completo pronto para copiar e colar.
Principais conclusões
- A diferença mais perigosa:
fetch()não rejeita em HTTP 4xx ou 5xx — ele resolve com sucesso e entrega umaResponsecomok: false, portanto um erro 500 passa pelo seu blocotrycomo se fosse um 200, a menos que você verifiqueresponse.okmanualmente. - Migre o wrapper, não os pontos de chamada: se o seu novo cliente
fetchexpõe as mesmas assinaturasget/post/patch/deletee retorna as mesmas promises, o React Query, suas mutations e o tratamento de erros nunca saberão que o Axios foi removido. - Use
AbortSignal.timeout(8000)para dar aofetcho timeout de requisição que o Axios tinha embutido — está disponível como Baseline em todos os browsers desde abril de 2024 e aborta com umTimeoutError. - O
fetchnão é mais exclusivo do browser: é um global estável no Node.js desde a versão 21, eliminando o último motivo pelo qual o antigo conselho “mantenha o Axios para o Node” fazia sentido. - A ordem importa: substitua todos os imports para o novo cliente primeiro, depois remova o
axiose atualize o lockfile — faça o inverso e sua build quebra no meio da migração.
Axios vs Fetch: o veredicto
Escolha o fetch quando usar apenas uma pequena parte do Axios e quiser eliminar uma dependência de runtime; mantenha o Axios quando depender dos seus recursos de mais alto nível. O Axios oferece base URL, JSON automático, rejeição automática em respostas com falha, response.data e interceptors de graça; com o fetch, você recria exatamente o subconjunto que sua aplicação realmente usa — e nada mais. Para a maioria das SPAs React que já centralizam as requisições em um único módulo de API, esse subconjunto é pequeno.
As motivações para migrar são concretas:
- Tamanho do bundle. O Axios é uma dependência de runtime adicional — aproximadamente 13–14 KB minificado e comprimido com gzip, dependendo da versão — enquanto um wrapper
fetchconsiste em algumas centenas de linhas de código que você controla e pode otimizar com tree-shaking. O valor exato varia a cada release, então meça contra a versão instalada com uma ferramenta como o Bundlephobia em vez de confiar em um número fixo. - Uma dependência a menos / menor superfície de supply chain. Reduzir dependências também é uma postura de segurança. Em março de 2026, atacantes publicaram duas versões backdoored do Axios no npm por meio de uma conta de mantenedor comprometida, e todos os projetos com uma faixa de versão flutuante as baixaram na próxima instalação. O post-mortem dos mantenedores (issue #10636 no GitHub) e o comunicado de segurança da Microsoft confirmam que foi um comprometimento de distribuição/conta — duas versões maliciosas publicadas, não uma falha no código-fonte do Axios — e as versões maliciosas foram removidas em poucas horas; a CISA também emitiu um alerta. A lição não é “o Axios é inseguro”; é que uma dependência a menos é uma conta a menos que pode ser comprometida em seu nome.
- Você usa apenas uma fração. Se o seu código é
axios.get(url).then(r => r.data), você está pagando por uma biblioteca para fazer o que oito linhas defetchfazem.
Quando manter o Axios
Mantenha o Axios — a migração não vale a pena — se alguma destas situações descreve sua aplicação:
| Mantenha o Axios se você depende de… | Por que o fetch torna isso difícil |
|---|---|
| Uso intenso de interceptors de requisição/resposta | O fetch não tem sistema de interceptors; você teria que recriá-lo |
| UI de progresso de upload | Os corpos de requisição do fetch não expõem eventos de progresso de upload; você precisa do XMLHttpRequest |
| Suporte a browsers legados (ex.: IE11) | O fetch nunca esteve disponível nesses browsers |
Uma grande superfície de configuração específica do Axios (paramsSerializer, transformers, adapters) | Recriar isso custa mais do que a dependência economiza |
Um motivo que não pertence mais a esta lista é o Node.js. O fetch não é mais uma história exclusiva do browser: o Node.js 18 (abril de 2022) incluiu um fetch global habilitado por padrão, e o Node.js 21 (outubro de 2023) promoveu o fetch e as Web Streams para estável. Para uma comparação mais detalhada recurso a recurso, o artigo do LogRocket Axios vs Fetch breakdown aborda progresso de download, streaming e cancelamento em detalhes.
As lacunas comportamentais que você precisa preencher
Discover how at OpenReplay.com.
O Axios fazia seis coisas implicitamente que o fetch não faz — e cada uma é uma mudança comportamental silenciosa: código que compila e executa, mas se comporta de forma diferente. Internalize essas lacunas antes de escrever um wrapper.
fetchnão rejeita em 4xx/5xx. Este é o ponto crítico. Conforme a referência da Fetch API no MDN, a promise resolve assim que o servidor responde com os headers — mesmo em um status de erro HTTP. Um 500 resolve comresponse.ok === false. O Axios rejeita automaticamente status não-2xx para o.catch(); ofetchnão. Você deve verificarresponse.oke lançar o erro manualmente.- Sem JSON automático. O Axios serializa corpos de requisição e analisa respostas para você. Com o
fetch, você chamaJSON.stringify()no envio eawait response.json()no recebimento, e defineContent-Type: application/jsonmanualmente. - Sem
baseURL. O Axios prefixa uma base URL configurada. Ofetchaceita uma URL completa ou relativa ao documento; você concatena os caminhos manualmente. - Sem
response.data. O corpo analisado está disponível viaawait response.json(), não em uma propriedade.data. - Sem interceptors. Não há hook nativo para anexar tokens de autenticação ou tratar erros 401 globalmente.
- Sem opção de timeout. O Axios tem uma configuração
timeout; ofetchnão tem. Você fornece umAbortSignal.
Antes e depois
A mesma chamada, em Axios e no cliente que você vai construir:
// Axios — rejeição automática em erro, response.data, JSON automático
const { data } = await axios.get(`/users/${id}`);
return data;
// fetch (bruto) — deve verificar ok, deve analisar, sem baseURL
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(res.statusText); // <-- sem isso, um 500 parece sucesso
return res.json();
A estratégia: migre o wrapper, não os pontos de chamada
Não toque nos seus componentes, hooks ou chamadas do React Query. Substitua a biblioteca por trás da sua superfície de API existente. Se sua aplicação já roteia requisições por um único módulo que expõe get/post/patch/delete, você só precisa tornar a nova implementação compatível em comportamento — não compatível com o Axios. Mantenha assinaturas idênticas, retorne as mesmas promises e lance o mesmo tipo de erro, e tudo que está abaixo permanece intocado. É isso que torna a migração simples em vez de arriscada.
A sequência:
- Audite o uso. Confirme que as chamadas passam por um único wrapper. Se não passarem, seu primeiro PR é roteá-las por um — essa refatoração é independente do
fetche segura para ser entregue separadamente. - Corresponda ao contrato de erros. Se sua UI ramifica em
error.code === 'EXPIRED'ou lêerror.status, seu novo cliente deve produzir erros com esses mesmos campos. Uma incompatibilidade aqui é uma quebra silenciosa. - Preserve o formato das promises. O React Query, o SWR e suas mutations se importam que você retorne uma promise que resolve para dados ou rejeita com um erro — não com qual biblioteca a produziu.
Um cliente fetch completo para você copiar
Este api-client.ts completo preenche todas as lacunas da comparação: concatenação de base URL, query params, merge de headers, corpo JSON, a verificação de ok que rejeita respostas de erro, uma classe de erro personalizada, injeção de token de autenticação, tratamento centralizado de erros 401 e um timeout via AbortSignal.timeout(). Adicione-o ao projeto, aponte seus imports para ele, e seus pontos de chamada continuarão funcionando.
// lib/api-client.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? "/api";
const DEFAULT_TIMEOUT = 8000;
export class ApiClientError extends Error {
status: number;
code?: string;
data?: unknown;
constructor(message: string, status: number, code?: string, data?: unknown) {
super(message);
this.name = "ApiClientError";
this.status = status;
this.code = code;
this.data = data;
}
}
type Query = Record<string, string | number | boolean | undefined>;
interface RequestOptions extends Omit<RequestInit, "body"> {
body?: unknown;
params?: Query;
timeout?: number;
}
function buildUrl(path: string, params?: Query): string {
const base = BASE_URL.endsWith("/") ? BASE_URL : `${BASE_URL}/`;
const url = new URL(path.replace(/^\//, ""), new URL(base, window.location.origin));
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
}
return url.toString();
}
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { body, params, timeout = DEFAULT_TIMEOUT, headers, ...rest } = options;
const token = localStorage.getItem("token");
let res: Response;
try {
res = await fetch(buildUrl(path, params), {
...rest,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(timeout),
});
} catch (err) {
// Chromium may surface a fetch timeout as AbortError rather than TimeoutError.
if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) {
throw new ApiClientError("Request timed out", 0, "TIMEOUT");
}
throw new ApiClientError("Network error", 0, "NETWORK");
}
// The footgun: fetch resolves on 4xx/5xx. Reject explicitly.
if (!res.ok) {
let payload: any;
try { payload = await res.json(); } catch { payload = undefined; }
if (res.status === 401) { // interceptor-equivalent: central 401 handling
localStorage.removeItem("token");
window.location.assign("/login");
}
throw new ApiClientError(
payload?.message ?? res.statusText,
res.status,
payload?.code,
payload,
);
}
if (res.status === 204) return undefined as T;
return (await res.json()) as T;
}
export const api = {
get: <T>(path: string, options?: RequestOptions) => request<T>(path, { ...options, method: "GET" }),
post: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "POST", body }),
patch: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "PATCH", body }),
put: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "PUT", body }),
delete: <T>(path: string, options?: RequestOptions) => request<T>(path, { ...options, method: "DELETE" }),
};
Dois detalhes merecem destaque. O timeout usa AbortSignal.timeout(), que retorna um AbortSignal que será automaticamente abortado após um tempo especificado, e o signal aborta com uma DOMException do tipo TimeoutError ao expirar; desde abril de 2024, esse recurso funciona em todos os browsers e dispositivos modernos. Capture tanto TimeoutError quanto AbortError: em browsers baseados em Chromium, um timeout de fetch pode aparecer como AbortError em vez de TimeoutError, então trate ambos os nomes de forma defensiva. O curto-circuito no status 204 evita chamar .json() em um corpo vazio.
Recriando interceptors — e o que deixar no XHR
Seu wrapper é o seu interceptor. Tudo o que os interceptors do Axios faziam — anexar um token Bearer, normalizar erros, redirecionar em 401 — vive na função request() acima, aplicada a cada chamada por construção. Você não precisa de um sistema de plugins; você precisa de um único ponto de controle, que você já tem. Adicionar retry em 429 ou logging de requisições significa adicionar algumas linhas ao request(), não registrar um handler.
Uma coisa que deliberadamente não deve ser migrada: progresso de upload. Os corpos de requisição do fetch no browser não expõem eventos de progresso de upload da forma que o XMLHttpRequest faz, então mantenha seu fluxo de upload baseado em XHR existente. Não há problema em ter uma abordagem híbrida: fetch para os 95% de chamadas JSON, XHR para a barra de progresso de upload de arquivos.
A sequência de migração
Execute estas etapas em ordem. A ordenação é a parte em que as pessoas erram.
- Substitua os imports primeiro. Aponte todos os pontos de chamada para o novo cliente (
import { api } from "@/lib/api-client"). Como as assinaturas correspondem, isso é um find-and-replace, não uma reescrita. - Remova o Axios, depois atualize o lockfile. Delete o
axiosdopackage.jsone executenpm install(ou o equivalente do seu gerenciador de pacotes) para atualizar o lockfile. Faça isso depois da substituição dos imports — inverta a ordem e sua build quebra no meio da migração com imports não resolvidos. Em junho de 2026, o registro do npm lista o Axios na versão 1.18.1; se você estiver fixando uma versão segura em outro lugar em vez de removê-lo, verifique o registro para o release atual. - Valide fazendo o build, não lendo o código. Execute sua suíte de testes, uma verificação de tipos do TypeScript (
tsc --noEmit) e um build de produção. Uma migração está concluída quando a aplicação faz o build, passa na verificação de tipos e funciona corretamente — não quando o diff parece limpo.
É nesse último ponto que as regressões silenciosas se escondem. A regressão mais perigosa da migração Axios→fetch é a resposta de erro não tratada: uma requisição que retornou 500 mas, como o fetch não lança erro, fluiu como “sucesso” e deixou o usuário olhando para uma UI vazia ou quebrada sem nenhum erro exibido. Os testes unitários simulam o caminho feliz e passam; o log do servidor mostra o 500, então o backend parece estar bem. A lacuna entre “o servidor registrou um erro” e “o usuário não viu nada” é exatamente o que o session replay com captura de rede evidencia — ele vincula a resposta de erro capturada à UI quebrada que o usuário realmente experimentou, identificando a regressão que seus testes e logs de servidor, individualmente, não conseguem detectar. A verificação !res.ok no cliente acima é o que previne essa classe de bug; verificar que ela dispara em produção é o que confirma que a migração está realmente concluída.
Um cliente baseado em fetch oferece uma árvore de dependências menor, controle total sobre o comportamento de erros e autenticação, e uma conta a menos de terceiros na sua supply chain — sem reescrever um único componente, desde que você mantenha a superfície de API idêntica. O próximo passo concreto: roteie as requisições da sua aplicação por um único módulo, se ainda não o fizer, depois adicione o cliente acima e deixe seu build, tipos e testes indicarem quando você terminou.
Perguntas frequentes
O fetch lança um erro em uma resposta 404 ou 500?
Não. O fetch só rejeita sua promise em uma falha de rede ou em uma requisição que nunca é concluída; um HTTP 404 ou 500 resolve com sucesso com um objeto Response cuja propriedade ok é false. Isso significa que um erro do servidor passa pelo seu bloco try como se fosse um sucesso, a menos que você verifique explicitamente response.ok e lance o erro manualmente. O Axios se comporta de forma diferente e rejeita automaticamente qualquer status não-2xx para o caminho do catch.
Ainda preciso do Axios para fazer requisições no Node.js?
Não. O fetch é agora um global estável no Node.js desde a versão 21, lançada em outubro de 2023, e estava disponível como global experimental habilitado por padrão desde o Node.js 18, em abril de 2022. O antigo conselho de manter o Axios especificamente para requisições no lado do servidor não se aplica mais. Você pode usar o mesmo cliente baseado em fetch tanto no browser quanto no código Node, embora versões mais antigas do Node anteriores à 18 ainda exijam um polyfill ou biblioteca.
Como defino um timeout de requisição com o fetch?
Passe AbortSignal.timeout(milissegundos) como a opção signal, por exemplo signal: AbortSignal.timeout(8000). Ele retorna um signal que aborta automaticamente após o tempo especificado, rejeitando o fetch com uma DOMException do tipo TimeoutError. Está disponível como Baseline em todos os browsers desde abril de 2024. Em browsers baseados em Chromium, a rejeição pode aparecer como AbortError em vez de TimeoutError, então capture ambos os nomes de forma defensiva ao distinguir um timeout de um cancelamento manual.
Trocar o Axios pelo fetch vai quebrar minha configuração do React Query ou SWR?
Não, desde que seu novo cliente fetch retorne as mesmas promises e exponha as mesmas assinaturas de método. O React Query, o SWR e suas mutations só se importam que uma chamada resolva para dados ou rejeite com um erro, não com qual biblioteca a produziu. Mantenha suas assinaturas get, post, patch e delete idênticas e lance o mesmo tipo de erro com os mesmos campos que sua UI usa para ramificação, e a camada de busca de dados nunca saberá que o Axios foi removido.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k