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
Responsee 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
AbortControllerpara 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 AJAX — Asynchronous 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
XMLHttpRequeste 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/