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 a aula
DevClub no YouTubeApp de Previsão do tempo do Zero!Assistir a aula
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?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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


