A Fetch API
Nesta aula, você aprenderá a usar a Fetch API do JavaScript para fazer requisições HTTP assíncronas. Vamos explorar como fazer GET e POST, configurar headers, tratar respostas de forma eficiente e lidar com erros de rede, com exemplos práticos e boas práticas.
A Fetch API é uma interface moderna do JavaScript para realizar requisições HTTP de forma assíncrona. Ela substitui o antigo XMLHttpRequest (XHR) e é baseada em Promises, o que torna o código mais limpo e legível. Com a Fetch API, você pode buscar recursos da web, enviar dados para servidores e integrar aplicações com APIs externas. Nesta aula, vamos explorar os conceitos essenciais: como fazer requisições GET e POST, configurar headers, interpretar respostas e lidar com erros de rede.
Antes de mergulharmos nos detalhes, é importante entender que a Fetch API é amplamente suportada em navegadores modernos e também está disponível no Node.js desde a versão 18. Ela fornece um método global fetch() que retorna uma Promise que resolve para um objeto Response. Esse objeto contém os dados da resposta e métodos para processá-los. Vamos começar com o básico: requisições GET e POST.
GET e POST
O método fetch() aceita como primeiro argumento a URL que deseja acessar. Por padrão, se nenhum segundo argumento for fornecido, a requisição é do tipo GET. Para fazer uma requisição GET, você pode simplesmente chamar fetch(url). O retorno é uma Promise que resolve para um objeto Response. Para acessar o corpo da resposta, você precisa usar métodos como json(), text() ou blob(), dependendo do formato esperado.
Vejamos um exemplo simples de requisição GET para obter dados de uma API pública:
// Exemplo de GET usando fetch
fetch('https://api.exemplo.com/usuarios')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Erro:', error));
Para uma requisição POST, você precisa fornecer um objeto de configuração como segundo argumento. Esse objeto deve conter o método (method: 'POST') e, geralmente, um corpo (body) com os dados a serem enviados. O corpo pode ser uma string (como JSON) ou um objeto FormData para envio de formulários. É importante definir o header Content-Type para indicar o formato dos dados, caso esteja enviando JSON.
Exemplo de POST com dados JSON:
// Exemplo de POST usando fetch
const dados = {
nome: 'Maria',
email: 'maria@exemplo.com'
};
fetch('https://api.exemplo.com/usuarios', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(dados)
})
.then(response => response.json())
.then(data => console.log('Sucesso:', data))
.catch(error => console.error('Erro:', error));
Observe que usamos JSON.stringify() para converter o objeto em uma string JSON antes de enviar. Isso é necessário porque o corpo da requisição precisa ser uma string (ou um tipo de dados aceito pelo protocolo HTTP). A resposta, por sua vez, pode ser convertida de volta para um objeto JavaScript com response.json().
Headers
Os headers de uma requisição HTTP são metadados que definem informações sobre a requisição ou a resposta, como tipo de conteúdo, autenticação, cache, etc. Na Fetch API, você pode configurar headers no objeto de opções usando a propriedade headers. Ela pode ser um objeto simples ou uma instância da classe Headers.
É comum usar headers para especificar o formato dos dados enviados (Content-Type), enviar tokens de autenticação (Authorization) ou definir preferências de idioma (Accept-Language). Vamos ver como configurar headers em uma requisição:
// Configurando headers com um objeto simples
fetch('https://api.exemplo.com/dados', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
}
});
// Usando a classe Headers
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');
fetch('https://api.exemplo.com/dados', {
headers: headers
});
A classe Headers oferece métodos como append(), set(), get() e has() para manipular headers de forma mais flexível. Por exemplo, você pode verificar se um header existe antes de configurá-lo. Também é possível ler os headers da resposta, como veremos na próxima seção.
Além de configurar headers de requisição, você pode acessar os headers da resposta por meio do objeto Response. O método response.headers.get('nome') retorna o valor de um header específico, e response.headers.entries() retorna um iterador com todos os pares chave-valor. Isso é útil para verificar informações como tipo de conteúdo ou data de modificação.
Tratando respostas
O objeto Response retornado pela Promise do fetch() contém várias propriedades e métodos que nos permitem acessar os dados da resposta. As propriedades mais comuns são status (código HTTP), statusText (mensagem de status), ok (booleano que indica se o status está entre 200-299), url (URL final da requisição) e headers (objeto Headers da resposta).
Para obter o corpo da resposta, você deve usar um dos métodos do objeto Response: json(), text(), blob(), formData() ou arrayBuffer(). Cada um desses métodos retorna uma Promise que resolve para o corpo no formato correspondente. É crucial escolher o método certo, pois se você tentar usar json() em uma resposta que não é JSON, ocorrerá um erro.
Vamos ver como tratar uma resposta de forma robusta, verificando o status HTTP antes de processar os dados:
fetch('https://api.exemplo.com/usuarios')
.then(response => {
if (!response.ok) {
throw new Error('Erro HTTP: ' + response.status);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Erro:', error.message));
No exemplo acima, verificamos se response.ok é true. Se não for, lançamos um erro que será capturado no catch. Isso evita que tentemos processar uma resposta de erro como se fosse bem-sucedida. Uma prática comum é criar uma função auxiliar que verifica a resposta e lança um erro com uma mensagem mais detalhada, incluindo o corpo da resposta quando possível.
Outra técnica importante é lidar com respostas de erro que contêm JSON com mensagens de erro. Nesse caso, você pode tentar ler o corpo como JSON e extrair a mensagem, mas como o corpo pode ser consumido apenas uma vez, você precisa clonar a resposta ou usar o método text() e depois fazer o parse manualmente. Vamos ver um exemplo:
fetch('https://api.exemplo.com/usuarios')
.then(async response => {
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Erro desconhecido');
}
return data;
})
.then(data => console.log(data))
.catch(error => console.error('Erro:', error.message));
Nesse exemplo, primeiro lemos o corpo como JSON, depois verificamos o status. Se não for ok, lançamos um erro com a mensagem vinda do servidor. Isso fornece um tratamento mais rico de erros.
Erros de rede
Erros de rede ocorrem quando a requisição não pode ser concluída, por exemplo, se o servidor está fora do ar, se há problemas de conectividade ou se a URL não é válida. A Fetch API rejeita a Promise apenas em caso de falha de rede, ou seja, quando a requisição não chega a receber uma resposta HTTP. Erros HTTP (como 404 ou 500) não rejeitam a Promise; eles resolvem com response.ok sendo false. Portanto, é essencial tratar esses dois tipos de erro separadamente.
Para capturar erros de rede, usamos o método catch() da Promise. Exemplo:
fetch('https://api.exemplo.com/invalid-url')
.then(response => response.json())
.catch(error => console.error('Falha de rede:', error));
No entanto, é importante notar que erros de rede também podem ocorrer durante o processamento da resposta (por exemplo, se o corpo não puder ser lido). Para capturar todos os erros, é recomendável usar try/catch com async/await, que é uma sintaxe mais moderna e legível.
Vamos ver um exemplo completo usando async/await com tratamento de erros de rede e erros HTTP:
async function buscarUsuarios() {
try {
const response = await fetch('https://api.exemplo.com/usuarios');
if (!response.ok) {
throw new Error('Erro HTTP: ' + response.status);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Erro:', error.message);
}
}
buscarUsuarios();
Nesse código, qualquer erro, seja de rede ou HTTP, será capturado no bloco catch. A mensagem de erro será exibida no console. É importante notar que o catch captura tanto erros de rede quanto erros lançados manualmente (como o erro de HTTP). Isso simplifica o tratamento.
Uma boa prática é incluir um timeout para evitar que a requisição fique pendente indefinidamente. A Fetch API não tem um timeout nativo, mas você pode usar AbortController para cancelar a requisição após um certo tempo. Vamos ver um exemplo:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch('https://api.exemplo.com/usuarios', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Erro:', error.name === 'AbortError' ? 'Tempo esgotado' : error.message))
.finally(() => clearTimeout(timeout));
Nesse exemplo, se a requisição demorar mais de 5 segundos, ela é abortada e o erro AbortError é lançado. No catch, verificamos se o erro é do tipo AbortError para exibir uma mensagem específica.
Boas práticas e observações finais
- Sempre verifique
response.okantes de processar o corpo da resposta. - Use
try/catchou.catch()para capturar erros de rede e erros de processamento. - Prefira
async/awaitpara código mais legível e fácil de debugar. - Configure headers apropriados, especialmente
Content-Typeao enviar dados. - Considere usar
AbortControllerpara cancelar requisições desnecessárias (ex.: ao desmontar um componente em React). - Evite chamadas múltiplas de
response.json()na mesma resposta, pois o corpo só pode ser lido uma vez. Useresponse.clone()se precisar. - Para APIs que retornam erros com corpo JSON, tente ler o corpo antes de lançar o erro para obter a mensagem.
Referências
- MDN: Fetch API
- MDN: Usando a Fetch API
- MDN: Headers
- MDN: Response
- MDN: AbortController
- javascript.info: Fetch
- Node.js: fetch global
Exercícios
- Faça uma requisição GET para a API pública
https://jsonplaceholder.typicode.com/posts/1e exiba o título do post no console. - Envie uma requisição POST para
https://jsonplaceholder.typicode.com/postscom um objeto contendotitleebody. Exiba a resposta no console. - Faça uma requisição GET para
https://api.github.com/users/octocate verifique se a resposta é ok. Se for, exiba o nome do usuário; caso contrário, exiba uma mensagem de erro com o status. - Simule um erro de rede usando uma URL inválida (ex.:
https://exemplo.invalido) e capture o erro exibindo uma mensagem amigável. - Usando
AbortController, faça uma requisição parahttps://jsonplaceholder.typicode.com/todos/1com timeout de 1 segundo. Se demorar, aborte e exiba "Tempo esgotado".
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(post => console.log(post.title))
.catch(error => console.error('Erro:', error));
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meu post',
body: 'Conteúdo do post'
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Erro:', error));
fetch('https://api.github.com/users/octocat')
.then(response => {
if (!response.ok) {
throw new Error('Erro HTTP: ' + response.status);
}
return response.json();
})
.then(data => console.log('Nome:', data.name))
.catch(error => console.error('Erro:', error.message));
fetch('https://exemplo.invalido')
.catch(error => console.log('Falha de rede:', error.message));
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1000);
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Tempo esgotado');
} else {
console.error('Erro:', error);
}
})
.finally(() => clearTimeout(timeout));