Javascript

APIs públicas: exemplos práticos Já leu

17 min de leitura

APIs públicas: exemplos práticos
Teoria sem prática é esquecida rápido. Neste artigo vamos consumir APIs públicas reais — sem cadastro, sem chave, sem custo — e construir mini-aplicações funcionais com cada uma. O objetivo é duplo: solidificar tudo que

Teoria sem prática é esquecida rápido. Neste artigo vamos consumir APIs públicas reais — sem cadastro, sem chave, sem custo — e construir mini-aplicações funcionais com cada uma.

O objetivo é duplo: solidificar tudo que aprendemos sobre Fetch, async/await e tratamento de erros, e ao mesmo tempo mostrar o quão poderoso o JavaScript se torna quando conectado ao mundo real.


As APIs que vamos usar

API                  URL Base                              Chave?
─────────────────────────────────────────────────────────────────
JSONPlaceholder      https://jsonplaceholder.typicode.com  Não
ViaCEP               https://viacep.com.br                 Não
PokeAPI              https://pokeapi.co/api/v2             Não
Open-Meteo           https://api.open-meteo.com/v1         Não
IBGE                 https://servicodados.ibge.gov.br      Não
DiceBear (Avatares)  https://api.dicebear.com/7.x          Não

Todas gratuitas, públicas e sem autenticação. Perfeito para aprender.


API 1 — ViaCEP: buscando endereços brasileiros

A ViaCEP é uma API brasileira que retorna dados de endereço a partir de um CEP. Extremamente útil em formulários de e-commerce e cadastro.

async function buscarCEP(cep) {
  // Remove formatação — aceita "01310-100" ou "01310100"
  const cepLimpo = cep.replace(/D/g, "");

  if (cepLimpo.length !== 8) {
    throw new Error("CEP deve ter 8 dígitos.");
  }

  const response = await fetch(`https://viacep.com.br/ws/${cepLimpo}/json/`);

  if (!response.ok) {
    throw new Error(`Erro ${response.status} ao consultar CEP.`);
  }

  const dados = await response.json();

  // ViaCEP retorna { erro: true } para CEPs inválidos
  if (dados.erro) {
    throw new Error(`CEP ${cep} não encontrado.`);
  }

  return {
    cep: dados.cep,
    logradouro: dados.logradouro,
    complemento: dados.complemento,
    bairro: dados.bairro,
    cidade: dados.localidade,
    estado: dados.uf,
    ibge: dados.ibge,
    ddd: dados.ddd,
  };
}

// Preenchimento automático de formulário
const inputCEP = document.querySelector("#cep");
const campos = {
  logradouro: document.querySelector("#logradouro"),
  bairro: document.querySelector("#bairro"),
  cidade: document.querySelector("#cidade"),
  estado: document.querySelector("#estado"),
};

function debounce(fn, delay) {
  let timer;
  return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
}

const preencherEndereco = debounce(async (cep) => {
  if (cep.replace(/D/g, "").length !== 8) return;

  inputCEP.style.borderColor = "";

  try {
    const endereco = await buscarCEP(cep);

    campos.logradouro.value = endereco.logradouro;
    campos.bairro.value = endereco.bairro;
    campos.cidade.value = endereco.cidade;
    campos.estado.value = endereco.estado;

    // Foca no próximo campo relevante
    document.querySelector("#numero")?.focus();

    console.log(`✅ Endereço encontrado: ${endereco.logradouro}, ${endereco.cidade}/${endereco.estado}`);

  } catch (erro) {
    inputCEP.style.borderColor = "red";
    console.error(erro.message);
  }
}, 600);

inputCEP.addEventListener("input", (e) => {
  preencherEndereco(e.target.value);
});

API 2 — IBGE: dados geográficos do Brasil

O IBGE disponibiliza uma API completa com estados, municípios e muito mais:

const IBGE_BASE = "https://servicodados.ibge.gov.br/api/v1";

async function buscarEstados() {
  const response = await fetch(`${IBGE_BASE}/localidades/estados?orderBy=nome`);
  const estados = await response.json();

  return estados.map(e => ({
    id: e.id,
    sigla: e.sigla,
    nome: e.nome,
    regiao: e.regiao.nome,
  }));
}

async function buscarMunicipios(siglaEstado) {
  const response = await fetch(
    `${IBGE_BASE}/localidades/estados/${siglaEstado}/municipios?orderBy=nome`
  );
  const municipios = await response.json();

  return municipios.map(m => ({
    id: m.id,
    nome: m.nome,
  }));
}

// Populando selects de estado/cidade encadeados
async function inicializarSelects() {
  const selectEstado = document.querySelector("#estado");
  const selectCidade = document.querySelector("#cidade");

  // Carrega estados
  const estados = await buscarEstados();
  estados.forEach(estado => {
    const option = document.createElement("option");
    option.value = estado.sigla;
    option.textContent = `${estado.nome} (${estado.sigla})`;
    selectEstado.appendChild(option);
  });

  // Ao trocar estado, carrega cidades
  selectEstado.addEventListener("change", async () => {
    const sigla = selectEstado.value;
    selectCidade.innerHTML = "<option>Carregando...</option>";
    selectCidade.disabled = true;

    try {
      const municipios = await buscarMunicipios(sigla);
      selectCidade.innerHTML = '<option value="">Selecione a cidade</option>';
      municipios.forEach(m => {
        const option = document.createElement("option");
        option.value = m.id;
        option.textContent = m.nome;
        selectCidade.appendChild(option);
      });
      selectCidade.disabled = false;
    } catch (erro) {
      selectCidade.innerHTML = "<option>Erro ao carregar cidades</option>";
    }
  });
}

inicializarSelects();

API 3 — Open-Meteo: previsão do tempo

Open-Meteo é uma API meteorológica gratuita e de código aberto — sem cadastro, sem chave:

async function buscarPrevisao(latitude, longitude, dias = 7) {
  const params = new URLSearchParams({
    latitude,
    longitude,
    daily: [
      "temperature_2m_max",
      "temperature_2m_min",
      "precipitation_sum",
      "weathercode",
      "windspeed_10m_max",
    ].join(","),
    current_weather: true,
    timezone: "America/Sao_Paulo",
    forecast_days: dias,
  });

  const response = await fetch(
    `https://api.open-meteo.com/v1/forecast?${params}`
  );

  if (!response.ok) throw new Error("Erro ao buscar previsão do tempo.");

  const dados = await response.json();

  // Interpretando o weathercode
  const descricaoTempo = (codigo) => {
    const codigos = {
      0: { desc: "Céu limpo", icone: "☀️" },
      1: { desc: "Predominantemente limpo", icone: "🌤️" },
      2: { desc: "Parcialmente nublado", icone: "⛅" },
      3: { desc: "Nublado", icone: "☁️" },
      45: { desc: "Neblina", icone: "🌫️" },
      51: { desc: "Garoa leve", icone: "🌦️" },
      61: { desc: "Chuva leve", icone: "🌧️" },
      71: { desc: "Neve leve", icone: "🌨️" },
      80: { desc: "Pancadas de chuva", icone: "⛈️" },
      95: { desc: "Tempestade", icone: "🌩️" },
    };
    return codigos[codigo] || { desc: "Variável", icone: "🌈" };
  };

  const { daily, current_weather } = dados;

  return {
    agora: {
      temperatura: current_weather.temperature,
      vento: current_weather.windspeed,
      ...descricaoTempo(current_weather.weathercode),
    },
    previsao: daily.time.map((data, i) => ({
      data: new Date(data).toLocaleDateString("pt-BR", { weekday: "short", day: "2-digit", month: "2-digit" }),
      maxima: daily.temperature_2m_max[i],
      minima: daily.temperature_2m_min[i],
      chuva: daily.precipitation_sum[i],
      vento: daily.windspeed_10m_max[i],
      ...descricaoTempo(daily.weathercode[i]),
    })),
  };
}

// Buscando o tempo em algumas cidades brasileiras
async function tempoNoBrasil() {
  const cidades = [
    { nome: "São Paulo", lat: -23.55, lon: -46.63 },
    { nome: "Rio de Janeiro", lat: -22.91, lon: -43.17 },
    { nome: "Curitiba", lat: -25.43, lon: -49.27 },
    { nome: "Recife", lat: -8.05, lon: -34.88 },
    { nome: "Manaus", lat: -3.10, lon: -60.02 },
  ];

  const resultados = await Promise.all(
    cidades.map(async (cidade) => {
      const previsao = await buscarPrevisao(cidade.lat, cidade.lon, 1);
      return { cidade: cidade.nome, ...previsao.agora };
    })
  );

  console.log("
🌡️ TEMPO AGORA NO BRASIL
");
  resultados.forEach(r => {
    console.log(`${r.icone} ${r.cidade}: ${r.temperatura}°C — ${r.desc}`);
  });
}

tempoNoBrasil();

API 4 — PokeAPI: construindo um Pokédex

const POKE_BASE = "https://pokeapi.co/api/v2";

// Cache em memória para evitar requisições repetidas
const cachePokemons = new Map();

async function buscarPokemon(nomeOuId) {
  const chave = String(nomeOuId).toLowerCase();

  if (cachePokemons.has(chave)) {
    return cachePokemons.get(chave);
  }

  const response = await fetch(`${POKE_BASE}/pokemon/${chave}`);

  if (response.status === 404) {
    throw new Error(`Pokémon "${nomeOuId}" não encontrado.`);
  }

  if (!response.ok) throw new Error(`Erro ${response.status}`);

  const dados = await response.json();

  const pokemon = {
    id: dados.id,
    nome: dados.name,
    nomeFormatado: dados.name.charAt(0).toUpperCase() + dados.name.slice(1),
    tipos: dados.types.map(t => t.type.name),
    altura: dados.height / 10,    // decímetros → metros
    peso: dados.weight / 10,      // hectogramas → kg
    habilidades: dados.abilities.map(a => a.ability.name),
    stats: dados.stats.reduce((acc, s) => {
      acc[s.stat.name] = s.base_stat;
      return acc;
    }, {}),
    sprites: {
      frente: dados.sprites.front_default,
      costas: dados.sprites.back_default,
      artwork: dados.sprites.other?.["official-artwork"]?.front_default,
    },
    experienciaBase: dados.base_experience,
  };

  cachePokemons.set(chave, pokemon);
  return pokemon;
}

async function buscarEvolucoes(nomeOuId) {
  // Busca a espécie primeiro
  const especieResp = await fetch(`${POKE_BASE}/pokemon-species/${nomeOuId}`);
  const especie = await especieResp.json();

  // Busca a cadeia de evolução
  const evolucaoResp = await fetch(especie.evolution_chain.url);
  const evolucao = await evolucaoResp.json();

  // Extrai a cadeia recursivamente
  function extrairCadeia(chain) {
    const resultado = [chain.species.name];
    if (chain.evolves_to.length > 0) {
      resultado.push(...extrairCadeia(chain.evolves_to[0]));
    }
    return resultado;
  }

  return extrairCadeia(evolucao.chain);
}

async function comparar(nome1, nome2) {
  const [p1, p2] = await Promise.all([
    buscarPokemon(nome1),
    buscarPokemon(nome2),
  ]);

  const stats = ["hp", "attack", "defense", "speed", "special-attack", "special-defense"];

  console.log(`
⚔️ BATALHA: ${p1.nomeFormatado} vs ${p2.nomeFormatado}
`);
  console.log(`${"STAT".padEnd(20)} ${"".padEnd(10, p1.nomeFormatado.slice(0,10))} ${"".padEnd(10, p2.nomeFormatado.slice(0,10))}`);
  console.log("─".repeat(50));

  let vitorias1 = 0, vitorias2 = 0;

  stats.forEach(stat => {
    const v1 = p1.stats[stat] || 0;
    const v2 = p2.stats[stat] || 0;
    const vencedor = v1 > v2 ? "←" : v1 < v2 ? "→" : "=";
    if (v1 > v2) vitorias1++;
    if (v2 > v1) vitorias2++;
    console.log(`${stat.padEnd(20)} ${String(v1).padEnd(15)} ${v2} ${vencedor}`);
  });

  console.log("─".repeat(50));
  const vencedor = vitorias1 > vitorias2
    ? p1.nomeFormatado
    : vitorias2 > vitorias1
      ? p2.nomeFormatado
      : "Empate";

  console.log(`
🏆 Vencedor nos stats: ${vencedor}`);
}

// Exemplos
const pikachu = await buscarPokemon("pikachu");
console.log(`${pikachu.nomeFormatado} — Tipos: ${pikachu.tipos.join(", ")}`);
console.log(`Altura: ${pikachu.altura}m | Peso: ${pikachu.peso}kg`);

const evolucoes = await buscarEvolucoes("charmander");
console.log(`Cadeia de evolução: ${evolucoes.join(" → ")}`);
// charmander → charmeleon → charizard

await comparar("pikachu", "bulbasaur");

API 5 — DiceBear: gerando avatares dinâmicos

Uma API para gerar avatares SVG únicos — ótimo para perfis de usuário:

function gerarAvatarURL(seed, estilo = "adventurer", opcoes = {}) {
  const {
    tamanho = 128,
    backgroundColor = "b6e3f4",
  } = opcoes;

  const params = new URLSearchParams({
    seed,
    size: tamanho,
    backgroundColor,
  });

  return `https://api.dicebear.com/7.x/${estilo}/svg?${params}`;
}

// Estilos disponíveis
const estilos = [
  "adventurer", "avataaars", "big-smile",
  "bottts", "croodles", "fun-emoji",
  "icons", "identicon", "initials",
  "lorelei", "micah", "miniavs",
  "notionists", "open-peeps", "personas",
  "pixel-art", "rings", "shapes",
  "thumbs",
];

// Gerar avatar baseado no nome do usuário
function avatarParaUsuario(nome, estilo = "adventurer") {
  return gerarAvatarURL(nome, estilo);
}

// Galeria de avatares
async function criarGaleriaAvatares(usuarios) {
  const container = document.querySelector("#galeria");

  usuarios.forEach(usuario => {
    const div = document.createElement("div");
    div.className = "avatar-card";

    const img = document.createElement("img");
    img.src = avatarParaUsuario(usuario.nome);
    img.alt = usuario.nome;
    img.width = 80;
    img.height = 80;
    img.style.borderRadius = "50%";

    const nome = document.createElement("p");
    nome.textContent = usuario.nome;

    div.append(img, nome);
    container.appendChild(div);
  });
}

// Uso
criarGaleriaAvatares([
  { nome: "Ana Paula" },
  { nome: "Carlos Silva" },
  { nome: "Beatriz Costa" },
]);

Aplicação completa — Dashboard multi-API

Vamos combinar ViaCEP, IBGE e Open-Meteo em uma única aplicação:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Dashboard Brasil</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    :root {
      --bg: #0f1117;
      --surface: #1a1d27;
      --surface2: #232637;
      --border: #2e3248;
      --accent: #7c6ff7;
      --text: #e4e6f0;
      --muted: #8b8fa8;
    }

    body {
      font-family: 'Segoe UI', sans-serif;
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      padding: 2rem 1rem;
    }

    .container { max-width: 900px; margin: 0 auto; }

    h1 {
      text-align: center;
      font-size: 1.5rem;
      margin-bottom: 2rem;
      color: var(--text);
    }

    .busca-cep {
      display: flex;
      gap: .75rem;
      margin-bottom: 2rem;
    }

    .busca-cep input {
      flex: 1;
      padding: .75rem 1rem;
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 8px;
      color: var(--text);
      font-size: 1rem;
    }

    .busca-cep input:focus {
      outline: none;
      border-color: var(--accent);
    }

    .busca-cep button {
      padding: .75rem 1.5rem;
      background: var(--accent);
      color: white;
      border: none;
      border-radius: 8px;
      cursor: pointer;
      font-weight: 700;
      font-size: 1rem;
    }

    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
      gap: 1rem;
      margin-bottom: 1.5rem;
    }

    .card {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 12px;
      padding: 1.5rem;
      animation: aparecer .3s ease;
    }

    @keyframes aparecer {
      from { opacity: 0; transform: translateY(-6px); }
      to   { opacity: 1; transform: translateY(0); }
    }

    .card-titulo {
      font-size: .8rem;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: .1em;
      margin-bottom: 1rem;
    }

    .card-valor {
      font-size: 1.75rem;
      font-weight: 700;
      margin-bottom: .25rem;
    }

    .card-sub { font-size: .9rem; color: var(--muted); }

    .endereco-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: .5rem;
    }

    .campo { font-size: .9rem; }
    .campo span { display: block; color: var(--muted); font-size: .75rem; }

    .previsao {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
      gap: .5rem;
    }

    .dia {
      background: var(--surface2);
      border-radius: 8px;
      padding: .75rem .5rem;
      text-align: center;
      font-size: .85rem;
    }

    .dia .icone { font-size: 1.5rem; margin: .25rem 0; }
    .dia .maxima { font-weight: 700; }
    .dia .minima { color: var(--muted); }

    .skeleton {
      background: linear-gradient(90deg, var(--surface2) 25%, var(--border) 50%, var(--surface2) 75%);
      background-size: 200% 100%;
      animation: shimmer 1.5s infinite;
      border-radius: 4px;
      height: 1rem;
      margin: .25rem 0;
    }

    @keyframes shimmer {
      from { background-position: -200% 0; }
      to   { background-position: 200% 0; }
    }

    .erro-card {
      background: rgba(248, 113, 113, .1);
      border: 1px solid rgba(248, 113, 113, .3);
      border-radius: 12px;
      padding: 1rem 1.5rem;
      color: #f87171;
      margin-bottom: 1rem;
      display: none;
    }

    .erro-card.visivel { display: block; }
  </style>
</head>
<body>
<div class="container">
  <h1>🇧🇷 Dashboard Brasil</h1>

  <div class="busca-cep">
    <input type="text" id="input-cep"
      placeholder="Digite um CEP (ex: 01310-100)"
      maxlength="9">
    <button id="btn-buscar">Buscar</button>
  </div>

  <div class="erro-card" id="erro"></div>
  <div id="resultado"></div>
</div>

<script>
  const inputCEP = document.querySelector("#input-cep");
  const btnBuscar = document.querySelector("#btn-buscar");
  const erroEl = document.querySelector("#erro");
  const resultado = document.querySelector("#resultado");

  // ── CEP com máscara ──────────────────────────────
  inputCEP.addEventListener("input", (e) => {
    let v = e.target.value.replace(/D/g, "");
    if (v.length > 5) v = v.slice(0, 5) + "-" + v.slice(5, 8);
    e.target.value = v;
  });

  // ── APIs ─────────────────────────────────────────
  async function buscarCEP(cep) {
    const limpo = cep.replace(/D/g, "");
    if (limpo.length !== 8) throw new Error("CEP deve ter 8 dígitos.");

    const r = await fetch(`https://viacep.com.br/ws/${limpo}/json/`);
    const d = await r.json();
    if (d.erro) throw new Error(`CEP "${cep}" não encontrado.`);
    return d;
  }

  async function buscarTempo(uf) {
    // Coordenadas aproximadas das capitais por UF
    const coords = {
      SP: [-23.55, -46.63], RJ: [-22.91, -43.17], MG: [-19.92, -43.94],
      RS: [-30.03, -51.22], PR: [-25.43, -49.27], SC: [-27.60, -48.55],
      BA: [-12.97, -38.50], PE: [-8.05, -34.88], CE: [-3.73, -38.52],
      PA: [-1.46, -48.50], AM: [-3.10, -60.02], GO: [-16.69, -49.25],
      MT: [-15.60, -56.10], MS: [-20.44, -54.65], ES: [-20.32, -40.34],
      MA: [-2.53, -44.30], RN: [-5.79, -35.21], PB: [-7.12, -34.86],
      AL: [-9.67, -35.74], SE: [-10.91, -37.07], PI: [-5.09, -42.80],
      TO: [-10.25, -48.33], RO: [-8.76, -63.90], AC: [-9.97, -67.81],
      AP: [0.03, -51.07], RR: [2.82, -60.67], DF: [-15.78, -47.93],
    };

    const [lat, lon] = coords[uf] || [-15.78, -47.93];

    const params = new URLSearchParams({
      latitude: lat, longitude: lon,
      current_weather: true,

      daily: "temperature_2m_max,temperature_2m_min,weathercode",
      timezone: "America/Sao_Paulo",
      forecast_days: 5,
    });

    const r = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
    return await r.json();
  }

  async function buscarMunicipios(uf) {
    const r = await fetch(
      `https://servicodados.ibge.gov.br/api/v1/localidades/estados/${uf}/municipios`
    );
    const dados = await r.json();
    return dados.length;
  }

  // ── Renderização ─────────────────────────────────
  function codigoTempo(c) {
    const m = {
      0: "☀️", 1: "🌤️", 2: "⛅", 3: "☁️",
      45: "🌫️", 51: "🌦️", 61: "🌧️", 71: "🌨️",
      80: "⛈️", 95: "🌩️",
    };
    return m[c] || "🌈";
  }

  function renderizarSkeleton() {
    resultado.innerHTML = `
      <div class="grid">
        <div class="card">
          <div class="card-titulo">Carregando endereço...</div>
          <div class="skeleton" style="width:70%"></div>
          <div class="skeleton" style="width:50%"></div>
          <div class="skeleton" style="width:60%"></div>
        </div>
        <div class="card">
          <div class="card-titulo">Carregando clima...</div>
          <div class="skeleton" style="width:40%; height: 2rem"></div>
          <div class="skeleton" style="width:60%"></div>
        </div>
      </div>`;
  }

  function renderizarDados(cep, tempo, totalMunicipios) {
    const { current_weather, daily } = tempo;
    const iconeAgora = codigoTempo(current_weather.weathercode);

    const diasHTML = daily.time.slice(0, 5).map((data, i) => `
      <div class="dia">
        <div>${new Date(data + "T12:00:00").toLocaleDateString("pt-BR", { weekday: "short" })}</div>
        <div class="icone">${codigoTempo(daily.weathercode[i])}</div>
        <div class="maxima">${Math.round(daily.temperature_2m_max[i])}°</div>
        <div class="minima">${Math.round(daily.temperature_2m_min[i])}°</div>
      </div>
    `).join("");

    resultado.innerHTML = `
      <div class="grid">

        <div class="card">
          <div class="card-titulo">📍 Endereço</div>
          <div class="endereco-grid">
            <div class="campo">
              <span>Logradouro</span>
              ${cep.logradouro || "—"}
            </div>
            <div class="campo">
              <span>Bairro</span>
              ${cep.bairro || "—"}
            </div>
            <div class="campo">
              <span>Cidade</span>
              ${cep.localidade}
            </div>
            <div class="campo">
              <span>Estado</span>
              ${cep.uf}
            </div>
            <div class="campo">
              <span>DDD</span>
              (${cep.ddd})
            </div>
            <div class="campo">
              <span>Código IBGE</span>
              ${cep.ibge}
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-titulo">🌡️ Clima Agora (capital)</div>
          <div class="card-valor">${iconeAgora} ${current_weather.temperature}°C</div>
          <div class="card-sub">Vento: ${current_weather.windspeed} km/h</div>
        </div>

        <div class="card">
          <div class="card-titulo">🏙️ Estado em números</div>
          <div class="card-valor">${totalMunicipios.toLocaleString("pt-BR")}</div>
          <div class="card-sub">Municípios em ${cep.uf}</div>
        </div>

      </div>

      <div class="card">
        <div class="card-titulo">📅 Previsão para os próximos 5 dias</div>
        <div class="previsao" style="margin-top:.75rem">${diasHTML}</div>
      </div>
    `;
  }

  // ── Busca principal ───────────────────────────────
  async function buscar() {
    const cep = inputCEP.value.trim();
    if (!cep) return;

    erroEl.classList.remove("visivel");
    renderizarSkeleton();
    btnBuscar.disabled = true;
    btnBuscar.textContent = "Buscando...";

    try {
      // Busca CEP primeiro (necessário para saber o estado)
      const dadosCEP = await buscarCEP(cep);

      // Busca clima e municípios em paralelo
      const [tempo, totalMunicipios] = await Promise.all([
        buscarTempo(dadosCEP.uf),
        buscarMunicipios(dadosCEP.uf),
      ]);

      renderizarDados(dadosCEP, tempo, totalMunicipios);

    } catch (erro) {
      resultado.innerHTML = "";
      erroEl.textContent = `⚠️ ${erro.message}`;
      erroEl.classList.add("visivel");
    } finally {
      btnBuscar.disabled = false;
      btnBuscar.textContent = "Buscar";
    }
  }

  btnBuscar.addEventListener("click", buscar);

  inputCEP.addEventListener("keydown", (e) => {
    if (e.key === "Enter") buscar();
  });

  // Carrega um CEP de exemplo
  inputCEP.value = "01310-100";
  buscar();
</script>
</body>
</html>

Boas práticas ao consumir APIs públicas

// ✅ 1. Respeite os rate limits — não faça spam de requisições
// Use debounce em campos de busca

// ✅ 2. Faça cache de respostas que não mudam frequentemente
const cache = new Map();
async function comCache(url, ttl = 60000) {
  if (cache.has(url)) {
    const { dados, timestamp } = cache.get(url);
    if (Date.now() - timestamp < ttl) return dados;
  }
  const dados = await fetch(url).then(r => r.json());
  cache.set(url, { dados, timestamp: Date.now() });
  return dados;
}

// ✅ 3. Sempre mostre loading e trate erros visualmente
// ✅ 4. Use Promise.all para requisições independentes
// ✅ 5. Documente quais APIs você usa e seus termos de uso
// ✅ 6. Prepare um fallback para quando a API estiver fora
async function comFallback(fn, fallback) {
  try {
    return await fn();
  } catch {
    return fallback;
  }
}

Tarefa para você

Usando as APIs deste artigo, construa um cartão de perfil de desenvolvedor:

Dados a exibir:
1. Avatar gerado pelo DiceBear com o nome do usuário
2. Campo de CEP com preenchimento automático via ViaCEP
3. Temperatura atual da capital do estado detectado via Open-Meteo
4. Um campo de username do GitHub que busca os repositórios do usuário
5. Exibir os 3 repos com mais estrelas

Extras:
- Skeleton loading em todos os campos
- Tratamento de erro em cada seção independentemente
  (se o GitHub falhar, o resto ainda funciona)
- Botão "Gerar PDF" que exporta o cartão como CSV
  com nome, cidade, estado e repos

Conclusão

Neste artigo você aprendeu:

  • Como consumir a ViaCEP para preenchimento automático de endereços
  • Como usar a API do IBGE para dados geográficos brasileiros
  • Como buscar previsão do tempo com Open-Meteo sem precisar de chave
  • Como construir um Pokédex com PokeAPI incluindo cache e comparação
  • Como gerar avatares dinâmicos com DiceBear
  • Como combinar múltiplas APIs em uma única aplicação
  • Boas práticas de cache, debounce e tratamento de erros com APIs reais

No próximo artigo encerramos o Módulo 3 com uma revisão completa e um mini projeto que une tudo que aprendemos sobre JavaScript assíncrono e APIs.


 

📚 Fontes e Referências

  • ViaCEP — Documentação: https://viacep.com.br
  • IBGE API — Documentação: https://servicodados.ibge.gov.br/api/docs
  • Open-Meteo — Documentação: https://open-meteo.com/en/docs
  • PokeAPI — Documentação: https://pokeapi.co/docs/v2
  • DiceBear — Documentação: https://www.dicebear.com/introduction
  • JSONPlaceholder — Documentação: https://jsonplaceholder.typicode.com
  • MDN Web Docs — Using Fetch: https://developer.mozilla.org/pt-BR/docs/Web/API/Fetch_API/Using_Fetch
  • You Don't Know JS: Async & Performance — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS
Comentários

Mais em Javascript

MongoDB e Mongoose: banco de dados com Node
MongoDB e Mongoose: banco de dados com Node

Até agora nossa API guardava dados em um array na memória — tudo sumia quando...

Projeto: API REST com autenticação JWT
Projeto: API REST com autenticação JWT

Chegamos ao fim do Módulo 4. Em seis artigos você saiu do zero no Node.js até...

Testes automatizados com Jest
Testes automatizados com Jest

Existe um momento na vida de todo desenvolvedor em que ele mexe em um código...