Javascript

Fetch API: consumindo dados da internet Já leu

25 min de leitura

Fetch API: consumindo dados da internet
Até agora simulamos requisições com setTimeout. A partir deste artigo, vamos buscar dados reais da internet. A Fetch API é a fo

Até agora simulamos requisições com setTimeout. A partir deste artigo, vamos buscar dados reais da internet. A Fetch API é a forma moderna e nativa do JavaScript para fazer requisições HTTP — substituindo o antigo XMLHttpRequest com uma interface muito mais limpa baseada em Promises.

Com Fetch você vai conseguir consumir APIs públicas, enviar dados para servidores, fazer login, carregar imagens, e muito mais. É uma das habilidades mais importantes do desenvolvimento web moderno.


O básico — uma requisição GET

// fetch() retorna uma Promise
fetch("https://jsonplaceholder.typicode.com/users/1")
  .then(response => response.json()) // converte a resposta para JSON
  .then(usuario => console.log(usuario))
  .catch(erro => console.error("Erro:", erro));

// Com async/await — muito mais limpo
async function buscarUsuario() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const usuario = await response.json();
  console.log(usuario);
}

buscarUsuario();

O fetch retorna uma Promise com um objeto Response. Esse objeto não é diretamente os dados — você precisa chamar .json() para converter o corpo da resposta.


O objeto Response

O Response tem várias propriedades e métodos importantes:

async function inspecionarResponse() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");

  // Status HTTP
  console.log(response.status);     // 200
  console.log(response.statusText); // "OK"
  console.log(response.ok);         // true (200-299), false para erros

  // Headers
  console.log(response.headers.get("content-type")); // "application/json; charset=utf-8"

  // URL final (após redirects)
  console.log(response.url);

  // Métodos para ler o corpo — só pode chamar UM por resposta
  const json = await response.json();     // parse JSON
  // ou
  const texto = await response.text();   // texto puro
  // ou
  const blob = await response.blob();    // arquivo binário (imagens, PDFs)
  // ou
  const buffer = await response.arrayBuffer(); // dados binários brutos
}

O erro mais comum com Fetch

Fetch não rejeita a Promise em erros HTTP (404, 500, etc.). Ele só rejeita em falhas de rede. Você precisa verificar response.ok manualmente:

async function buscarComVerificacao(url) {
  const response = await fetch(url);

  // ❌ Sem verificação — passa silenciosamente mesmo com erro 404
  const dados = await response.json();

  // ✅ Com verificação correta
  if (!response.ok) {
    throw new Error(`Erro HTTP: ${response.status} — ${response.statusText}`);
  }

  return await response.json();
}

// Testando
async function main() {
  try {
    // Rota que não existe — retorna 404
    const dados = await buscarComVerificacao(
      "https://jsonplaceholder.typicode.com/users/99999"
    );
    console.log(dados);
  } catch (erro) {
    console.error(erro.message); // Erro HTTP: 404 — Not Found
  }
}

Criando um wrapper robusto para Fetch

Uma função auxiliar que você vai querer ter em todos os seus projetos:

async function requisitar(url, opcoes = {}) {
  try {
    const response = await fetch(url, opcoes);

    // Verifica erros HTTP
    if (!response.ok) {
      const erro = new Error(`Erro ${response.status}: ${response.statusText}`);
      erro.status = response.status;
      erro.url = url;
      throw erro;
    }

    // Se não há conteúdo (204 No Content), retorna null
    if (response.status === 204) return null;

    // Detecta o tipo e faz o parse correto
    const contentType = response.headers.get("content-type") || "";
    if (contentType.includes("application/json")) {
      return await response.json();
    }

    return await response.text();

  } catch (erro) {
    // Falha de rede (sem internet, CORS, etc.)
    if (erro.name === "TypeError") {
      throw new Error("Falha de rede. Verifique sua conexão.");
    }
    throw erro;
  }
}

// Uso limpo
const usuario = await requisitar("https://jsonplaceholder.typicode.com/users/1");
console.log(usuario.name);

Fazendo requisições POST, PUT, DELETE

O segundo argumento do fetch é um objeto de opções que configura o método, headers e corpo:

const BASE_URL = "https://jsonplaceholder.typicode.com";

// POST — criar um recurso
async function criarPost(dados) {
  const response = await fetch(`${BASE_URL}/posts`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(dados),
  });

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

// PUT — substituir um recurso completo
async function atualizarPost(id, dados) {
  const response = await fetch(`${BASE_URL}/posts/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(dados),
  });

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

// PATCH — atualização parcial
async function atualizarTitulo(id, titulo) {
  const response = await fetch(`${BASE_URL}/posts/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title: titulo }),
  });

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

// DELETE — remover um recurso
async function deletarPost(id) {
  const response = await fetch(`${BASE_URL}/posts/${id}`, {
    method: "DELETE",
  });

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

// Testando
async function main() {
  // Criar
  const novoPost = await criarPost({
    title: "Meu artigo sobre JavaScript",
    body: "JavaScript é incrível...",
    userId: 1,
  });
  console.log("Criado:", novoPost);

  // Atualizar título
  const atualizado = await atualizarTitulo(1, "Novo título");
  console.log("Atualizado:", atualizado.title);

  // Deletar
  await deletarPost(1);
  console.log("Post deletado.");
}

main();

Headers e autenticação

// Token JWT — o padrão mais comum de autenticação em APIs
async function buscarPerfilAutenticado(token) {
  const response = await fetch("https://api.exemplo.com/perfil", {
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
  });

  if (response.status === 401) {
    throw new Error("Token expirado. Faça login novamente.");
  }

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

// API Key — outro padrão comum
async function buscarClimaComApiKey(cidade) {
  const API_KEY = "sua_chave_aqui";
  const url = `https://api.openweathermap.org/data/2.5/weather?q=${cidade}&appid=${API_KEY}&lang=pt_br&units=metric`;

  const response = await fetch(url);
  if (!response.ok) throw new Error(`Cidade "${cidade}" não encontrada.`);
  return await response.json();
}

AbortController — cancelando requisições

Às vezes você precisa cancelar uma requisição em andamento — quando o usuário navega para outra página ou digita algo novo na busca:

let controlador = null;

async function buscarComCancelamento(termo) {
  // Cancela a requisição anterior se ainda estiver em andamento
  if (controlador) {
    controlador.abort();
  }

  controlador = new AbortController();

  try {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts?q=${termo}`,
      { signal: controlador.signal }
    );

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

  } catch (erro) {
    if (erro.name === "AbortError") {
      console.log("Requisição cancelada.");
      return null;
    }
    throw erro;
  }
}

// Uso com busca ao vivo
const input = document.querySelector("#busca");
input.addEventListener("input", async (e) => {
  const resultado = await buscarComCancelamento(e.target.value);
  if (resultado) exibirResultados(resultado);
});

Exemplo completo — app de busca de usuários do GitHub

Vamos construir uma aplicação real que consome a API pública do GitHub:

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

    :root {
      --bg: #0d1117;
      --surface: #161b22;
      --surface2: #21262d;
      --border: #30363d;
      --accent: #58a6ff;
      --text: #c9d1d9;
      --muted: #8b949e;
      --green: #3fb950;
    }

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

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

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

    .busca {
      position: relative;
      margin-bottom: 1.5rem;
    }

    .busca input {
      width: 100%;
      padding: .75rem 1rem .75rem 2.75rem;
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 8px;
      color: var(--text);
      font-size: 1rem;
      transition: border-color .2s;
    }

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

    .busca-icone {
      position: absolute;
      left: .9rem;
      top: 50%;
      transform: translateY(-50%);
      color: var(--muted);
    }

    .loading {
      text-align: center;
      color: var(--muted);
      padding: 2rem;
      display: none;
    }

    .loading.visivel { display: block; }

    .erro {
      background: rgba(248, 81, 73, .1);
      border: 1px solid rgba(248, 81, 73, .4);
      border-radius: 8px;
      padding: 1rem;
      color: #f85149;
      display: none;
      margin-bottom: 1rem;
    }

    .erro.visivel { display: block; }

    /* Card do usuário */
    .card-usuario {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 12px;
      padding: 1.5rem;
      margin-bottom: 1.5rem;
      display: none;
      animation: aparecer .3s ease;
    }

    .card-usuario.visivel { display: block; }

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

    .usuario-topo {
      display: flex;
      gap: 1rem;
      align-items: center;
      margin-bottom: 1rem;
    }

    .avatar {
      width: 72px;
      height: 72px;
      border-radius: 50%;
      border: 2px solid var(--border);
    }

    .usuario-info h2 { font-size: 1.1rem; }

    .usuario-info a {
      color: var(--accent);
      text-decoration: none;
      font-size: .9rem;
    }

    .usuario-info a:hover { text-decoration: underline; }

    .bio {
      color: var(--muted);
      font-size: .9rem;
      line-height: 1.5;
      margin-bottom: 1rem;
    }

    .stats {
      display: flex;
      gap: 1.5rem;
      margin-bottom: 1rem;
    }

    .stat { text-align: center; }

    .stat-valor {
      display: block;
      font-size: 1.1rem;
      font-weight: 700;
      color: var(--text);
    }

    .stat-label {
      font-size: .75rem;
      color: var(--muted);
    }

    .tags {
      display: flex;
      flex-wrap: wrap;
      gap: .5rem;
      margin-bottom: 1rem;
    }

    .tag {
      background: var(--surface2);
      border: 1px solid var(--border);
      border-radius: 999px;
      padding: .2rem .75rem;
      font-size: .8rem;
      color: var(--muted);
    }

    /* Repositórios */
    .repos-titulo {
      font-size: .9rem;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: .05em;
      margin-bottom: .75rem;
    }

    .repo {
      background: var(--surface2);
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: .85rem 1rem;
      margin-bottom: .5rem;
      animation: aparecer .2s ease;
    }

    .repo-nome {
      color: var(--accent);
      font-weight: 600;
      font-size: .95rem;
      text-decoration: none;
    }

    .repo-nome:hover { text-decoration: underline; }

    .repo-desc {
      color: var(--muted);
      font-size: .85rem;
      margin: .3rem 0;
      line-height: 1.4;
    }

    .repo-meta {
      display: flex;
      gap: 1rem;
      font-size: .8rem;
      color: var(--muted);
      margin-top: .4rem;
    }

    .repo-lang { color: var(--green); }
    .repo-stars::before { content: "⭐ "; }
    .repo-forks::before { content: "🍴 "; }
  </style>
</head>
<body>
<div class="container">
  <h1>🐙 GitHub User Search</h1>

  <div class="busca">
    <span class="busca-icone">🔍</span>
    <input type="text" id="input-busca" placeholder="Digite um usuário do GitHub...">
  </div>

  <div class="erro" id="erro"></div>
  <div class="loading" id="loading">⏳ Buscando...</div>

  <div class="card-usuario" id="card-usuario">
    <div class="usuario-topo">
      <img class="avatar" id="avatar" src="" alt="Avatar">
      <div class="usuario-info">
        <h2 id="nome-completo"></h2>
        <a id="link-perfil" href="" target="_blank" id="login"></a>
      </div>
    </div>
    <p class="bio" id="bio"></p>
    <div class="stats">
      <div class="stat">
        <span class="stat-valor" id="stat-repos"></span>
        <span class="stat-label">Repos</span>
      </div>
      <div class="stat">
        <span class="stat-valor" id="stat-seguidores"></span>
        <span class="stat-label">Seguidores</span>
      </div>
      <div class="stat">
        <span class="stat-valor" id="stat-seguindo"></span>
        <span class="stat-label">Seguindo</span>
      </div>
    </div>
    <div class="tags" id="tags"></div>
    <p class="repos-titulo">Repositórios populares</p>
    <div id="lista-repos"></div>
  </div>
</div>

<script>
  // ── Referências ──────────────────────────────────
  const inputBusca = document.querySelector("#input-busca");
  const cardUsuario = document.querySelector("#card-usuario");
  const erroEl = document.querySelector("#erro");
  const loadingEl = document.querySelector("#loading");

  // ── Utilitários de UI ────────────────────────────
  function mostrarLoading() {
    loadingEl.classList.add("visivel");
    cardUsuario.classList.remove("visivel");
    erroEl.classList.remove("visivel");
  }

  function mostrarErro(mensagem) {
    loadingEl.classList.remove("visivel");
    erroEl.textContent = mensagem;
    erroEl.classList.add("visivel");
    cardUsuario.classList.remove("visivel");
  }

  function mostrarCard() {
    loadingEl.classList.remove("visivel");
    erroEl.classList.remove("visivel");
    cardUsuario.classList.add("visivel");
  }

  // ── API do GitHub ────────────────────────────────
  async function buscarUsuarioGitHub(login) {
    const response = await fetch(`https://api.github.com/users/${login}`, {
      headers: { "Accept": "application/vnd.github.v3+json" },
    });

    if (response.status === 404) throw new Error(`Usuário "${login}" não encontrado.`);
    if (response.status === 403) throw new Error("Limite de requisições atingido. Aguarde um momento.");
    if (!response.ok) throw new Error(`Erro ${response.status} ao buscar usuário.`);

    return await response.json();
  }

  async function buscarReposGitHub(login) {
    const response = await fetch(
      `https://api.github.com/users/${login}/repos?sort=stars&per_page=5`,
      { headers: { "Accept": "application/vnd.github.v3+json" } }
    );

    if (!response.ok) return [];
    return await response.json();
  }

  // ── Renderização ─────────────────────────────────
  function renderizarUsuario(usuario, repos) {
    // Dados básicos
    document.querySelector("#avatar").src = usuario.avatar_url;
    document.querySelector("#avatar").alt = usuario.login;
    document.querySelector("#nome-completo").textContent = usuario.name || usuario.login;

    const linkPerfil = document.querySelector("#link-perfil");
    linkPerfil.textContent = `@${usuario.login}`;
    linkPerfil.href = usuario.html_url;

    document.querySelector("#bio").textContent = usuario.bio || "Sem bio disponível.";

    // Stats
    document.querySelector("#stat-repos").textContent =
      usuario.public_repos.toLocaleString("pt-BR");
    document.querySelector("#stat-seguidores").textContent =
      usuario.followers.toLocaleString("pt-BR");
    document.querySelector("#stat-seguindo").textContent =
      usuario.following.toLocaleString("pt-BR");

    // Tags
    const tagsEl = document.querySelector("#tags");
    tagsEl.innerHTML = "";
    const infos = [
      usuario.location && `📍 ${usuario.location}`,
      usuario.company && `🏢 ${usuario.company}`,
      usuario.blog && `🔗 Blog`,
      usuario.twitter_username && `🐦 @${usuario.twitter_username}`,
    ].filter(Boolean);

    infos.forEach(info => {
      const tag = document.createElement("span");
      tag.classList.add("tag");
      tag.textContent = info;
      tagsEl.appendChild(tag);
    });

    // Repositórios
    const listaRepos = document.querySelector("#lista-repos");
    listaRepos.innerHTML = "";

    if (repos.length === 0) {
      listaRepos.innerHTML = '<p style="color: var(--muted); font-size: .9rem;">Nenhum repositório público.</p>';
      return;
    }

    repos.forEach(repo => {
      const div = document.createElement("div");
      div.classList.add("repo");
      div.innerHTML = `
        <a class="repo-nome" href="${repo.html_url}" target="_blank">${repo.name}</a>
        ${repo.description
          ? `<p class="repo-desc">${repo.description}</p>`
          : ""
        }
        <div class="repo-meta">
          ${repo.language
            ? `<span class="repo-lang">${repo.language}</span>`
            : ""
          }
          <span class="repo-stars">${repo.stargazers_count.toLocaleString("pt-BR")}</span>
          <span class="repo-forks">${repo.forks_count.toLocaleString("pt-BR")}</span>
        </div>
      `;
      listaRepos.appendChild(div);
    });
  }

  // ── Busca principal ──────────────────────────────
  let controlador = null;

  async function buscar(login) {
    if (!login.trim()) {
      cardUsuario.classList.remove("visivel");
      erroEl.classList.remove("visivel");
      return;
    }

    if (controlador) controlador.abort();
    controlador = new AbortController();

    mostrarLoading();

    try {
      // Busca usuário e repos em paralelo
      const [usuario, repos] = await Promise.all([
        buscarUsuarioGitHub(login),
        buscarReposGitHub(login),
      ]);

      renderizarUsuario(usuario, repos);
      mostrarCard();

    } catch (erro) {
      if (erro.name === "AbortError") return;
      mostrarErro(erro.message);
    }
  }

  // ── Debounce para não buscar a cada tecla ────────
  function debounce(fn, delay) {
    let timer;
    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => fn(...args), delay);
    };
  }

  const buscarDebounced = debounce(buscar, 600);

  inputBusca.addEventListener("input", (e) => {
    buscarDebounced(e.target.value.trim());
  });

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

  // Busca um usuário famoso para demonstração
  inputBusca.value = "torvalds";
  buscar("torvalds");
</script>
</body>
</html>

Boas práticas com Fetch

// ✅ 1. Sempre verifique response.ok
if (!response.ok) throw new Error(`Erro ${response.status}`);

// ✅ 2. Sempre use try/catch com async/await
try {
  const dados = await fetch(url).then(r => r.json());
} catch (erro) {
  tratarErro(erro);
}

// ✅ 3. Nunca exponha API keys no frontend
// Use variáveis de ambiente e proxies de backend

// ✅ 4. Use AbortController em buscas ao vivo
// para cancelar requisições desatualizadas

// ✅ 5. Mostre feedback de loading ao usuário
// sempre que uma requisição estiver em andamento

// ✅ 6. Implemente retry para falhas temporárias
// especialmente em requisições críticas

// ✅ 7. Cache respostas quando possível
const cache = new Map();

async function buscarComCache(url) {
  if (cache.has(url)) {
    return cache.get(url);
  }
  const dados = await fetch(url).then(r => r.json());
  cache.set(url, dados);
  return dados;
}

Tarefa para você

Use a API pública do PokeAPI (https://pokeapi.co/api/v2/) para construir:

// 1. Função buscarPokemon(nome) que retorna:
//    { nome, id, tipos, altura, peso, habilidades, sprite }

// 2. Função buscarTipoPokemon(tipo) que retorna
//    os primeiros 10 pokémons daquele tipo

// 3. Crie uma interface HTML simples com:
//    - Campo de busca por nome
//    - Exibição do sprite (imagem)
//    - Listagem de tipos com cores diferentes para cada tipo
//    - Botão "Pokémon aleatório" que busca um ID entre 1 e 898

// Dica: a URL base é https://pokeapi.co/api/v2/pokemon/{nome-ou-id}
// Não precisa de API key — é totalmente pública e gratuita

Conclusão

Neste artigo você aprendeu:

  • Como usar fetch() para fazer requisições HTTP
  • O objeto Response e seus métodos
  • Por que é necessário verificar response.ok
  • Como fazer requisições GET, POST, PUT, PATCH e DELETE
  • Como enviar e receber JSON
  • Como usar headers e autenticação com tokens
  • AbortController para cancelar requisições
  • Como construir um wrapper robusto para o fetch
  • Boas práticas de performance com cache e debounce
  • Uma aplicação real consumindo a API do GitHub

No próximo artigo vamos estudar o JSON em profundidade — serialização, parsing, manipulação e os casos especiais que todo desenvolvedor precisa conhecer.


📌 Próximo artigo: Aula 23 — Trabalhando com JSON


📚 Fontes e Referências

  • MDN Web Docs — Fetch API: https://developer.mozilla.org/pt-BR/docs/Web/API/Fetch_API
  • MDN Web Docs — Using Fetch: https://developer.mozilla.org/pt-BR/docs/Web/API/Fetch_API/Using_Fetch
  • MDN Web Docs — Response: https://developer.mozilla.org/pt-BR/docs/Web/API/Response
  • MDN Web Docs — AbortController: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
  • JavaScript.info — Fetch: https://javascript.info/fetch
  • JavaScript.info — Fetch: Abort: https://javascript.info/fetch-abort
  • GitHub REST API Docs: https://docs.github.com/en/rest
  • PokeAPI Docs: https://pokeapi.co/docs/v2
  • web.dev — Introduction to fetch: https://web.dev/introduction-to-fetch

Prof. Ricardo Matos

Mestre em Programação | Série: Dominando JavaScript em 1 Ano — Artigo 22/52


Quer a Aula 23 — Trabalhando com JSON agora?


# you asked

Não sei se não seria bom mostrar como era o AJAX - XMLHttpRequest, como era antigamente e comparar com o fetch. SE não for uma boa ideia, prossiga com os artigos


# claude response

Ótima ideia! Vale muito a pena mostrar a evolução histórica — entender o "antes" dá contexto e valor ao "depois". Vou inserir esse artigo como 22-B, sem quebrar a numeração da série.


# 📘 Série: Dominando o JavaScript em 1 Ano

Artigo 22-B — A evolução das requisições: de XMLHttpRequest ao Fetch


Por Prof. Ricardo Matos | Mestre em Programação

Módulo 3 — JavaScript Assíncrono e APIs


Introdução

No artigo anterior aprendemos a Fetch API — moderna, elegante e baseada em Promises. Mas ela não surgiu do nada. Por mais de uma década, toda comunicação assíncrona entre o navegador e um servidor era feita com o XMLHttpRequest (XHR).

Entender essa história tem valor prático: você vai encontrar código legado com XHR em projetos reais. E mais importante — entender o problema que cada ferramenta resolve faz você apreciar e usar melhor o que temos hoje.


O começo: páginas que recarregavam tudo

Antes de 1999, toda interação com o servidor exigia recarregar a página inteira. Você clicava em "Ver mais comentários" e a página inteira era recarregada — perdendo scroll, estado, e causando aquela piscada incômoda.

O problema era simples: o HTTP é um protocolo de requisição-resposta. O navegador pedia, o servidor respondia com uma página HTML completa, e o navegador a renderizava do zero.


XMLHttpRequest — a revolução silenciosa (1999–2015)

Em 1999, a Microsoft introduziu o XMLHTTP no Internet Explorer 5 para o Outlook Web Access. Em 2006, o W3C padronizou como XMLHttpRequest. E em 2005, Jesse James Garrett cunhou o termo AJAXAsynchronous JavaScript and XML — descrevendo a técnica de atualizar partes de uma página sem recarregar tudo.

O Gmail (2004) e o Google Maps (2005) mostraram ao mundo o que era possível. A web nunca mais foi a mesma.

Veja como era uma requisição com XHR:

// Jeito clássico — XMLHttpRequest puro
var xhr = new XMLHttpRequest();

// Configura a requisição
xhr.open("GET", "https://jsonplaceholder.typicode.com/users/1", true);
// true = assíncrono

// Define o que fazer quando a resposta chegar
xhr.onreadystatechange = function() {
  // readyState 4 = requisição completa
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      var usuario = JSON.parse(xhr.responseText);
      console.log("Nome:", usuario.name);
    } else {
      console.error("Erro:", xhr.status);
    }
  }
};

// Define handlers de erro
xhr.onerror = function() {
  console.error("Falha de rede.");
};

// Envia a requisição
xhr.send();

Funciona. Mas note o quanto é verboso e callback-driven.


Os estados do XMLHttpRequest

O XHR tinha cinco estados de progresso — e você precisava verificar manualmente:

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function() {
  switch (xhr.readyState) {
    case 0: console.log("UNSENT — objeto criado, open() não chamado"); break;
    case 1: console.log("OPENED — open() foi chamado"); break;
    case 2: console.log("HEADERS_RECEIVED — headers recebidos"); break;
    case 3: console.log("LOADING — corpo chegando"); break;
    case 4: console.log("DONE — tudo concluído"); break;
  }
};

xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
xhr.send();

Na prática, você só se importava com o estado 4. Mas a API te expunha tudo isso de qualquer forma.


POST com XMLHttpRequest

Enviar dados para o servidor era ainda mais trabalhoso:

var xhr = new XMLHttpRequest();
xhr.open("POST", "https://jsonplaceholder.typicode.com/posts", true);

// Precisa setar o header manualmente
xhr.setRequestHeader("Content-Type", "application/json");

xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 201) {
    var resposta = JSON.parse(xhr.responseText);
    console.log("Post criado com id:", resposta.id);
  }
};

// Precisa serializar manualmente
xhr.send(JSON.stringify({
  title: "Meu post",
  body: "Conteúdo do post",
  userId: 1
}));

jQuery.ajax() — a salvação dos anos 2000

O jQuery chegou em 2006 e abstraiu toda essa verbosidade. Por anos, foi a forma dominante de fazer requisições:

// jQuery — muito mais simples que o XHR puro
$.ajax({
  url: "https://jsonplaceholder.typicode.com/users/1",
  method: "GET",
  dataType: "json",
  success: function(usuario) {
    console.log("Nome:", usuario.name);
  },
  error: function(xhr, status, erro) {
    console.error("Erro:", erro);
  }
});

// Versão mais curta
$.getJSON("https://jsonplaceholder.typicode.com/users/1", function(usuario) {
  console.log(usuario.name);
});

// POST com jQuery
$.post("https://jsonplaceholder.typicode.com/posts",
  { title: "Meu post", userId: 1 },
  function(resposta) {
    console.log("Criado:", resposta.id);
  }
);

O jQuery resolveu a verbosidade mas ainda era baseado em callbacks — e quando você precisava encadear múltiplas requisições, o Callback Hell voltava.


O Callback Hell do AJAX

Qualquer fluxo com múltiplas requisições dependentes virava isso:

// Autenticação → perfil → permissões → dashboard
// Tudo com jQuery AJAX aninhado

$.ajax({
  url: "/api/login",
  method: "POST",
  data: { email: email, senha: senha },
  success: function(auth) {

    $.ajax({
      url: "/api/perfil/" + auth.userId,
      headers: { "Authorization": "Bearer " + auth.token },
      success: function(perfil) {

        $.ajax({
          url: "/api/permissoes/" + perfil.cargo,
          success: function(permissoes) {

            $.ajax({
              url: "/api/dashboard",
              success: function(dashboard) {
                // finalmente chegamos aqui
                renderizarApp(perfil, permissoes, dashboard);
              },
              error: function() { mostrarErro("Erro ao carregar dashboard"); }
            });

          },
          error: function() { mostrarErro("Erro ao carregar permissões"); }
        });

      },
      error: function() { mostrarErro("Erro ao carregar perfil"); }
    });

  },
  error: function() { mostrarErro("Login inválido"); }
});

Insuportável. E esse era o padrão real de aplicações complexas nos anos 2000 e início dos 2010.


jQuery Deferred — a proto-Promise (2010)

O jQuery tentou resolver isso com os objetos Deferred, que eram uma espécie de Promise antes das Promises existirem:

function buscarUsuario(id) {
  return $.getJSON("/api/usuario/" + id); // retorna um Deferred
}

function buscarPedidos(usuarioId) {
  return $.getJSON("/api/pedidos/" + usuarioId);
}

// Encadeamento — muito melhor que callbacks aninhados
buscarUsuario(1)
  .then(function(usuario) {
    console.log("Usuário:", usuario.nome);
    return buscarPedidos(usuario.id);
  })
  .then(function(pedidos) {
    console.log("Pedidos:", pedidos.length);
  })
  .fail(function(erro) {
    console.error("Erro:", erro);
  });

// Em paralelo
$.when(buscarUsuario(1), buscarPedidos(1))
  .done(function(usuario, pedidos) {
    console.log(usuario[0].nome, pedidos[0].length);
  });

Era melhor — mas a API do jQuery Deferred tinha inconsistências e não era compatível com as Promises nativas que vieram depois.


A linha do tempo completa

1999 — Microsoft cria XMLHTTP no IE5
2004 — Gmail usa AJAX extensivamente pela primeira vez
2005 — Google Maps. Jesse James Garrett cunha o termo "AJAX"
2006 — jQuery 1.0 — $.ajax() simplifica tudo
2006 — W3C padroniza o XMLHttpRequest
2010 — jQuery Deferred — proto-Promises
2012 — Promises/A+ — especificação da comunidade
2015 — ES6: Promise nativa + Fetch API chegam ao padrão
2017 — ES2017: async/await
2022 — XMLHttpRequest ainda existe, mas é considerado legado

A comparação direta — mesma operação, três eras

Buscar um usuário e seus posts, tratar erros:

// ── ERA 1: XMLHttpRequest puro (1999–2006) ──────────
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/users/1", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      var usuario = JSON.parse(xhr.responseText);

      var xhr2 = new XMLHttpRequest();
      xhr2.open("GET",
        "https://jsonplaceholder.typicode.com/posts?userId=" + usuario.id,
        true
      );
      xhr2.onreadystatechange = function() {
        if (xhr2.readyState === 4) {
          if (xhr2.status === 200) {
            var posts = JSON.parse(xhr2.responseText);
            console.log(usuario.name + " tem " + posts.length + " posts");
          } else {
            console.error("Erro ao buscar posts:", xhr2.status);
          }
        }
      };
      xhr2.send();

    } else {
      console.error("Erro ao buscar usuário:", xhr.status);
    }
  }
};
xhr.send();


// ── ERA 2: jQuery.ajax (2006–2015) ──────────────────
$.getJSON("https://jsonplaceholder.typicode.com/users/1")
  .then(function(usuario) {
    return $.getJSON(
      "https://jsonplaceholder.typicode.com/posts?userId=" + usuario.id
    ).then(function(posts) {
      return { usuario: usuario, posts: posts };
    });
  })
  .done(function(dados) {
    console.log(dados.usuario.name + " tem " + dados.posts.length + " posts");
  })
  .fail(function(erro) {
    console.error("Erro:", erro.statusText);
  });


// ── ERA 3: Fetch + async/await (2017–hoje) ──────────
async function carregarDados() {
  try {
    const resUsuario = await fetch(
      "https://jsonplaceholder.typicode.com/users/1"
    );
    if (!resUsuario.ok) throw new Error(`Erro ${resUsuario.status}`);
    const usuario = await resUsuario.json();

    const resPosts = await fetch(
      `https://jsonplaceholder.typicode.com/posts?userId=${usuario.id}`
    );
    if (!resPosts.ok) throw new Error(`Erro ${resPosts.status}`);
    const posts = await resPosts.json();

    console.log(`${usuario.name} tem ${posts.length} posts`);
  } catch (erro) {
    console.error("Erro:", erro.message);
  }
}

carregarDados();

A progressão é clara: de 30 linhas aninhadas e confusas para 15 linhas lineares e legíveis.


Quando você ainda vai ver XHR

O XHR ainda tem um caso de uso relevante hoje: monitorar o progresso de upload de arquivos — algo que o Fetch ainda não suporta nativamente:

function uploadComProgresso(arquivo, url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append("arquivo", arquivo);

    // Este evento SÓ existe no XHR — Fetch não tem equivalente ainda
    xhr.upload.addEventListener("progress", (evento) => {
      if (evento.lengthComputable) {
        const porcentagem = Math.round((evento.loaded / evento.total) * 100);
        console.log(`Upload: ${porcentagem}%`);
        atualizarBarraProgresso(porcentagem);
      }
    });

    xhr.addEventListener("load", () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(`Erro ${xhr.status}`));
      }
    });

    xhr.addEventListener("error", () => reject(new Error("Falha de rede")));
    xhr.addEventListener("abort", () => reject(new Error("Upload cancelado")));

    xhr.open("POST", url);
    xhr.send(formData);
  });
}

// Usando
const input = document.querySelector("#arquivo");
input.addEventListener("change", async () => {
  try {
    const resultado = await uploadComProgresso(input.files[0], "/api/upload");
    console.log("Upload concluído:", resultado);
  } catch (erro) {
    console.error(erro.message);
  }
});

Note que mesmo usando XHR, nós o envolvemos em uma Promise — aproveitando o melhor dos dois mundos.


O futuro: Fetch com progresso

A API de Streams já permite monitorar o progresso de download com Fetch, e o progresso de upload está em desenvolvimento na especificação:

// Progresso de DOWNLOAD com Fetch (já possível hoje)
async function downloadComProgresso(url) {
  const response = await fetch(url);
  const tamanhoTotal = Number(response.headers.get("Content-Length"));
  let recebido = 0;

  const reader = response.body.getReader();
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    chunks.push(value);
    recebido += value.length;

    const porcentagem = Math.round((recebido / tamanhoTotal) * 100);
    console.log(`Download: ${porcentagem}%`);
  }

  // Monta o resultado final
  const totalBytes = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
  const resultado = new Uint8Array(totalBytes);
  let offset = 0;
  for (const chunk of chunks) {
    resultado.set(chunk, offset);
    offset += chunk.length;
  }

  return resultado;
}

Resumo da evolução

| Característica | XHR Puro | jQuery.ajax | Fetch + async/await |

|---|---|---|---|

| Verbosidade | Alta | Média | Baixa |

| Baseado em | Callbacks | Callbacks / Deferred | Promises nativas |

| Tratamento de erro | Manual e verboso | .fail() | try/catch |

| Legibilidade | Baixa | Média | Alta |

| Sem dependência | ✅ | ❌ (jQuery) | ✅ |

| Cancelamento | xhr.abort() | xhr.abort() | AbortController |

| Progresso de upload | ✅ | ✅ | ⚠️ ainda limitado |

| Streams | ❌ | ❌ | ✅ |

| Status em erros HTTP | Manual | Manual | Manual (response.ok) |


Conclusão

Neste artigo você aprendeu:

  • Como funcionava o XMLHttpRequest e por que foi revolucionário
  • Os cinco estados do XHR e como eram verificados
  • Como o jQuery simplificou as requisições com $.ajax()
  • O Callback Hell do AJAX nos anos 2000
  • O jQuery Deferred como proto-Promise
  • A evolução completa de 1999 até hoje
  • A comparação direta entre as três eras
  • Quando o XHR ainda é relevante — progresso de upload

A evolução de XHR para Fetch reflete perfeitamente a evolução do JavaScript como um todo: de uma linguagem de scripts simples para uma plataforma de desenvolvimento completa e moderna.

 

📚 Fontes e Referências

  • MDN Web Docs — XMLHttpRequest: https://developer.mozilla.org/pt-BR/docs/Web/API/XMLHttpRequest
  • MDN Web Docs — XMLHttpRequest.upload: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/upload
  • MDN Web Docs — Streams API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
  • jQuery Docs — $.ajax: https://api.jquery.com/jquery.ajax
  • Jesse James Garrett — Ajax: A New Approach to Web Applications (2005): https://adaptivepath.org/ideas/ajax-new-approach-web-applications
  • JavaScript.info — XMLHttpRequest: https://javascript.info/xmlhttprequest
  • JavaScript: The Good Parts — Douglas Crockford (O'Reilly)
  • You Don't Know JS: Async & Performance — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS
  • A história do jQuery: https://jquery.com/
Comentários

Mais em Javascript

Objetos: estruturando dados do mundo real
Objetos: estruturando dados do mundo real

Arrays s&atilde;o &oacute;timos para listas. Mas como representar uma&nbsp;pe...

Async/Await: escrevendo código assíncrono de forma limpa
Async/Await: escrevendo código assíncrono de forma limpa

As Promises resolveram o Callback Hell. Mas encadear muitos&nbsp;.then() aind...

LocalStorage e SessionStorage
LocalStorage e SessionStorage

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