Efetuando requisições com o fetch do Node.js 18

Exemplos de como efetuar requisições http GET e POST utilizando o fetch, disponível em modo experimental no Node.js 18.

Efetuando requisições com o fetch do Node.js 18
Efetuando requisições com o fetch do Node.js 18

No dia 25/10/2022 a versão 18 do Node.js foi designada como LTS (long-term support) com o codinome Hydrogen:

This release marks the transition of Node.js 18.x into Long Term Support (LTS) with the codename 'Hydrogen'. The 18.x release line now moves into "Active LTS" and will remain so until October 2023. After that time, it will move into "Maintenance" until end of life in April 2025.

https://nodejs.org/uk/blog/release/v18.12.0/

Alguns dias antes, 18/10/2022, a versão 19 foi anunciada, sendo a nova versão "Current".

Node.js 19 will replace Node.js 18 as our ‘Current’ release line when Node.js 18 enters long-term support (LTS) later this month.
As per the release schedule, Node.js 19 will be ‘Current' release for the next 6 months, until April 2023.

https://github.com/nodejs/node/releases/tag/v19.0.0

Uma das novas características da versão 18 é a possibilidade de utilizar a API fetch exatamente como utilizamos no navegador. Neste contexto vamos explorar a seguir alguns exemplos de como utilizá-la.

A API Fetch fornece uma interface JavaScript para acessar e manipular partes do pipeline HTTP, tais como os pedidos e respostas. Ela também fornece o método global fetch() (en-US) que fornece uma maneira fácil e lógica para buscar recursos de forma assíncrona através da rede.

https://developer.mozilla.org/pt-BR/docs/Web/API/Fetch_API/Using_Fetch

Atualizando a versão do Node

Como estamos falando sobre uma atualização de versão (do Node.js), acho que vale citar o pacote n. Disponível no npm, este pacote ajuda a fazer upgrade/downgrade de versões do Node.js.

n is supported on macOS, Linux, including with Windows Subsystem for Linux, and various other unix-like systems. It is written as a BASH script but does not require you to use BASH as your command shell.

Com um único comando é possível instalar a versão 18:

$ sudo n install 18

Resultado:

installing : node-v18.12.0
       mkdir : /usr/local/n/versions/node/18.12.0
       fetch : https://nodejs.org/dist/v18.12.0/node-v18.12.0-darwin-arm64.tar.xz
     copying : node/18.12.0
   installed : v18.12.0 (with npm 8.19.2)

Verificando a versão instalada:

$ node --version
v18.12.0

Caso seu setup não atenda aos requisitos para uso do n, utilize a opção do download no site oficial.

fetch - efetuando requisições

Se você tem um pouco mais de tempo na área, provavelmente vai lembrar do ActiveXObject("Microsoft.XMLHTTP") no antigo Internet Explorer ou do XmlHttpRequest. Felizmente o fetch chegou para ajudar. Em 2014 na versão 40 do Chrome já tínhamos um suporte parcial ao fetch. Atualmente todos os navegadores dão suporte e desde a versão 17.5.0 do Node temos esta função em caracter experimental.

Preparando o ambiente de estudo

Para preparar o ambiente de testes utilizei os seguintes comandos:

# Cria um novo diretório
$ mkdir consolelog-node18

# Entre no novo diretório
$ cd consolelog-node18

# Configura um novo pacote npm (vai criar o package.json)
$ npm init -y

# Cria um arquivo chamado "index.js"
$ touch index.js

Após a execução destes comandos editei o arquivo package.json que foi gerado:

{
  "name": "consolelog-node18",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "type": "module",
  "author": "Marcelo R. Vismari",
  "license": "ISC"
}
package.json

Efetuando um GET

É preciso poucas linhas de código para efetuar uma requisição GET utilizando o fetch:

const url = "https://run.mocky.io/v3/be9fc3e9-69d6-4624-bc73-9cb3baac2b40";
const response = await fetch(url);
const responseJson = await response.json();
console.log(responseJson);
index.js

Executando o comando npm start:

$ npm start

> [email protected] start
> node index.js

(node:1164) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
{ site: 'consolelog.com.br', mensagem: 'Seja bem vindo!' }

Analisando o código e seu resultado podemos destacar dois pontos:

  1. O fetch é um objeto global, então não é necessário efetuar um import
  2. Após executar o npm start apareceu uma mensagem ((node:1164) ExperimentalWarning...). Esta mensagem é exibida porque o fetch ainda está em modo experimental. Para suprimir a mensagem adicione o --no-warnings, como no arquivo package.json abaixo:
{
  "name": "consolelog-node18",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node --no-warnings index.js"
  },
  "keywords": [],
  "type": "module",
  "author": "Marcelo R. Vismari",
  "license": "ISC"
}
package.json

Executando o npm start:

$ npm start

> [email protected] start
> node --no-warnings index.js

{ site: 'consolelog.com.br', mensagem: 'Seja bem vindo!' }

Efetuando um POST - application/json

Antes de testarmos uma requisição do tipo POST, vamos construir de uma forma bem simples uma API que receberá esta requisição. Então crie um arquivo chamado api.js e cole o conteúdo abaixo:

import http from "node:http";

const port = 3000;
const server = http.createServer(async (request, response) => {
  let body = "";

  for await (const chunk of request) {
    body += chunk;
  }

  // Como sabemos que será postado um application/json,
  // efetuamos o parse com o JSON.parse
  const objBody = JSON.parse(body);

  response
    .writeHead(200, null, { "Content-type": "application/json" })
    .end(JSON.stringify({ objRecebido: objBody }));
});

server.listen(port, () => console.log(`Executando na porta ${port}`));
api.js

O código acima está preparado para receber requisições na porta 3000, pegar o conteúdo postado no body, efetuar o parse e devolver na resposta o conteúdo recebido no body dentro de um objeto com a propriedade objRecebido.

Para executar o código acima basta digitar node api.js no terminal.

Agora vamos alterar o arquivo index.js para efetuar um POST em localhost:3000:

const url = "http://localhost:3000";

const response = await fetch(url, {
  method: "post",
  body: JSON.stringify({ campo1: "teste 1" }),
  headers: { "Content-Type": "application/json" },
});

const responseJson = await response.json();
console.log(responseJson);
index.js

No resultado abaixo é possível observar que o POST foi feito corretamente e a pequena API recebeu e devolveu o resultado esperado:

$ npm start

> [email protected] start
> node --no-warnings index.js

{ objRecebido: { campo1: 'teste 1' } }

Observação: antes de executar o npm start não se esqueça de subir a API de testes: node api.js.

Efetuando o POST - multipart/form-data

No exemplo anterior efetuamos um POST de um application/json, ou seja, no body da requisição foi enviado um conteúdo na estrutura JSON. Agora vamos modificar o código para postar os dados de um multipart/form-data, porém antes, caso a ideia do multipart/form-data não esteja tão clara, sugiro a leitura do trecho abaixo com uma breve explicação que deixei no stackoverflow. Caso esteja confortável pule este trecho.


O multipart/form-data representa um tipo de conteúdo que será submetido ao servidor da mesma forma que um application/json. A diferença é a origem da requisição. Se a mesma tiver sua origem em um formulário HTML, podemos trabalhar apenas com os seguintes tipos:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

Por outro lado, se a origem for uma aplicação ou uma requisição AJAX, podemos trabalhar com application/json e adicionar o JSON no body.

Por exemplo, um formulário com o enctype=application/x-www-form-urlencoded tem seus campos representados na seguinte estrutura: campo1=valor1&campo2=valor2&.... Já no caso do multipart/form-data os dados são representados através de várias partes, onde cada parte pode ter seu próprio MIME type:

--<<boundary_value>>
Content-Disposition: form-data; name="campo1"

valor1
--<<boundary_value>>
Content-Disposition: form-data; name="campo2"
Content-Type: text/html

valor2
--<<boundary_value>>--

Basicamente cada parte é delimitada pelo boundary e possuí na primeira linha a estrutura Content-Disposition: form-data; name="NOME DO MEU CAMPO". Na sequência, separando as informações por CR LR, opcionalmente pode-se declarar o Content-type, que por padrão é text/plain. Por fim, vêm o valor do campo.

Para mais detalhes: https://www.w3.org/TR/html401/interact/forms.html#idx-character_encoding


Para efetuar um POST no formato multipart/form-data podemos utilizar o FormData, que também está disponível de forma global:

const url = "http://localhost:4000";

const formData = new FormData();
formData.append("campo1", "valor1");
formData.append("campo2", "valor2");

const response = await fetch(url, {
  method: "post",
  body: formData,
  headers: { Authorization: "123", ...formData.headers },
});

const responseJson = await response.json();
console.log(responseJson);
index.js

Observação: o ...formData.headers incluí o header Content-type: multipart/form-data.

No código da API, api.js,  que irá receber o POST, modifiquei a porta e retirei o JSON.parse já que agora os dados estão no formato multipart/form-data:

import http from "node:http";
const port = 4000;
const server = http.createServer(async (request, response) => {
  let body = "";
  for await (const chunk of request) {
    body += chunk;
  }

  response
    .writeHead(200, null, { "Content-type": "application/json" })
    .end(JSON.stringify({ objRecebido: { body, headers: request.headers } }));
});
server.listen(port, () => console.log(`Executando na porta ${port}`));
api.js

No resultado abaixo é possível observar que o conteúdo postado foi apenas devolvido pela API. No exemplo anterior nós efetuamos o parse do conteúdo (JSON) através do JSON.parse, mas neste exemplo pulamos o parse.

Caso queria efetuar o parse do multipart/form-data, sugiro o uso da lib busboy. Se estiver trabalhando com o express, utilize o multer que trabalha internamente com o busboy.

$ npm start

> [email protected] start
> node --no-warnings index.js

{
  objRecebido: {
    body: '------formdata-undici-0.1737084714722541\r\n' +
      'Content-Disposition: form-data; name="campo1"\r\n' +
      '\r\n' +
      'valor1\r\n' +
      '------formdata-undici-0.1737084714722541\r\n' +
      'Content-Disposition: form-data; name="campo2"\r\n' +
      '\r\n' +
      'valor2\r\n' +
      '------formdata-undici-0.1737084714722541--',
    headers: {
      host: 'localhost:4000',
      connection: 'keep-alive',
      authorization: '123',
      'content-type': 'multipart/form-data; boundary=----formdata-undici-0.1737084714722541',
      accept: '*/*',
      'accept-language': '*',
      'sec-fetch-mode': 'cors',
      'user-agent': 'undici',
      'accept-encoding': 'gzip, deflate',
      'transfer-encoding': 'chunked'
    }
  }
}

Considerações

Apesar de ainda ser experimental, o fetch oferece uma nova possibilidade de efetuarmos requisições http sem o uso de libs, como o axios ou o descontinuado request. Esta possibilidade não significa necessariamente a substituição de uma lib que já faz este trabalho, é apenas uma nova alternativa a ser considerada.