Capítulo 16 — Consumo de APIs¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.1 — O que é uma API REST¶
Vídeo curto explicativo (link será adicionado posteriormente)
Uma API (Application Programming Interface — Interface de Programação de Aplicações) é um contrato que define como dois sistemas de software podem se comunicar. No contexto web, uma API é tipicamente um serviço acessível via HTTP que recebe requisições, processa dados e retorna respostas estruturadas — geralmente em formato JSON.
A analogia mais precisa é a de um garçom em um restaurante: o cliente (frontend) não vai até a cozinha (banco de dados) diretamente. Ele comunica seu pedido ao garçom (API), que o transmite à cozinha, aguarda o preparo e traz o resultado ao cliente. O cliente não precisa saber como a cozinha funciona — apenas como se comunicar com o garçom.
16.1.1 — Conceito de API e o modelo cliente-servidor revisitado¶
O modelo cliente-servidor, introduzido no Capítulo 1, ganha uma nova dimensão quando consideramos APIs:
FRONTEND (cliente) API (servidor) BANCO DE DADOS
┌──────────────────┐ ┌──────────────┐ ┌─────────────┐
│ HTML + CSS + JS │ HTTP │ Node.js │ SQL │ PostgreSQL │
│ │ ──────▶│ Python │ ───────▶│ MongoDB │
│ Faz requisições │◀────── │ PHP │◀─────── │ MySQL │
│ Exibe dados │ JSON │ Java │ JSON │ │
└──────────────────┘ └──────────────┘ └─────────────┘
O frontend nunca acessa o banco de dados diretamente — ele sempre passa pela API. Isso garante segurança (credenciais do banco ficam no servidor), controle (a API valida e autoriza cada operação) e independência (o frontend não precisa saber qual banco de dados é usado).
16.1.2 — REST: princípios e convenções¶
REST (Representational State Transfer) é um estilo arquitetural para APIs web definido por Roy Fielding em sua dissertação de doutorado em 2000. Não é um protocolo ou padrão formal — é um conjunto de princípios que, quando seguidos, produzem APIs previsíveis, escaláveis e fáceis de consumir.
Os princípios REST mais relevantes para o frontend:
Interface uniforme: recursos são identificados por URLs. A mesma URL com métodos HTTP diferentes realiza operações distintas sobre o mesmo recurso.
Stateless (sem estado): cada requisição contém todas as informações necessárias para ser processada. O servidor não mantém estado entre requisições — autenticação, contexto e dados de sessão são enviados em cada requisição.
Recursos e representações: tudo é um recurso (usuário, produto, pedido) identificado por uma URL. A representação do recurso (o dado retornado) pode variar conforme o Accept header — JSON, XML, HTML.
16.1.3 — Métodos HTTP: GET, POST, PUT, PATCH, DELETE¶
Os métodos HTTP expressam a intenção da operação sobre um recurso:
| Método | Operação | Idempotente | Corpo |
|---|---|---|---|
GET |
Ler/listar | ✅ Sim | ❌ Não |
POST |
Criar | ❌ Não | ✅ Sim |
PUT |
Substituir (completo) | ✅ Sim | ✅ Sim |
PATCH |
Atualizar (parcial) | ✅ Sim* | ✅ Sim |
DELETE |
Remover | ✅ Sim | Opcional |
Idempotente significa que repetir a operação múltiplas vezes produz o mesmo resultado que executá-la uma única vez. GET /produtos/1 retorna sempre o mesmo produto; DELETE /produtos/1 aplicado duas vezes tem o mesmo efeito que uma vez (o produto já foi removido). POST /produtos cria um novo produto a cada chamada — não é idempotente.
Convenções de URL em APIs REST:
GET /produtos → lista todos os produtos
GET /produtos/42 → retorna o produto com ID 42
POST /produtos → cria um novo produto
PUT /produtos/42 → substitui completamente o produto 42
PATCH /produtos/42 → atualiza campos específicos do produto 42
DELETE /produtos/42 → remove o produto 42
GET /usuarios/7/pedidos → pedidos do usuário 7
GET /usuarios/7/pedidos/3 → pedido 3 do usuário 7
POST /usuarios/7/pedidos → cria pedido para o usuário 7
GET /produtos?categoria=eletronicos&preco_max=500
→ filtra por query string
16.1.4 — Códigos de status HTTP¶
O código de status na resposta HTTP comunica o resultado da operação:
| Faixa | Categoria | Exemplos mais comuns |
|---|---|---|
| 2xx | Sucesso | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirecionamento | 301 Moved Permanently, 304 Not Modified |
| 4xx | Erro do cliente | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity |
| 5xx | Erro do servidor | 500 Internal Server Error, 503 Service Unavailable |
Os mais relevantes para o frontend:
// 200 OK — requisição bem-sucedida com corpo de resposta
// 201 Created — recurso criado com sucesso (resposta a POST)
// 204 No Content — sucesso sem corpo (resposta comum a DELETE)
// 400 Bad Request — dados enviados são inválidos
// 401 Unauthorized — autenticação necessária ou inválida
// 403 Forbidden — autenticado mas sem permissão
// 404 Not Found — recurso não existe
// 422 Unprocessable Entity — dados válidos sintaticamente mas inválidos semanticamente
// 429 Too Many Requests — limite de requisições excedido
// 500 Internal Server Error — erro inesperado no servidor
16.1.5 — Endpoints, recursos e parâmetros de URL¶
Uma URL de API é composta por partes com semânticas distintas:
https://api.exemplo.com/v1/produtos/42?campos=nome,preco&formato=resumido
│─────────────────────│ │─────────│ │─│ │──────────────────────────────│
Base URL Recurso ID Query parameters
(host + versão) (coleção) (item) (filtros e opções)
Path parameters identificam recursos específicos:
/usuarios/{id} → /usuarios/42
/produtos/{id}/avaliacoes/{avId} → /produtos/15/avaliacoes/3
Query parameters filtram, ordenam e paginam:
/produtos?categoria=livros → filtro
/produtos?ordem=preco&direcao=asc → ordenação
/produtos?pagina=2&limite=20 → paginação
/produtos?busca=javascript → busca textual
/produtos?categoria=livros&preco_max=100&pagina=1&limite=10 → combinado
16.2 — JSON: estrutura e manipulação¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.2.1 — O que é JSON e por que é o formato padrão de APIs¶
JSON (JavaScript Object Notation) é um formato de texto para serialização de dados estruturados. Criado por Douglas Crockford nos anos 2000 a partir da sintaxe de objetos do JavaScript, tornou-se o formato padrão para troca de dados em APIs web por três razões principais: é legível por humanos, é facilmente parseável por máquinas, e é nativo ao JavaScript (sem necessidade de bibliotecas externas).
{
"id": 42,
"nome": "Maria Silva",
"email": "maria@exemplo.com",
"ativo": true,
"saldo": 1250.75,
"tags": ["estudante", "frontend"],
"endereco": {
"rua": "Av. Lourival Melo Mota",
"numero": "s/n",
"cidade": "Maceió",
"estado": "AL",
"cep": "57072-970"
},
"ultimoAcesso": "2026-03-15T14:30:00Z",
"preferencias": null
}
Tipos de dados válidos em JSON:
| Tipo JSON | Exemplo | Equivalente JS |
|---|---|---|
| string | "texto" |
string |
| number | 42, 3.14 |
number |
| boolean | true, false |
boolean |
| null | null |
null |
| array | [1, 2, 3] |
Array |
| object | {"chave": "valor"} |
Object |
O que JSON NÃO suporta: undefined, funções, Date (datas são strings), Symbol, BigInt, referências circulares.
16.2.2 — JSON.parse() e JSON.stringify()¶
// JSON.parse() — converte string JSON em objeto JavaScript
const jsonString = '{"nome":"Ana","idade":22,"ativo":true}';
const objeto = JSON.parse(jsonString);
console.log(objeto.nome); // → "Ana"
console.log(objeto.idade); // → 22
console.log(typeof objeto.idade); // → "number"
// JSON.stringify() — converte objeto JavaScript em string JSON
const usuario = {
nome: 'Bruno',
idade: 25,
hobbies: ['leitura', 'programação'],
senha: undefined, // undefined é omitido
saldo: 1500.50
};
const json = JSON.stringify(usuario);
// → '{"nome":"Bruno","idade":25,"hobbies":["leitura","programação"],"saldo":1500.5}'
// Indentação para legibilidade (útil para debug)
const jsonFormatado = JSON.stringify(usuario, null, 2);
/*
{
"nome": "Bruno",
"idade": 25,
...
}
*/
// Replacer: filtrar ou transformar propriedades
const jsonSemSenha = JSON.stringify(usuario, (chave, valor) => {
if (chave === 'senha') return undefined; // omite a propriedade
return valor;
});
// Reviver: transformar valores ao fazer parse
const dados = JSON.parse('{"nascimento":"1995-08-20"}', (chave, valor) => {
if (chave === 'nascimento') return new Date(valor);
return valor;
});
console.log(dados.nascimento instanceof Date); // → true
// Tratamento de erros: JSON inválido lança SyntaxError
try {
JSON.parse('{nome: "Ana"}'); // chaves sem aspas — JSON inválido
} catch (erro) {
console.error('JSON inválido:', erro.message);
}
16.2.3 — Estruturas JSON complexas¶
// Resposta típica de uma API paginada
const resposta = {
"dados": [
{ "id": 1, "titulo": "HTML Semântico", "autor": { "id": 5, "nome": "Prof. Silva" } },
{ "id": 2, "titulo": "CSS Grid", "autor": { "id": 5, "nome": "Prof. Silva" } },
{ "id": 3, "titulo": "JavaScript", "autor": { "id": 7, "nome": "Prof. Lima" } }
],
"meta": {
"total": 42,
"pagina": 1,
"porPagina": 3,
"totalPaginas": 14
},
"links": {
"self": "/artigos?pagina=1",
"proximo": "/artigos?pagina=2",
"anterior": null,
"ultimo": "/artigos?pagina=14"
}
};
// Acessando dados aninhados
const primeiroTitulo = resposta.dados[0].titulo;
const nomeAutor = resposta.dados[0].autor.nome;
const totalPaginas = resposta.meta.totalPaginas;
// Desestruturação de resposta de API
const { dados: artigos, meta: { total, pagina } } = resposta;
// Mapeando para estrutura simplificada
const titulosPorAutor = artigos.map(a => ({
titulo: a.titulo,
autor: a.autor.nome
}));
16.2.4 — Armadilhas comuns com JSON¶
// 1. Datas em JSON são strings — não objetos Date
const evento = JSON.parse('{"data":"2026-03-22T10:00:00Z"}');
console.log(evento.data instanceof Date); // → false (é string!)
const dataReal = new Date(evento.data); // conversão manual necessária
// 2. null ≠ undefined em JSON
const obj = JSON.parse('{"nome":null}');
console.log(obj.nome); // → null
console.log(obj.sobrenome); // → undefined (propriedade não existe)
// 3. Números grandes perdem precisão
// JSON.parse preserva apenas até Number.MAX_SAFE_INTEGER (2^53 - 1)
const grande = JSON.parse('{"id":9007199254740993}');
console.log(grande.id); // → 9007199254740992 (impreciso!)
// Solução: APIs modernas enviam IDs grandes como strings
// 4. JSON.stringify omite undefined, functions e Symbol
const obj2 = { a: 1, b: undefined, c: () => {}, d: Symbol() };
JSON.stringify(obj2); // → '{"a":1}'
// 5. Referências circulares causam erro
const circular = {};
circular.self = circular;
JSON.stringify(circular); // TypeError: Converting circular structure to JSON
16.3 — Fetch API¶
Vídeo curto explicativo (link será adicionado posteriormente)
A Fetch API é a interface nativa do navegador para realizar requisições HTTP. Introduzida no ES2015 e amplamente suportada desde então, substituiu o antigo XMLHttpRequest com uma API baseada em Promises — muito mais legível e integrável com async/await.
16.3.1 — Sintaxe básica e o objeto Response¶
// fetch() retorna uma Promise que resolve para um objeto Response
const resposta = await fetch('https://viacep.com.br/ws/57072970/json/');
// O objeto Response contém metadados da resposta HTTP
console.log(resposta.ok); // → true (status 200-299)
console.log(resposta.status); // → 200
console.log(resposta.statusText); // → "OK"
console.log(resposta.url); // → URL final (após redirecionamentos)
console.log(resposta.headers.get('content-type')); // → "application/json"
// O corpo da resposta é lido com métodos assíncronos
const dados = await resposta.json(); // → objeto JavaScript
// ou
const texto = await resposta.text(); // → string
// ou
const blob = await resposta.blob(); // → Blob (para arquivos/imagens)
16.3.2 — Por que fetch não rejeita em erros HTTP¶
Este é o comportamento mais contraintuitivo da Fetch API e causa de bugs frequentes:
// fetch() APENAS rejeita (throw) em caso de erro de REDE
// (sem conexão, DNS falhou, timeout, etc.)
// Erros HTTP (404, 500, 403) NÃO causam rejeição — chegam como Response normal
// ❌ CÓDIGO BUGADO: não detecta erros HTTP
async function buscarUsuarioBugado(id) {
const dados = await fetch(`/api/usuarios/${id}`).then(r => r.json());
// Se a API retornar 404, dados conterá {"erro": "não encontrado"}
// mas nenhum erro será lançado
return dados;
}
// ✅ CÓDIGO CORRETO: verifica resposta.ok antes de parsear
async function buscarUsuario(id) {
const resposta = await fetch(`/api/usuarios/${id}`);
if (!resposta.ok) {
throw new Error(`Erro HTTP ${resposta.status}: ${resposta.statusText}`);
}
return resposta.json();
}
16.3.3 — Requisições GET com async/await¶
// Padrão completo e robusto para requisições GET
async function buscarDados(url) {
try {
const resposta = await fetch(url);
if (!resposta.ok) {
throw new Error(`HTTP ${resposta.status}: ${resposta.statusText}`);
}
return await resposta.json();
} catch (erro) {
if (erro instanceof TypeError) {
// TypeError: falha de rede (sem conexão, CORS, URL inválida)
throw new Error('Falha de rede. Verifique sua conexão.');
}
throw erro; // relança outros erros
}
}
// Uso
const cep = await buscarDados('https://viacep.com.br/ws/57072970/json/');
console.log(cep.logradouro); // → "Av. Lourival Melo Mota"
// Com query parameters
function construirUrl(base, params) {
const url = new URL(base);
Object.entries(params).forEach(([chave, valor]) => {
if (valor !== undefined && valor !== null) {
url.searchParams.append(chave, valor);
}
});
return url.toString();
}
const url = construirUrl('https://api.exemplo.com/produtos', {
categoria: 'eletronicos',
preco_max: 500,
pagina: 1,
limite: 20
});
// → "https://api.exemplo.com/produtos?categoria=eletronicos&preco_max=500&pagina=1&limite=20"
const produtos = await buscarDados(url);
16.3.4 — Requisições POST, PUT, PATCH e DELETE¶
// POST — criar recurso
async function criarProduto(dadosProduto) {
const resposta = await fetch('https://api.exemplo.com/produtos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${obterToken()}`
},
body: JSON.stringify(dadosProduto)
});
if (!resposta.ok) {
const erro = await resposta.json().catch(() => ({}));
throw new Error(erro.mensagem || `Erro ${resposta.status}`);
}
return resposta.json(); // retorna o recurso criado (geralmente com ID)
}
// PUT — substituição completa
async function substituirProduto(id, dadosCompletos) {
const resposta = await fetch(`https://api.exemplo.com/produtos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dadosCompletos)
});
if (!resposta.ok) throw new Error(`Erro ${resposta.status}`);
return resposta.json();
}
// PATCH — atualização parcial
async function atualizarProduto(id, camposParaAtualizar) {
const resposta = await fetch(`https://api.exemplo.com/produtos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(camposParaAtualizar) // apenas os campos alterados
});
if (!resposta.ok) throw new Error(`Erro ${resposta.status}`);
return resposta.json();
}
// DELETE — remoção
async function removerProduto(id) {
const resposta = await fetch(`https://api.exemplo.com/produtos/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${obterToken()}` }
});
// DELETE frequentemente retorna 204 No Content (sem corpo)
if (!resposta.ok) throw new Error(`Erro ${resposta.status}`);
if (resposta.status === 204) return null;
return resposta.json();
}
16.3.5 — Enviando dados JSON no corpo da requisição¶
// Padrão completo para POST com JSON
const novoPedido = {
clienteId: 42,
itens: [
{ produtoId: 1, quantidade: 2 },
{ produtoId: 5, quantidade: 1 }
],
enderecoEntrega: {
cep: '57072-970',
numero: '100'
},
observacoes: 'Entregar pela manhã'
};
const resposta = await fetch('/api/pedidos', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // OBRIGATÓRIO para JSON
'Accept': 'application/json', // indica que esperamos JSON de volta
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(novoPedido)
});
// Verificar status específico de criação
if (resposta.status === 201) {
const pedidoCriado = await resposta.json();
console.log('Pedido criado com ID:', pedidoCriado.id);
} else if (!resposta.ok) {
const erro = await resposta.json();
// API pode retornar erros detalhados de validação
if (erro.erros) {
erro.erros.forEach(e => console.error(`Campo ${e.campo}: ${e.mensagem}`));
}
}
16.3.6 — Headers: Content-Type, Authorization e outros¶
// Configuração de headers comuns
const headersBase = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Language': 'pt-BR',
};
// Autenticação Bearer (JWT — o mais comum em APIs modernas)
function headersAutenticados() {
const token = localStorage.getItem('token');
return {
...headersBase,
'Authorization': `Bearer ${token}`
};
}
// Autenticação por API Key (comum em APIs públicas)
function headersApiKey(chave) {
return {
...headersBase,
'X-API-Key': chave
};
}
// Usando o objeto Headers para manipulação mais rica
const headers = new Headers({
'Content-Type': 'application/json'
});
headers.append('Authorization', `Bearer ${token}`);
headers.has('Content-Type'); // → true
headers.get('Content-Type'); // → "application/json"
headers.delete('Authorization');
// ⚠️ Headers que o navegador não permite definir manualmente
// (por segurança — são definidos automaticamente):
// Cookie, Host, Referer, Origin, User-Agent
16.3.7 — Enviando formulários com FormData¶
// FormData para envio de arquivos (multipart/form-data)
const form = document.querySelector('#form-upload');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
// FormData captura automaticamente todos os campos do formulário
// incluindo arquivos do type="file"
// NÃO definir Content-Type manualmente — o browser define
// automaticamente com o boundary correto para multipart
const resposta = await fetch('/api/upload', {
method: 'POST',
body: formData
// sem headers Content-Type aqui!
});
if (!resposta.ok) throw new Error('Falha no upload');
const resultado = await resposta.json();
console.log('Arquivo enviado:', resultado.url);
});
// FormData programático — construindo manualmente
const fd = new FormData();
fd.append('nome', 'Maria');
fd.append('foto', arquivoInput.files[0], 'foto-perfil.jpg');
fd.append('dados', JSON.stringify({ role: 'estudante' }));
// Inspecionar FormData
for (const [chave, valor] of fd.entries()) {
console.log(chave, valor);
}
16.4 — Tratamento de erros e UX¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.4.1 — Erros de rede vs erros HTTP¶
// Classificação completa de erros em requisições fetch
async function requisicaoRobusta(url, opcoes = {}) {
try {
const resposta = await fetch(url, opcoes);
// Erro HTTP: servidor respondeu com código de erro
if (!resposta.ok) {
const corpo = await resposta.json().catch(() => null);
const erro = new Error(corpo?.mensagem || `HTTP ${resposta.status}`);
erro.status = resposta.status;
erro.corpo = corpo;
// Classificar por tipo de erro HTTP
if (resposta.status === 401) erro.tipo = 'nao_autenticado';
else if (resposta.status === 403) erro.tipo = 'sem_permissao';
else if (resposta.status === 404) erro.tipo = 'nao_encontrado';
else if (resposta.status === 422) erro.tipo = 'validacao';
else if (resposta.status === 429) erro.tipo = 'limite_excedido';
else if (resposta.status >= 500) erro.tipo = 'servidor';
else erro.tipo = 'cliente';
throw erro;
}
return resposta;
} catch (erro) {
// TypeError: erro de REDE (sem conexão, CORS, timeout, DNS)
if (erro instanceof TypeError) {
const erroRede = new Error('Falha de rede. Verifique sua conexão.');
erroRede.tipo = 'rede';
throw erroRede;
}
throw erro;
}
}
16.4.2 — Estados de interface: carregando, sucesso, erro, vazio¶
Toda operação assíncrona com uma API deve ser refletida na interface com estados visuais claros:
// Gerenciador de estado de UI para operações assíncronas
class EstadoUI {
constructor(containerSelector) {
this.container = document.querySelector(containerSelector);
}
carregando(mensagem = 'Carregando...') {
this.container.innerHTML = `
<div class="estado estado--carregando" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>${mensagem}</p>
</div>
`;
}
sucesso(html) {
this.container.innerHTML = html;
}
erro(mensagem, onRetry = null) {
this.container.innerHTML = `
<div class="estado estado--erro" role="alert">
<span class="estado__icone" aria-hidden="true">⚠️</span>
<p class="estado__mensagem">${mensagem}</p>
${onRetry ? `
<button type="button" class="btn btn--secundario" id="btn-tentar-novamente">
Tentar novamente
</button>
` : ''}
</div>
`;
if (onRetry) {
this.container.querySelector('#btn-tentar-novamente')
.addEventListener('click', onRetry);
}
}
vazio(mensagem = 'Nenhum item encontrado.') {
this.container.innerHTML = `
<div class="estado estado--vazio" role="status">
<span class="estado__icone" aria-hidden="true">📭</span>
<p>${mensagem}</p>
</div>
`;
}
}
// Uso integrado com fetch
async function carregarProdutos(filtros = {}) {
const ui = new EstadoUI('#lista-produtos');
ui.carregando('Buscando produtos...');
try {
const url = construirUrl('/api/produtos', filtros);
const dados = await buscarDados(url);
if (!dados.items.length) {
ui.vazio('Nenhum produto encontrado para os filtros selecionados.');
return;
}
ui.sucesso(renderizarProdutos(dados.items));
} catch (erro) {
const mensagem = erro.tipo === 'rede'
? 'Sem conexão com a internet.'
: 'Erro ao carregar produtos. Tente novamente.';
ui.erro(mensagem, () => carregarProdutos(filtros));
}
}
16.4.3 — Indicadores de carregamento acessíveis¶
<!-- Spinner acessível -->
<div class="spinner-container" role="status" aria-label="Carregando">
<div class="spinner" aria-hidden="true"></div>
<!-- Texto visível apenas para leitores de tela -->
<span class="sr-only">Carregando, aguarde...</span>
</div>
<!-- Skeleton loading — mais elegante que spinner para listas -->
<div class="skeleton-lista" aria-busy="true" aria-label="Carregando lista">
<div class="skeleton-item">
<div class="skeleton skeleton--titulo"></div>
<div class="skeleton skeleton--texto"></div>
<div class="skeleton skeleton--texto skeleton--curto"></div>
</div>
<!-- repetir N vezes -->
</div>
/* Animação de skeleton */
@keyframes skeleton-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#e2e8f0 25%,
#f1f5f9 50%,
#e2e8f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
}
/* Respeitar preferências de movimento reduzido */
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
}
16.4.4 — Retry e timeout: padrões de resiliência¶
// Retry automático com backoff exponencial
async function fetchComRetry(url, opcoes = {}, maxTentativas = 3) {
let tentativa = 0;
while (tentativa < maxTentativas) {
try {
const resposta = await fetch(url, opcoes);
// Tentar novamente apenas em erros de servidor (5xx) ou 429
if (resposta.status >= 500 || resposta.status === 429) {
tentativa++;
if (tentativa >= maxTentativas) throw new Error(`HTTP ${resposta.status}`);
// Backoff exponencial: 1s, 2s, 4s...
const espera = Math.pow(2, tentativa - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, espera));
continue;
}
return resposta;
} catch (erro) {
if (erro instanceof TypeError) { // erro de rede
tentativa++;
if (tentativa >= maxTentativas) throw erro;
await new Promise(resolve => setTimeout(resolve, 1000 * tentativa));
continue;
}
throw erro;
}
}
}
// Timeout com AbortController
async function fetchComTimeout(url, opcoes = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const resposta = await fetch(url, {
...opcoes,
signal: controller.signal
});
return resposta;
} catch (erro) {
if (erro.name === 'AbortError') {
throw new Error(`Timeout: a requisição demorou mais de ${timeoutMs}ms`);
}
throw erro;
} finally {
clearTimeout(timeout);
}
}
16.4.5 — Exercício prático: busca de CEP com ViaCEP¶
<form class="form-cep" id="form-cep" novalidate>
<div class="campo">
<label for="cep">CEP</label>
<div class="campo__input-wrapper">
<input type="text" id="cep" name="cep"
placeholder="00000-000" maxlength="9"
aria-describedby="cep-erro" autocomplete="postal-code" />
<button type="submit" class="btn btn--primario">Buscar</button>
</div>
<p class="campo__erro" id="cep-erro" role="alert" hidden></p>
</div>
</form>
<div id="resultado-cep"></div>
// Máscara automática de CEP
document.getElementById('cep').addEventListener('input', (e) => {
let v = e.target.value.replace(/\D/g, '').slice(0, 8);
if (v.length > 5) v = v.slice(0, 5) + '-' + v.slice(5);
e.target.value = v;
});
// Busca ao pressionar Enter ou sair do campo
document.getElementById('cep').addEventListener('blur', buscarCEP);
document.getElementById('form-cep').addEventListener('submit', (e) => {
e.preventDefault();
buscarCEP();
});
async function buscarCEP() {
const input = document.getElementById('cep');
const erroEl = document.getElementById('cep-erro');
const resultEl = document.getElementById('resultado-cep');
const cep = input.value.replace(/\D/g, '');
// Validação
if (cep.length !== 8) {
erroEl.textContent = 'Informe um CEP válido com 8 dígitos.';
erroEl.hidden = false;
input.setAttribute('aria-invalid', 'true');
return;
}
erroEl.hidden = true;
input.setAttribute('aria-invalid', 'false');
// Estado de carregamento
resultEl.innerHTML = `
<div class="estado estado--carregando" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Buscando endereço...</p>
</div>
`;
try {
const resposta = await fetchComTimeout(
`https://viacep.com.br/ws/${cep}/json/`,
{}, 5000
);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dados = await resposta.json();
// ViaCEP retorna { erro: true } para CEPs inexistentes
if (dados.erro) {
resultEl.innerHTML = `
<div class="estado estado--vazio" role="alert">
<p>CEP não encontrado. Verifique o número informado.</p>
</div>
`;
return;
}
// Renderizar resultado
resultEl.innerHTML = `
<div class="endereco-card" aria-label="Endereço encontrado">
<dl class="endereco-dados">
<div class="endereco-campo">
<dt>Logradouro</dt>
<dd>${dados.logradouro || '—'}</dd>
</div>
<div class="endereco-campo">
<dt>Bairro</dt>
<dd>${dados.bairro || '—'}</dd>
</div>
<div class="endereco-campo">
<dt>Cidade</dt>
<dd>${dados.localidade} — ${dados.uf}</dd>
</div>
<div class="endereco-campo">
<dt>DDD</dt>
<dd>${dados.ddd || '—'}</dd>
</div>
<div class="endereco-campo">
<dt>IBGE</dt>
<dd>${dados.ibge || '—'}</dd>
</div>
</dl>
</div>
`;
// Preencher formulário automaticamente (se existir)
preencherFormularioComEndereco(dados);
} catch (erro) {
const mensagem = erro.message.includes('Timeout')
? 'A busca demorou muito. Tente novamente.'
: 'Erro ao buscar o CEP. Tente novamente.';
resultEl.innerHTML = `
<div class="estado estado--erro" role="alert">
<p>${mensagem}</p>
</div>
`;
}
}
function preencherFormularioComEndereco(dados) {
const mapa = {
'endereco-rua': dados.logradouro,
'endereco-bairro': dados.bairro,
'endereco-cidade': dados.localidade,
'endereco-estado': dados.uf,
};
Object.entries(mapa).forEach(([id, valor]) => {
const campo = document.getElementById(id);
if (campo && valor) campo.value = valor;
});
document.getElementById('endereco-numero')?.focus();
}
16.5 — APIs públicas: exemplos práticos¶
Vídeo curto explicativo (link será adicionado posteriormente)
16.5.1 — Critérios para escolher uma API pública¶
| Critério | O que verificar |
|---|---|
| Documentação | Clara, com exemplos de requisição e resposta |
| Autenticação | Gratuita sem chave? Cadastro necessário? |
| Rate limiting | Quantas requisições por minuto/hora/dia? |
| CORS | Permite requisições de qualquer origem? |
| Formato | JSON? XML? |
| Confiabilidade | SLA? Uptime histórico? |
| Versioning | URL versionada /v1/? Política de deprecação? |
16.5.2 — ViaCEP: busca de endereço por CEP¶
// Documentação: https://viacep.com.br
// Sem autenticação, sem rate limit declarado, CORS liberado
class ViaCEP {
static BASE_URL = 'https://viacep.com.br/ws';
static async buscarPorCEP(cep) {
const cepNumerico = cep.replace(/\D/g, '');
if (cepNumerico.length !== 8) throw new Error('CEP inválido');
const resposta = await fetch(`${this.BASE_URL}/${cepNumerico}/json/`);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dados = await resposta.json();
if (dados.erro) throw new Error('CEP não encontrado');
return dados;
}
static async buscarPorLogradouro(uf, cidade, logradouro) {
const params = [uf, cidade, logradouro].map(encodeURIComponent).join('/');
const resposta = await fetch(`${this.BASE_URL}/${params}/json/`);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
}
// Uso
const endereco = await ViaCEP.buscarPorCEP('57072-970');
console.log(endereco.logradouro); // → "Av. Lourival Melo Mota"
16.5.3 — OpenWeatherMap: previsão do tempo¶
// Documentação: https://openweathermap.org/api
// Requer cadastro gratuito para obter API key
class OpenWeather {
#apiKey;
static BASE_URL = 'https://api.openweathermap.org/data/2.5';
constructor(apiKey) {
this.#apiKey = apiKey;
}
async buscarPorCidade(cidade) {
const url = new URL(`${OpenWeather.BASE_URL}/weather`);
url.searchParams.set('q', cidade);
url.searchParams.set('appid', this.#apiKey);
url.searchParams.set('units', 'metric'); // Celsius
url.searchParams.set('lang', 'pt_br');
const resposta = await fetch(url);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
async previsao5Dias(cidade) {
const url = new URL(`${OpenWeather.BASE_URL}/forecast`);
url.searchParams.set('q', cidade);
url.searchParams.set('appid', this.#apiKey);
url.searchParams.set('units', 'metric');
url.searchParams.set('lang', 'pt_br');
url.searchParams.set('cnt', '5'); // 5 períodos de 3h
const resposta = await fetch(url);
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
}
// Renderizar dados do tempo
async function exibirTempo(cidade) {
const weather = new OpenWeather('SUA_CHAVE_AQUI');
try {
const dados = await weather.buscarPorCidade(cidade);
document.getElementById('tempo-card').innerHTML = `
<div class="tempo">
<h2 class="tempo__cidade">${dados.name}, ${dados.sys.country}</h2>
<div class="tempo__principal">
<img
src="https://openweathermap.org/img/wn/${dados.weather[0].icon}@2x.png"
alt="${dados.weather[0].description}"
class="tempo__icone"
/>
<span class="tempo__temperatura">${Math.round(dados.main.temp)}°C</span>
</div>
<p class="tempo__descricao">${dados.weather[0].description}</p>
<dl class="tempo__detalhes">
<div><dt>Sensação</dt><dd>${Math.round(dados.main.feels_like)}°C</dd></div>
<div><dt>Umidade</dt><dd>${dados.main.humidity}%</dd></div>
<div><dt>Vento</dt><dd>${Math.round(dados.wind.speed * 3.6)} km/h</dd></div>
</dl>
</div>
`;
} catch (erro) {
console.error('Erro ao buscar tempo:', erro);
}
}
16.5.4 — JSONPlaceholder: simulação de CRUD¶
// Documentação: https://jsonplaceholder.typicode.com
// API de teste sem autenticação — simula operações CRUD
// As operações POST/PUT/PATCH/DELETE são simuladas (não persistem)
class JSONPlaceholder {
static BASE = 'https://jsonplaceholder.typicode.com';
// Posts
static async listarPosts(params = {}) {
const url = construirUrl(`${this.BASE}/posts`, params);
return buscarDados(url);
}
static async buscarPost(id) {
return buscarDados(`${this.BASE}/posts/${id}`);
}
static async criarPost(dados) {
const resposta = await fetch(`${this.BASE}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados)
});
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
static async atualizarPost(id, dados) {
const resposta = await fetch(`${this.BASE}/posts/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados)
});
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return resposta.json();
}
static async removerPost(id) {
const resposta = await fetch(`${this.BASE}/posts/${id}`, {
method: 'DELETE'
});
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
return true;
}
// Comentários de um post
static async comentariosDoPost(postId) {
return buscarDados(`${this.BASE}/posts/${postId}/comments`);
}
// Usuários
static async listarUsuarios() {
return buscarDados(`${this.BASE}/users`);
}
}
16.5.5 — IBGE: dados geográficos do Brasil¶
// Documentação: https://servicodados.ibge.gov.br/api/docs
// Sem autenticação, dados geográficos e populacionais oficiais do Brasil
class IBGE {
static BASE = 'https://servicodados.ibge.gov.br/api/v1';
static async listarEstados() {
const estados = await buscarDados(`${this.BASE}/localidades/estados?orderBy=nome`);
return estados.sort((a, b) => a.nome.localeCompare(b.nome));
}
static async municipiosPorEstado(uf) {
return buscarDados(
`${this.BASE}/localidades/estados/${uf}/municipios?orderBy=nome`
);
}
static async buscarMunicipio(id) {
return buscarDados(`${this.BASE}/localidades/municipios/${id}`);
}
}
// Exemplo: select dinâmico de estado → município
async function inicializarSelectsLocalizacao() {
const selectEstado = document.getElementById('estado');
const selectMunicipio = document.getElementById('municipio');
// Carregar estados
const estados = await IBGE.listarEstados();
estados.forEach(estado => {
const option = document.createElement('option');
option.value = estado.sigla;
option.textContent = estado.nome;
selectEstado.appendChild(option);
});
// Carregar municípios ao selecionar estado
selectEstado.addEventListener('change', async () => {
const uf = selectEstado.value;
selectMunicipio.innerHTML = '<option value="">Carregando...</option>';
selectMunicipio.disabled = true;
try {
const municipios = await IBGE.municipiosPorEstado(uf);
selectMunicipio.innerHTML = '<option value="">Selecione o município...</option>';
municipios.forEach(m => {
const option = document.createElement('option');
option.value = m.id;
option.textContent = m.nome;
selectMunicipio.appendChild(option);
});
selectMunicipio.disabled = false;
} catch (erro) {
selectMunicipio.innerHTML = '<option value="">Erro ao carregar</option>';
}
});
}
16.5.6 — Exercício prático: dashboard com múltiplas APIs¶
// Dashboard que exibe tempo + dados geográficos + posts simulados
async function carregarDashboard() {
const ui = {
tempo: new EstadoUI('#widget-tempo'),
estados: new EstadoUI('#widget-estados'),
posts: new EstadoUI('#widget-posts'),
};
// Carregar tudo em paralelo
ui.tempo.carregando();
ui.estados.carregando();
ui.posts.carregando();
const [tempoResult, estadosResult, postsResult] = await Promise.allSettled([
new OpenWeather('SUA_CHAVE').buscarPorCidade('Maceió'),
IBGE.listarEstados(),
JSONPlaceholder.listarPosts({ _limit: 5 }),
]);
// Processar cada resultado independentemente
if (tempoResult.status === 'fulfilled') {
const d = tempoResult.value;
ui.tempo.sucesso(`<p>${d.name}: ${Math.round(d.main.temp)}°C</p>`);
} else {
ui.tempo.erro('Não foi possível carregar o tempo.');
}
if (estadosResult.status === 'fulfilled') {
const html = estadosResult.value
.slice(0, 5)
.map(e => `<li>${e.nome} (${e.sigla})</li>`)
.join('');
ui.estados.sucesso(`<ul>${html}</ul>`);
} else {
ui.estados.erro('Erro ao carregar estados.');
}
if (postsResult.status === 'fulfilled') {
const html = postsResult.value
.map(p => `<li>${p.title}</li>`)
.join('');
ui.posts.sucesso(`<ul>${html}</ul>`);
} else {
ui.posts.erro('Erro ao carregar posts.');
}
}
Referências: - MDN — Fetch API - MDN — Using Fetch - ViaCEP - JSONPlaceholder - IBGE API - OpenWeatherMap API
Atividades — Capítulo 16¶
1. Por que fetch() não lança um erro quando o servidor retorna status 404 ou 500?
2. Qual a diferença entre os métodos HTTP PUT e PATCH?
3. Por que ao enviar arquivos com FormData não se deve definir o header Content-Type: multipart/form-data manualmente?
- GitHub Classroom: Construir um buscador de endereço que: (1) use a API ViaCEP para buscar dados por CEP com máscara automática; (2) use a API IBGE para popular selects dinâmicos de estado/município; (3) exiba todos os estados de interface (carregando, sucesso, erro, vazio) com indicadores acessíveis. (link será adicionado)
:material-arrow-left: Voltar ao Capítulo 15 — Eventos e Formulários :material-arrow-right: Ir ao Capítulo 17 — Integração Frontend + API