O Fetch é a forma moderna de fazer requisições HTTP em JavaScript. Ele substituiu o XMLHttpRequest nos navegadores e, desde o Node.js 18, está disponível nativamente também no servidor. A grande vantagem é que a gente usa a mesma API nos dois lados, com suporte nativo a promises.
Se você ainda não viu o que acontece por baixo dos panos numa requisição HTTP, vale ler antes Request e Response. Se você está começando agora ou quer dominar os casos de uso práticos do fetch, este guia mostra tudo: desde um GET simples até timeout com AbortController, passando por POST com JSON e tratamento de erros.
GET com fetch
O uso mais básico do fetch é fazer uma requisição GET. Basta passar a URL e processar a resposta:
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((res) => res.json())
.then((data) => console.log(data))
Repare que res.json() também retorna uma promise. Por isso usamos dois .then() encadeados: o primeiro processa a resposta HTTP, o segundo extrai o JSON.
Se você quiser pegar o texto puro, use res.text(). Para binário, res.blob() ou res.arrayBuffer(). A gente escolhe o método de acordo com o tipo de conteúdo que a API retorna.
POST com JSON (headers e body)
Para enviar dados, usamos o segundo parâmetro do fetch — um objeto de configuração com method, headers e body:
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meu Post',
body: 'Conteúdo do post',
userId: 1
})
})
.then((res) => res.json())
.then((data) => console.log(data))
O Content-Type: application/json é essencial — ele avisa o servidor que estamos enviando JSON. O body precisa ser uma string, por isso usamos JSON.stringify().
Se você quiser enviar um formulário tradicional, use Content-Type: application/x-www-form-urlencoded e formate o body como key=value&key2=value2. Para upload de arquivos, use FormData e deixe o navegador definir o Content-Type automaticamente (ele vai adicionar o boundary necessário para multipart/form-data).
Tratando erros: response.ok e try/catch
O fetch não rejeita a promise automaticamente quando a resposta é 404 ou 500. Ele só rejeita em casos de erro de rede (servidor fora do ar, sem internet, etc.). Para verificar se a resposta foi bem-sucedida, checamos response.ok:
fetch('https://jsonplaceholder.typicode.com/posts/999999')
.then((res) => {
if (!res.ok) {
throw new Error(`Erro HTTP: ${res.status}`)
}
return res.json()
})
.then((data) => console.log(data))
.catch((erro) => console.error(erro))
A propriedade response.ok é true quando o status está entre 200 e 299. Qualquer coisa fora disso — 404, 500, 403 — retorna false. Lançar um erro manual permite que o .catch() capture tanto erros de rede quanto erros HTTP.
Lembre: sempre valide response.ok antes de processar a resposta. Caso contrário, você pode tentar fazer .json() em uma página de erro HTML e receber um SyntaxError.
fetch com async/await
A sintaxe com .then() funciona, mas async/await deixa o código bem mais legível:
async function buscarPost() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1')
if (!res.ok) {
throw new Error(`Erro HTTP: ${res.status}`)
}
const data = await res.json()
console.log(data)
} catch (erro) {
console.error('Falha na requisição:', erro)
}
}
buscarPost()
Repare que usamos try/catch para capturar tanto erros de rede quanto erros que a gente mesmo lançou (como o throw new Error). Essa é a forma mais limpa de lidar com fetch em código de produção.
Timeout com AbortController
Por padrão, o fetch não tem timeout. Se o servidor demorar demais para responder, a requisição fica travada indefinidamente. Para controlar isso, usamos o AbortController:
async function buscarComTimeout(url, tempoLimite = 5000) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), tempoLimite)
try {
const res = await fetch(url, { signal: controller.signal })
clearTimeout(timeout)
if (!res.ok) {
throw new Error(`Erro HTTP: ${res.status}`)
}
return await res.json()
} catch (erro) {
if (erro.name === 'AbortError') {
throw new Error('Requisição expirou após ' + tempoLimite + 'ms')
}
throw erro
}
}
buscarComTimeout('https://jsonplaceholder.typicode.com/posts/1', 3000)
.then((data) => console.log(data))
.catch((erro) => console.error(erro.message))
O controller.abort() cancela a requisição. O fetch lança um erro com name: 'AbortError', que a gente captura no catch. Importante: sempre limpe o timeout com clearTimeout() quando a requisição terminar, senão ele vai disparar mesmo depois da resposta chegar.
fetch no Node.js 18+
Até o Node 17, o fetch não existia nativamente — era preciso instalar node-fetch ou undici. A partir do Node 18, o fetch é global, igual no navegador:
// Node.js 18+ — não precisa de require nem import
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1')
const data = await res.json()
console.log(data)
Se você está em uma versão anterior do Node, ainda pode instalar o node-fetch:
npm install node-fetch
E importar:
import fetch from 'node-fetch'
A API é idêntica. Se o seu código roda tanto no navegador quanto no servidor (SSR, por exemplo), o fetch unifica a forma de fazer requisições.
fetch ou Axios?
Essa é uma dúvida comum. O Axios tem alguns recursos prontos que o fetch não tem nativamente: timeout configurável, interceptadores, transformação automática de JSON e tratamento de erro HTTP por padrão. Se você está construindo um cliente HTTP robusto com retry, autenticação e logging, Axios ainda é uma boa escolha.
Mas o fetch tem vantagens: é nativo (zero dependências), mais leve, e suficiente para a maioria dos casos. Com AbortController e async/await, você monta as mesmas funcionalidades que o Axios oferece — só precisa escrever um pouco mais de código.
Minha recomendação: use fetch para projetos simples e médios, especialmente se você quer menos dependências. Se o projeto exige muita padronização de requisições (headers globais, retry, interceptadores), Axios pode economizar tempo.
Confira o video:
Curta o DevPleno no Facebook, inscreva-se no canal e cadastre seu e-mail para não perder nenhuma novidade. Deixe suas dúvidas e sugestões nos comentários. Abraço!
Mestre em Sistemas de Informação pela USP e criador do DevPleno. Iniciou sua carreira como professor com apenas 18 anos em um curso técnico, foram 11 anos em sala de aula formando desenvolvedores fullstack no sul de Minas Gerais.