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

Introdução ao Node.js: JavaScript fora do navegador
Introdução ao Node.js: JavaScript fora do navegador

Até agora todo o JavaScript que escrevemos rodou no navegador. O navegador é...

LocalStorage e SessionStorage
LocalStorage e SessionStorage

Imagine que o usu&aacute;rio passou dez minutos preenchendo uma lista de tare...

Revisão  + Projeto Final: Produção Real
Revisão + Projeto Final: Produção Real

Chegamos ao fim de mais um módulo. Em três artigos cobrimos deploy, segurança...