Contexto
Antes de existir como API nativa disponível em qualquer navegador ou runtime, fazer uma chamada HTTP em JavaScript significava escrever contra XMLHttpRequest diretamente — verboso o bastante para que bibliotecas de terceiros nascessem só para embrulhá-lo numa API mais confortável. fetch é essa API nativa, especificada pelo WHATWG e disponível tanto em navegadores quanto no runtime do Node.js, sem precisar instalar nada: é a primitiva sobre a qual bibliotecas de requisição são construídas. Axios é uma dessas bibliotecas: uma dependência de terceiros que envolve a camada de transporte HTTP do próprio ambiente (XMLHttpRequest no navegador, o módulo http nativo no Node.js) numa API com mais conveniências prontas: interceptors, timeout configurável, serialização e parsing automáticos, e um tratamento de erro que já assume que um 404 ou 500 é uma falha.
Essa última diferença é a que mais surpreende quem migra de um lado para o outro: uma chamada fetch que recebe um 500 do servidor não lança nenhuma exceção. A promise resolve normalmente, e cabe ao código checar response.ok ou response.status manualmente para descobrir que algo deu errado. Axios trata esse mesmo cenário lançando uma exceção automaticamente, então o try/catch já captura tanto falhas de rede quanto respostas HTTP de erro no mesmo lugar.
Quando escolher Axios
Axios compensa quando a aplicação faz chamadas HTTP repetidas o suficiente para que interceptors, timeout e tratamento de erro uniforme paguem por si mesmos, evitando reescrever esse encanamento em cada chamada:
import axios from "axios";
const api = axios.create({ baseURL: "/api", timeout: 5000 });
api.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
});
O interceptor de request acima injeta o token de autenticação em toda chamada feita através dessa instância, sem precisar repetir essa lógica em cada lugar que faz uma requisição. E o mesmo mecanismo existe para interceptar respostas, útil para renovar um token expirado ou padronizar o formato de erro antes que ele chegue ao código de negócio. Somado ao timeout embutido na configuração e ao parsing automático de JSON em response.data, Axios resolve de fábrica um conjunto de problemas que, com fetch puro, cada projeto acaba reimplementando de um jeito ligeiramente diferente.
Axios também expõe eventos de progresso (onUploadProgress, onDownloadProgress) direto na configuração da chamada, o que é particularmente útil em upload de arquivos grandes onde a UI precisa mostrar uma barra de progresso — cenário em que fetch puro exige ler o corpo da resposta como ReadableStream manualmente para calcular o mesmo progresso. Para times que fazem chamadas HTTP em muitos pontos diferentes da aplicação, centralizar esse comportamento numa única instância configurada de Axios costuma sair mais barato do que garantir que todo mundo escreveu o wrapper de fetch do mesmo jeito.
Quando escolher fetch (nativo)
fetch nativo compensa quando o volume e a complexidade das chamadas HTTP não justificam uma dependência externa, ou quando o projeto já tem (ou está disposto a escrever) um wrapper fino próprio:
async function apiFetch(path: string, timeoutMs = 5000) {
const res = await fetch(`/api${path}`, { signal: AbortSignal.timeout(timeoutMs) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
AbortSignal.timeout() é a peça que resolve, de forma nativa e precisa, o que Axios oferece pronto na configuração: gera um AbortSignal que aborta a requisição sozinho depois do tempo especificado, sem precisar orquestrar um setTimeout e um AbortController manualmente. O if (!res.ok) throw acima é o equivalente ao comportamento automático do Axios diante de um erro HTTP — só que escrito à mão, uma vez, dentro do wrapper. Para quem não precisa de interceptors nem de eventos de progresso de upload, esse wrapper de poucas linhas cobre a maior parte do que uma aplicação típica usa de uma biblioteca de HTTP, sem adicionar peso ao bundle nem mais uma dependência para manter atualizada.
Como fetch é uma API padronizada pelo próprio navegador e pelo runtime do Node.js, o mesmo código de requisição roda em ambos os ambientes sem precisar de um pacote isomórfico — algo que bibliotecas de terceiros historicamente precisaram resolver com builds ou polyfills separados para cada lado. Isso importa especialmente em projetos que compartilham código entre cliente e servidor, como funções de busca de dados usadas tanto num componente de servidor quanto num teste rodando em Node: não existe risco de a API se comportar de forma diferente entre os dois ambientes, porque é a mesma implementação nativa dos dois lados.
Veredito
Quanto código de encanamento — interceptors, timeout, tratamento de erro, cancelamento — vale a pena escrever e manter à mão, em vez de terceirizar para uma dependência já testada? É essa pergunta, mais do que qualquer benchmark, que decide entre Axios e fetch puro, já que tecnicamente fetch sozinho cobre a grande maioria dos casos: é uma API madura e bem suportada. Times que já escrevem esse wrapper uma vez e reaproveitam em todo o projeto raramente sentem falta do Axios; times que preferem essas conveniências prontas, testadas e documentadas por uma biblioteca dedicada tendem a preferir instalar a dependência a manter esse código internamente.