Como consumir uma API REST com JavaScript: fetch, async/await e erros
Consumir API REST com JavaScript é uma das tarefas mais frequentes de quem desenvolve para a web: buscar um CEP, enviar um formulário, carregar a lista de produtos de uma loja ou validar um código de verificação. Tudo isso passa por uma requisição HTTP, uma resposta em JSON e um código que precisa lidar com o que der errado no caminho.
A função fetch, nativa do navegador e do Node.js a partir da versão 18, resolve a parte da requisição. O que separa um código de tutorial de um código pronto para produção é o resto: verificar o status da resposta, tratar timeout, decidir quando repetir a chamada e manter o token longe do front-end.
Este guia percorre cada etapa com exemplos funcionais, de um GET simples até um exemplo completo que consulta um CEP e exibe o endereço na tela, sempre com async/await e um tratamento de erros que você pode copiar para o seu projeto.
O que é uma API REST: verbos, status e JSON
Uma API REST expõe recursos (clientes, pedidos, endereços) por meio de URLs, e cada ação sobre um recurso usa um verbo HTTP. A combinação de verbo, URL e corpo da requisição descreve o que você quer fazer; o código de status da resposta diz o que aconteceu.
| Verbo | Uso típico | Exemplo |
|---|---|---|
| GET | Ler um recurso ou uma lista | GET /clientes/42 |
| POST | Criar um recurso ou executar uma ação | POST /clientes |
| PUT / PATCH | Atualizar um recurso inteiro ou parte dele | PATCH /clientes/42 |
| DELETE | Remover um recurso | DELETE /clientes/42 |
Os códigos de status se agrupam em famílias, e é essa família que orienta o tratamento de erro:
- 2xx: deu certo (200 OK, 201 Created, 204 No Content).
- 4xx: o problema está na requisição (400 dados inválidos, 401 sem autenticação, 403 sem permissão, 404 não encontrado, 429 limite de requisições).
- 5xx: o problema está no servidor (500, 502, 503), geralmente temporário.
Na prática, quase tudo trafega em JSON. Você envia o corpo com JSON.stringify(), informa o header Content-Type: application/json e lê a resposta com response.json(). Vale um aviso: nem toda API segue o REST à risca. Algumas usam POST até para consultas e devolvem status 200 com um campo error no corpo, então o seu código precisa olhar o status e o conteúdo.
fetch básico: GET com async/await
fetch() devolve uma Promise que resolve para um objeto Response. Com async/await o código fica linear e fácil de ler, e o try/catch concentra o tratamento de erros em um só lugar. O exemplo abaixo é a forma mais simples de consumir API REST com JavaScript: um GET na ViaCEP, uma API pública que não exige token:
async function buscarCep(cep) {
const resposta = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
// fetch NÃO lança erro em 404 ou 500: é preciso checar o status
if (!resposta.ok) {
throw new Error(`Falha na requisição: HTTP ${resposta.status}`);
}
const dados = await resposta.json(); // ler o corpo também é assíncrono
if (dados.erro) {
throw new Error('CEP não encontrado');
}
return dados; // { cep, logradouro, bairro, localidade, uf, ... }
}
buscarCep('01001000')
.then((endereco) => console.log(endereco.logradouro, endereco.localidade))
.catch((erro) => console.error(erro.message));
Três detalhes desse código evitam bugs clássicos. Primeiro, a Promise do fetch só é rejeitada quando a requisição não se completa (falha de rede, DNS, CORS ou cancelamento); um 404 resolve normalmente, por isso a checagem de resposta.ok. Segundo, resposta.json() também retorna uma Promise e precisa do await. Terceiro, o catch no final captura tanto a falha de rede quanto os throw que você mesmo escreveu.
Se você precisa passar parâmetros de busca, monte a URL com URLSearchParams em vez de concatenar strings: new URLSearchParams({ uf: 'SP', pagina: 2 }).toString() cuida do encoding por você.
POST com headers, Bearer Token e body JSON
A maior parte das APIs comerciais exige autenticação, e o formato mais comum é o Bearer Token: um header Authorization: Bearer SEU_TOKEN em cada chamada. Para um exemplo real de API brasileira, os primeiros passos na APIBrasil com Bearer Token mostram como gerar o token e testar em homologação antes de gastar créditos. A consulta de CEP da APIBrasil é um POST que recebe JSON e devolve um envelope com error, message e data:
// Lado do servidor (Node.js 18+): o token vem de variável de ambiente
const token = process.env.APIBRASIL_TOKEN;
async function consultarCepApiBrasil(cep) {
const resposta = await fetch('https://gateway.apibrasil.io/api/v2/consulta/cep/credits', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ tipo: 'cep', cep }),
});
const corpo = await resposta.json();
// A API pode responder 200 com error: true no corpo
if (!resposta.ok || corpo.error) {
throw new Error(corpo.message || `Erro HTTP ${resposta.status}`);
}
return corpo.data.cep; // logradouro, bairro.bairro, cidade.cidade, estado...
}
Repare na ordem: primeiro o corpo é lido, depois vêm as checagens. Isso permite aproveitar a mensagem de erro que a API devolve em vez de mostrar um “HTTP 400” sem contexto. Se a resposta puder vir vazia (um 204, por exemplo), proteja a leitura com resposta.status === 204 ? null : await resposta.json().
Esse padrão de POST com Bearer e JSON se repete em praticamente qualquer serviço: gateways de pagamento, envio de mensagens, consultas de documentos. Depois que você escreve uma função dessas, trocar a URL e o corpo é tudo o que muda.

Tratando erros: status HTTP, timeout com AbortController e retry
Erro de rede, servidor fora do ar, limite de requisições estourado e resposta que nunca chega são situações diferentes e pedem reações diferentes. Separar os casos é o que torna o cliente HTTP confiável.
Classifique o erro pelo status
- 4xx: repetir não adianta; a requisição está errada. A exceção é o 429, que pede uma nova tentativa depois de esperar (respeite o header
Retry-Afterquando existir). - 5xx: costuma ser transitório; vale repetir com um intervalo crescente.
- Falha de rede: o
fetchrejeita com umTypeError; também pode ser repetido.
Timeout com AbortController
O fetch não tem timeout embutido. Sem ele, uma API lenta trava a sua tela ou o seu worker por tempo indefinido. O AbortController resolve isso: você passa o signal para o fetch e chama abort() quando o prazo estoura. Em ambientes mais novos, AbortSignal.timeout(ms) faz o mesmo em uma linha.
Retry com backoff exponencial
Repetir a chamada imediatamente só piora um servidor que já está sofrendo. A regra é esperar mais a cada tentativa (500 ms, 1 s, 2 s) e limitar o número de repetições. O código abaixo junta as três ideias em uma função reutilizável:
class ErroHttp extends Error {
constructor(status, mensagem) {
super(mensagem);
this.status = status;
}
}
async function fetchComTimeout(url, opcoes = {}, ms = 8000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { ...opcoes, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
function podeRepetir(erro) {
if (erro.name === 'AbortError') return true; // timeout
if (erro instanceof ErroHttp) return erro.status === 429 || erro.status >= 500;
return erro instanceof TypeError; // falha de rede
}
async function requisitar(url, opcoes = {}, tentativas = 3) {
for (let i = 1; i <= tentativas; i++) {
try {
const resposta = await fetchComTimeout(url, opcoes);
if (!resposta.ok) throw new ErroHttp(resposta.status, `HTTP ${resposta.status}`);
return await resposta.json();
} catch (erro) {
if (i === tentativas || !podeRepetir(erro)) throw erro;
await new Promise((r) => setTimeout(r, 500 * 2 ** (i - 1))); // 500ms, 1s, 2s
}
}
}
Um cuidado importante: só repita requisições que podem ser executadas duas vezes sem efeito colateral, como consultas. Um POST que cria um pedido ou cobra um cartão não deve entrar em retry automático, a não ser que a API aceite uma chave de idempotência.
Exemplo completo para consumir API REST com JavaScript: consultar CEP e exibir na tela
Agora o caso real: uma página com um campo de CEP que preenche o endereço. Como a consulta da APIBrasil usa token, o navegador não pode chamá-la diretamente. A solução é uma rota no seu backend que recebe só o CEP e injeta o token; o front-end conversa apenas com essa rota.
1. A rota no servidor (Express)
// server.js — o token nunca sai daqui
import express from 'express';
const app = express();
app.get('/api/cep/:cep', async (req, res) => {
try {
const endereco = await consultarCepApiBrasil(req.params.cep); // função da seção anterior
res.json(endereco);
} catch (erro) {
res.status(502).json({ mensagem: erro.message });
}
});
app.listen(3000, () => console.log('API em http://localhost:3000'));
2. O HTML
<form id="form-cep">
<input id="cep" placeholder="Somente números" maxlength="9" required>
<button type="submit">Buscar</button>
</form>
<p id="status"></p>
<ul id="resultado"></ul>
<script type="module" src="app.js"></script>
3. O script do front-end
// app.js — usa a função requisitar() da seção de erros
const form = document.querySelector('#form-cep');
const status = document.querySelector('#status');
const lista = document.querySelector('#resultado');
form.addEventListener('submit', async (evento) => {
evento.preventDefault();
const cep = document.querySelector('#cep').value.replace(/\D/g, '');
if (cep.length !== 8) {
status.textContent = 'Digite um CEP com 8 dígitos.';
return;
}
status.textContent = 'Buscando...';
lista.innerHTML = '';
try {
const endereco = await requisitar(`/api/cep/${cep}`);
const campos = {
Logradouro: endereco.logradouro,
Bairro: endereco.bairro?.bairro,
Cidade: endereco.cidade?.cidade,
UF: endereco.estado,
};
for (const [rotulo, valor] of Object.entries(campos)) {
const item = document.createElement('li');
item.textContent = `${rotulo}: ${valor ?? '-'}`; // textContent evita XSS
lista.appendChild(item);
}
status.textContent = '';
} catch (erro) {
status.textContent = 'Não foi possível consultar o CEP agora. Tente novamente.';
}
});
O fluxo cobre o que o usuário espera: validação antes de chamar a rede, um estado de “carregando”, o resultado renderizado com textContent (nunca innerHTML com dados externos) e uma mensagem de erro amigável. O mesmo esqueleto serve para qualquer consulta: uma API de consulta de placa, por exemplo, muda só a rota e os campos exibidos.
Boas práticas: variáveis de ambiente e token fora do front-end
Tudo o que vai para o bundle do navegador é público. Basta abrir o DevTools ou o código-fonte da página para copiar um token colocado em um arquivo JavaScript, mesmo que ele esteja “escondido” em uma variável. Por isso a regra é simples: token só no servidor.
- Guarde as chaves em variáveis de ambiente (
process.env.APIBRASIL_TOKEN) e carregue um arquivo.envapenas em desenvolvimento, com ele listado no.gitignore. O guia sobre segurança do Bearer Token com variáveis de ambiente e cofres de senhas detalha as opções para produção. - Exponha para o front-end apenas rotas específicas (
/api/cep/:cep), nunca um proxy genérico que repasse qualquer URL com o seu token. - Se a API bloquear a chamada direta do navegador por CORS, não tente contornar: é um sinal de que a chamada deve partir do servidor.
- Centralize o cliente HTTP em um módulo (URL base, headers, timeout, retry) para não repetir o mesmo código em cada tela.
- Registre em log a URL, o status e o tempo de cada chamada, mas nunca o token nem dados pessoais completos.
O mesmo desenho vale para serviços de verificação de identidade. Quando você precisa verificar o número de telefone no cadastro com Node.js, o navegador envia o telefone para o seu backend, e só o backend conversa com o provedor usando a chave secreta. A API de verificação segue o padrão REST que você acabou de ver: POST com Bearer, corpo em JSON e um status para cada resultado.
Perguntas frequentes
O fetch funciona no Node.js?
Sim. A partir do Node.js 18, fetch, Headers, Response e AbortController são globais, sem instalar nada. Em versões anteriores, use o pacote undici ou node-fetch, que seguem a mesma interface.
Qual é a diferença entre fetch e axios?
O axios lança erro automaticamente em respostas 4xx e 5xx, converte JSON sem passo extra, tem timeout nativo e interceptors. O fetch é nativo e não adiciona dependência, mas exige que você escreva essas camadas, como as funções deste guia. Para a maior parte dos projetos, as duas opções resolvem bem.
Por que o fetch não lança erro quando a API responde 404?
Porque, do ponto de vista do fetch, a requisição foi concluída: o servidor respondeu. A Promise só é rejeitada quando não há resposta (rede, DNS, CORS ou abort). Verificar response.ok ou response.status é responsabilidade do seu código.
Como consumir uma API com token sem expor a chave?
Crie uma rota no seu backend que recebe apenas os dados necessários, injeta o token a partir de uma variável de ambiente e devolve o resultado. O front-end chama essa rota, e a chave nunca chega ao navegador.
Posso usar retry em qualquer requisição?
Não. Repita apenas operações que podem ser executadas mais de uma vez sem efeito colateral (consultas, GETs) ou chamadas que aceitam chave de idempotência. Limite o número de tentativas, use backoff exponencial e trate 4xx como erro definitivo, com exceção do 429.
Próximos passos
Com async/await, a verificação de status, o timeout e o retry em um único módulo, consumir API REST com JavaScript deixa de ser uma coleção de fetch espalhados e vira uma camada previsível do seu projeto.
Transforme a função requisitar() em um módulo próprio, adicione a URL base e os headers padrão do seu backend e reaproveite em todas as telas. O passo seguinte é levar a mesma disciplina para quem envia dados: validação de formulário antes da requisição e feedback claro em cada resposta.