Fetch API: como consumir uma API com JavaScript
fetch não rejeita em erro HTTP, o corpo é uma segunda Promise e só pode ser lido uma vez. GET, POST, CORS e AbortController com respostas reais do Node.
fetch faz uma requisição HTTP e devolve uma Promise com a resposta. Ele é
nativo no navegador e no Node desde a versão 18 — nada para instalar.
Guarde estas duas frases antes de escrever a primeira linha. O fetch não
rejeita quando o servidor responde com erro: 404 e 500 chegam normalmente, com
response.ok valendo false. E o corpo da resposta é uma segunda Promise:
response.json() também precisa de await.
Os exemplos abaixo rodam contra uma API local da loja, servida em
http://localhost:4599 por este arquivo:
import { createServer } from 'node:http';
const produtos = [
{ id: 1, nome: 'Teclado mecânico', preco: 289.9, estoque: 12 },
{ id: 2, nome: 'Mouse sem fio', preco: 149.9, estoque: 3 },
{ id: 3, nome: 'Cadeira gamer', preco: 1199, estoque: 0 },
];
const espera = (ms) => new Promise((r) => setTimeout(r, ms));
createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost:4599');
const json = (status, corpo) => {
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(corpo));
};
// ?ms=400 atrasa a resposta — é assim que os exemplos de tempo são medidos
await espera(Number(url.searchParams.get('ms') ?? 0));
if (url.pathname === '/produtos') return json(200, produtos);
const um = url.pathname.match(/^\/produtos\/(\d+)$/);
if (um) {
const p = produtos.find((x) => x.id === Number(um[1]));
return p ? json(200, p) : json(404, { erro: 'produto não encontrado' });
}
if (url.pathname === '/estoque') {
const p = produtos.find((x) => x.id === Number(url.searchParams.get('id')));
return json(200, { id: p?.id ?? null, disponivel: (p?.estoque ?? 0) > 0 });
}
if (url.pathname === '/frete') {
return json(200, { cep: url.searchParams.get('cep'), prazo: 4, valor: 24.9 });
}
if (url.pathname === '/pedidos' && req.method === 'POST') {
let corpo = '';
for await (const parte of req) corpo += parte;
const pedido = JSON.parse(corpo);
return json(201, { id: 8241, cliente: pedido.cliente, total: pedido.total, status: 'aguardando pagamento' });
}
json(404, { erro: 'rota não encontrada' });
}).listen(4599);O objeto Response é a resposta HTTP, não o JSON já pronto. Em palavras
simples, primeiro chegam status e cabeçalhos; depois você escolhe como consumir
o corpo. É por isso que uma chamada comum tem dois await.
Dois envelopes: a resposta primeiro, o corpo depois
Pense numa compra que chega com um envelope de protocolo preso à caixa. No
protocolo estão o status da entrega e as informações do envio; o produto está
dentro da caixa e ainda precisa ser aberto. O Response é o protocolo. A
chamada response.json() abre e interpreta o corpo.
Antes de executar a primeira chamada, escreva os valores esperados para
response.status, response.ok e o JSON. Depois confira os três. Repita a
previsão para uma URL inexistente: a saída mostra por que erro HTTP e falha de
rede são casos técnicos diferentes.
Duas esperas: uma pela resposta, outra pelo corpo.
const resposta = await fetch('http://localhost:4599/produtos');
console.log('status:', resposta.status, resposta.statusText);
console.log('ok:', resposta.ok);
const produtos = await resposta.json();
console.log(produtos);O objeto Response chega assim que os cabeçalhos chegam — o corpo ainda está
vindo pela rede. Por isso json() é outra Promise, e não uma propriedade
pronta. Se esse encadeamento de esperas ainda estiver novo, vale a
lição de async e await.
fetch não rejeita em erro HTTP
Aqui está o comportamento que mais pega gente. Peço um produto que não existe:
const resposta = await fetch('http://localhost:4599/produtos/99');
console.log('caiu no catch?', 'não — o fetch resolveu normalmente');
console.log('status:', resposta.status);
console.log('ok:', resposta.ok);
const corpo = await resposta.json();
console.log('corpo:', corpo);
console.log('preço do produto:', corpo.preco);Nenhum erro. O programa segue em frente com um objeto que não é o esperado, e o
undefined viaja pela aplicação até estourar longe daqui — normalmente como
Cannot read properties of undefined.
A verificação de ok é obrigatória, e vale escrevê-la uma vez só, numa função:
async function buscarProduto(id) {
const resposta = await fetch(`http://localhost:4599/produtos/${id}`);
if (!resposta.ok) {
throw new Error(`produto ${id}: a API respondeu ${resposta.status}`);
}
return resposta.json();
}
try {
console.log(await buscarProduto(2));
console.log(await buscarProduto(99));
} catch (e) {
console.log('erro tratado:', e.message);
}O corpo só pode ser lido uma vez
Response é um fluxo. Depois de consumido, acabou:
const resposta = await fetch('http://localhost:4599/produtos');
const texto = await resposta.text();
console.log('texto tem', texto.length, 'caracteres');
const produtos = await resposta.json();
console.log(produtos);Isso aparece na vida real quando você tenta logar a resposta bruta antes de
converter para JSON. O contorno: leia como texto uma vez e use JSON.parse,
ou chame resposta.clone() antes de consumir a primeira cópia.
POST: método, cabeçalhos e corpo
O segundo argumento do fetch é o objeto de opções. Para enviar JSON são três
coisas: method, o cabeçalho Content-Type e o corpo passado por
JSON.stringify.
const resposta = await fetch('http://localhost:4599/pedidos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer token-da-loja',
},
body: JSON.stringify({
cliente: 'Ana Souza',
itens: [{ id: 1, quantidade: 1 }],
total: 289.9,
}),
});
console.log('status:', resposta.status);
console.log(await resposta.json());O erro de digitação mais caro desta seção é passar o objeto direto em body:
sem JSON.stringify, o corpo vira a string [object Object] e o servidor
devolve um 400 que parece culpa dele.
Quando o fetch realmente rejeita
Ele rejeita quando a requisição não acontece: servidor fora do ar, DNS que não resolve, conexão recusada, CORS, requisição abortada. Apontando para uma porta onde não há ninguém:
const resposta = await fetch('http://localhost:4600/produtos');
console.log(resposta.status);TypeError: fetch failed é uma mensagem que não diz nada sozinha. O diagnóstico
está em cause, e é ele que você deve imprimir:
try {
const resposta = await fetch('http://localhost:4600/produtos');
console.log(resposta.status);
} catch (e) {
console.log('nome:', e.name);
console.log('mensagem:', e.message);
console.log('causa:', e.cause?.code, e.cause?.message);
}No navegador, a mesma família de falhas aparece no console como
TypeError: Failed to fetch — e a causa não vem no objeto de erro: vem numa
segunda linha, escrita pelo próprio navegador.
CORS: o bloqueio que não é do seu código
Quando a página está em uma origem e a API em outra, o navegador só entrega a resposta ao seu JavaScript se o servidor autorizar por cabeçalho. Vamos olhar o que a nossa API manda:
const resposta = await fetch('http://localhost:4599/produtos');
console.log('content-type:', resposta.headers.get('content-type'));
console.log('access-control-allow-origin:', resposta.headers.get('access-control-allow-origin'));
console.log('todos os cabeçalhos:', [...resposta.headers.keys()]);O Access-Control-Allow-Origin não existe nessa resposta. No Node isso não faz
diferença nenhuma — a saída acima é de uma execução real, e os produtos vieram.
O Node não implementa a política de mesma origem: quem implementa é o navegador.
Com essa mesma API e uma página servida em outra origem, o desfecho é outro. A
requisição sai, o servidor responde, e o navegador descarta a resposta antes
de o seu then ver qualquer coisa. Você recebe duas mensagens, e elas são
diferentes de tudo que o Node imprime — o texto abaixo é do console do
navegador, não da execução acima. Primeiro o aviso do próprio navegador:
Access to fetch at ‘http://localhost:4599/produtos’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
E só então a Promise rejeita, com um erro que não diz absolutamente nada sobre
a causa: TypeError: Failed to fetch. Se você tratar só esse TypeError, vai
procurar bug na sua requisição por horas — a informação útil está na linha de
cima, que o catch não recebe.
Três consequências práticas:
- O bloqueio é do navegador, decidido pelos cabeçalhos do servidor. Nenhuma
opção do lado do cliente resolve —
mode: 'no-cors'só devolve uma resposta opaca, ilegível. - Se a API é sua, adicione o cabeçalho. Se é de terceiros, chame por um backend seu, que não passa pela regra do navegador.
- Requisições com
Content-Type: application/jsonou cabeçalhoAuthorizationdisparam umOPTIONSde verificação antes do POST. Ver dois pedidos na aba Network é o esperado.
AbortController: desistir no tempo certo
Sem cancelamento, uma API travada trava a sua tela junto. O AbortController
resolve:
const controlador = new AbortController();
const relogio = setTimeout(() => controlador.abort(), 150);
try {
const resposta = await fetch('http://localhost:4599/frete?cep=01310100&ms=800', {
signal: controlador.signal,
});
console.log(await resposta.json());
} catch (e) {
if (e.name === 'AbortError') {
console.log('desistimos do frete:', e.message);
} else {
throw e;
}
} finally {
clearTimeout(relogio);
}O mesmo controlador serve para cancelar quando o usuário digita a próxima letra
na busca ou sai da tela. O clearTimeout no finally é o cuidado que a
lição de timers cobra: timer criado é
timer que precisa morrer.
Um wrapper que cobre quase tudo
Juntando as quatro decisões — verificar ok, ler o corpo do erro, aplicar
tempo-limite e devolver JSON já convertido:
async function pedirJson(url, opcoes = {}) {
const { tempoLimite = 5000, ...resto } = opcoes;
const resposta = await fetch(url, {
...resto,
signal: AbortSignal.timeout(tempoLimite),
});
if (!resposta.ok) {
const corpo = await resposta.text();
throw new Error(`${resposta.status} ${resposta.statusText} em ${url} — ${corpo}`);
}
return resposta.json();
}
console.log(await pedirJson('http://localhost:4599/produtos/1'));
try {
await pedirJson('http://localhost:4599/produtos/99');
} catch (e) {
console.log('erro tratado:', e.message);
}
try {
await pedirJson('http://localhost:4599/frete?cep=01310100&ms=900', { tempoLimite: 200 });
} catch (e) {
console.log('nome:', e.name, '| mensagem:', e.message);
}AbortSignal.timeout(ms) evita o par controlador/setTimeout escrito à mão, e
o erro vem com o nome TimeoutError, distinguível de um cancelamento feito pelo
usuário.
Vinte linhas que você escreve uma vez por projeto e não repete mais. Quando a
tela precisa de várias dessas chamadas ao mesmo tempo, o próximo passo é
Promise.all, allSettled e race — sem isso, cada
await novo soma tempo de espera na cara do usuário.
Prefere aprender em vídeo?
Tem aula sobre este assunto no nosso canal.
- DevClub no YouTubeCriando um App de Previsão do tempo com HTML, CSS e JavaScriptAssistir no YouTube
- DevClub no YouTubeApp de Previsão do tempo do Zero!Assistir no YouTube
Perguntas frequentes
Por que meu catch não pega o erro 404 do fetch?
Preciso instalar axios ou node-fetch?
Como resolvo um erro de CORS no front-end?
Preciso de try/catch e de response.ok, os dois?
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Usando Fetch — developer.mozilla.org
- MDN — CORS — developer.mozilla.org


