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.ok antes de processar o corpo da resposta.
  • Use try/catch ou .catch() para capturar erros de rede e erros de processamento.
  • Prefira async/await para código mais legível e fácil de debugar.
  • Configure headers apropriados, especialmente Content-Type ao enviar dados.
  • Considere usar AbortController para 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. Use response.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

Exercícios

  1. Faça uma requisição GET para a API pública https://jsonplaceholder.typicode.com/posts/1 e exiba o título do post no console.
  2. ✓ Resposta:
    fetch('https://jsonplaceholder.typicode.com/posts/1')
      .then(response => response.json())
      .then(post => console.log(post.title))
      .catch(error => console.error('Erro:', error));
    
  3. Envie uma requisição POST para https://jsonplaceholder.typicode.com/posts com um objeto contendo title e body. Exiba a resposta no console.
  4. ✓ Resposta:
    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));
    
  5. Faça uma requisição GET para https://api.github.com/users/octocat e verifique se a resposta é ok. Se for, exiba o nome do usuário; caso contrário, exiba uma mensagem de erro com o status.
  6. ✓ Resposta:
    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));
    
  7. Simule um erro de rede usando uma URL inválida (ex.: https://exemplo.invalido) e capture o erro exibindo uma mensagem amigável.
  8. ✓ Resposta:
    fetch('https://exemplo.invalido')
      .catch(error => console.log('Falha de rede:', error.message));
    
  9. Usando AbortController, faça uma requisição para https://jsonplaceholder.typicode.com/todos/1 com timeout de 1 segundo. Se demorar, aborte e exiba "Tempo esgotado".
  10. ✓ Resposta:
    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));