Richieste HTTP in Node.js

Node.js offre diversi strumenti per eseguire richieste HTTP, sia attraverso i moduli nativi inclusi nel runtime, sia tramite librerie di terze parti che semplificano l'interfaccia e aggiungono funzionalità avanzate. In questo articolo analizzeremo nel dettaglio le principali tecniche disponibili, partendo dal modulo http integrato fino ad arrivare a soluzioni moderne come fetch nativo, axios e got, esaminando casi d'uso reali, gestione degli errori, autenticazione e streaming.

Il modulo http nativo

Il modulo http è incluso in Node.js senza la necessità di installare dipendenze esterne. Fornisce un'API di basso livello che richiede una gestione manuale dei dati ricevuti, ma offre il massimo controllo sul ciclo di vita della richiesta.

// Importazione del modulo nativo http
const http = require('http');

// Opzioni della richiesta GET
const options = {
  hostname: 'jsonplaceholder.typicode.com', // host di destinazione
  port: 80,                                  // porta HTTP standard
  path: '/posts/1',                          // percorso della risorsa
  method: 'GET',                             // metodo HTTP
  headers: {
    'Accept': 'application/json'             // tipo di risposta accettato
  }
};

// Creazione della richiesta
const req = http.request(options, (res) => {
  let data = '';

  // Accumulo dei chunk ricevuti
  res.on('data', (chunk) => {
    data += chunk;
  });

  // Fine della ricezione dei dati
  res.on('end', () => {
    const parsed = JSON.parse(data);
    console.log(parsed);
  });
});

// Gestione degli errori di rete
req.on('error', (err) => {
  console.error('Errore nella richiesta:', err.message);
});

// Chiusura della richiesta
req.end();

Lo stesso approccio si applica al modulo https per le connessioni cifrate, semplicemente sostituendo il require con require('https') e impostando la porta a 443.

Richieste POST con il modulo nativo

Per inviare dati a un server è necessario serializzare il corpo della richiesta e specificare le intestazioni appropriate, in particolare Content-Type e Content-Length.

const https = require('https');

// Dati da inviare al server
const payload = JSON.stringify({
  title: 'Nuovo articolo',   // titolo del post
  body: 'Contenuto del post', // corpo del post
  userId: 1                   // identificatore utente
});

// Opzioni per la richiesta POST
const options = {
  hostname: 'jsonplaceholder.typicode.com',
  port: 443,
  path: '/posts',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',         // formato del corpo
    'Content-Length': Buffer.byteLength(payload) // lunghezza in byte
  }
};

const req = https.request(options, (res) => {
  let data = '';

  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
    // Lettura del codice di stato HTTP
    console.log('Stato:', res.statusCode);
    console.log('Risposta:', JSON.parse(data));
  });
});

req.on('error', (err) => {
  console.error('Errore:', err.message);
});

// Scrittura del corpo nel flusso della richiesta
req.write(payload);
req.end();

L'API fetch nativa in Node.js

A partire da Node.js 18, l'API fetch è disponibile globalmente senza alcuna importazione. Questa API, già ampiamente conosciuta nel contesto del browser, semplifica enormemente la sintassi delle richieste HTTP grazie all'utilizzo delle Promise e del pattern async/await.

// fetch è disponibile globalmente da Node.js 18+
async function fetchPost(id) {
  // Esecuzione della richiesta GET
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

  // Verifica del codice di stato
  if (!response.ok) {
    throw new Error(`Errore HTTP: ${response.status}`);
  }

  // Parsing automatico del corpo JSON
  const data = await response.json();
  return data;
}

fetchPost(1)
  .then((post) => console.log(post))
  .catch((err) => console.error('Errore:', err.message));

Richiesta POST con fetch

async function createPost(postData) {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json' // formato del corpo della richiesta
    },
    body: JSON.stringify(postData)        // serializzazione del corpo
  });

  if (!response.ok) {
    throw new Error(`Creazione fallita: ${response.status}`);
  }

  return response.json();
}

// Dati del nuovo post
const newPost = {
  title: 'Titolo di prova',
  body: 'Corpo del messaggio',
  userId: 42
};

createPost(newPost).then(console.log);

Gestione di header e risposta completa

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

  // Lettura degli header della risposta
  const contentType = response.headers.get('content-type');
  const xPoweredBy = response.headers.get('x-powered-by');

  console.log('Content-Type:', contentType);
  console.log('X-Powered-By:', xPoweredBy);
  console.log('Stato:', response.status);
  console.log('OK:', response.ok);
  console.log('URL finale (dopo redirect):', response.url);

  // Lettura del corpo come testo grezzo
  const text = await response.text();
  return text;
}

inspectResponse('https://jsonplaceholder.typicode.com/posts/1')
  .then((body) => console.log(body));

Gestione del timeout con fetch

L'API fetch nativa non supporta direttamente un'opzione di timeout. Per implementarlo è necessario utilizzare AbortController in combinazione con Promise.race o con il parametro signal.

// Funzione che aggiunge il timeout a una richiesta fetch
async function fetchWithTimeout(url, timeoutMs = 5000) {
  // Controller per l'annullamento della richiesta
  const controller = new AbortController();

  // Timer che annulla la richiesta allo scadere del timeout
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, {
      signal: controller.signal // collegamento al controller
    });

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

    return response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error(`Timeout dopo ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    // Pulizia del timer in ogni caso
    clearTimeout(timeoutId);
  }
}

fetchWithTimeout('https://jsonplaceholder.typicode.com/posts/1', 3000)
  .then(console.log)
  .catch((err) => console.error(err.message));

Utilizzo di Axios

Axios è una delle librerie HTTP più diffuse nell'ecosistema Node.js. Offre un'API intuitiva, la serializzazione e il parsing automatico del JSON, la gestione degli errori strutturata e la possibilità di definire istanze con configurazioni predefinite.

# Installazione di Axios tramite npm
npm install axios

Richiesta GET di base

const axios = require('axios');

async function getUser(userId) {
  try {
    // axios.get restituisce direttamente l'oggetto con data già parsificato
    const { data, status, headers } = await axios.get(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );

    console.log('Stato:', status);
    console.log('Utente:', data);
    return data;
  } catch (err) {
    // Risposta ricevuta con codice di errore (4xx, 5xx)
    if (err.response) {
      console.error('Errore server:', err.response.status, err.response.data);
    // Richiesta inviata ma nessuna risposta ricevuta
    } else if (err.request) {
      console.error('Nessuna risposta ricevuta');
    // Errore nella configurazione della richiesta
    } else {
      console.error('Errore di configurazione:', err.message);
    }
  }
}

getUser(1);

Istanza personalizzata con configurazione base

const axios = require('axios');

// Creazione di un'istanza con URL base e header predefiniti
const apiClient = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com', // URL di base per tutte le richieste
  timeout: 8000,                                    // timeout in millisecondi
  headers: {
    'Accept': 'application/json',
    'X-Custom-Header': 'node-client'
  }
});

// Interceptor sulle richieste: aggiunge un token di autenticazione
apiClient.interceptors.request.use((config) => {
  const token = process.env.API_TOKEN; // token letto dalle variabili d'ambiente
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  return config;
}, (err) => Promise.reject(err));

// Interceptor sulle risposte: log dello stato
apiClient.interceptors.response.use(
  (response) => {
    console.log(`[${response.status}] ${response.config.url}`);
    return response;
  },
  (err) => {
    console.error(`Errore risposta: ${err.message}`);
    return Promise.reject(err);
  }
);

// Utilizzo dell'istanza configurata
async function fetchComments(postId) {
  const { data } = await apiClient.get(`/comments?postId=${postId}`);
  return data;
}

fetchComments(1).then(console.log);

Richieste parallele con Axios

const axios = require('axios');

async function fetchMultipleResources() {
  // Esecuzione parallela di più richieste indipendenti
  const [usersRes, postsRes, todosRes] = await Promise.all([
    axios.get('https://jsonplaceholder.typicode.com/users'),
    axios.get('https://jsonplaceholder.typicode.com/posts'),
    axios.get('https://jsonplaceholder.typicode.com/todos')
  ]);

  return {
    users: usersRes.data,  // lista degli utenti
    posts: postsRes.data,  // lista dei post
    todos: todosRes.data   // lista dei compiti
  };
}

fetchMultipleResources().then((results) => {
  console.log('Utenti:', results.users.length);
  console.log('Post:', results.posts.length);
  console.log('Todo:', results.todos.length);
});

Utilizzo di got

got è un'altra libreria HTTP molto apprezzata, progettata esplicitamente per Node.js. Supporta il retry automatico, lo streaming, la paginazione e offre un'architettura basata su plugin. A partire dalla versione 12 è un modulo ESM-only.

# Installazione di got
npm install got
import got from 'got';

// Richiesta GET con parsing automatico del JSON
async function fetchAlbum(albumId) {
  const data = await got(
    `https://jsonplaceholder.typicode.com/albums/${albumId}`
  ).json();

  console.log(data);
  return data;
}

// Richiesta con opzioni avanzate: retry e timeout
async function fetchWithRetry(url) {
  const data = await got(url, {
    retry: {
      limit: 3,                   // numero massimo di tentativi
      methods: ['GET'],           // metodi su cui applicare il retry
      statusCodes: [500, 503]     // codici di stato che attivano il retry
    },
    timeout: {
      request: 5000               // timeout totale della richiesta in ms
    }
  }).json();

  return data;
}

fetchAlbum(2).catch(console.error);

Download di file e streaming

Quando si scaricano file di grandi dimensioni è preferibile utilizzare lo streaming per evitare di caricare l'intero contenuto in memoria. Il modulo nativo https e librerie come got supportano nativamente questo pattern.

const https = require('https');
const fs = require('fs');
const path = require('path');

// Funzione per scaricare un file tramite streaming
function downloadFile(fileUrl, destPath) {
  return new Promise((resolve, reject) => {
    // Percorso assoluto del file di destinazione
    const absolutePath = path.resolve(destPath);
    const writeStream = fs.createWriteStream(absolutePath);

    https.get(fileUrl, (response) => {
      // Gestione del redirect (codice 3xx)
      if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
        console.log('Redirect verso:', response.headers.location);
        return downloadFile(response.headers.location, destPath)
          .then(resolve)
          .catch(reject);
      }

      if (response.statusCode !== 200) {
        return reject(new Error(`Stato non valido: ${response.statusCode}`));
      }

      // Collegamento del flusso di lettura con quello di scrittura
      response.pipe(writeStream);

      writeStream.on('finish', () => {
        writeStream.close();
        resolve(absolutePath);
      });

      writeStream.on('error', reject);
    }).on('error', reject);
  });
}

downloadFile(
  'https://www.w3.org/TR/PNG/iso_8859-1.txt',
  './downloaded-file.txt'
).then((filePath) => {
  console.log('File salvato in:', filePath);
}).catch(console.error);

Streaming con got

import got from 'got';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

// Download di un file tramite got con pipeline asincrona
async function downloadWithGot(url, dest) {
  // got.stream restituisce un ReadableStream compatibile con pipeline
  await pipeline(
    got.stream(url),
    createWriteStream(dest)
  );

  console.log(`Download completato: ${dest}`);
}

downloadWithGot(
  'https://www.w3.org/TR/PNG/iso_8859-1.txt',
  './output.txt'
).catch(console.error);

Upload di file multipart

L'upload di file verso un server remoto richiede la codifica multipart/form-data. La libreria form-data semplifica la costruzione del corpo della richiesta in questo formato.

# Installazione delle dipendenze necessarie
npm install form-data axios
const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');

async function uploadFile(filePath, uploadUrl) {
  const form = new FormData();

  // Aggiunta del file al form con il nome del campo atteso dal server
  form.append('file', fs.createReadStream(filePath), {
    filename: 'upload.txt',      // nome del file inviato al server
    contentType: 'text/plain'    // tipo MIME del file
  });

  // Aggiunta di un campo testo al form
  form.append('description', 'File caricato da Node.js');

  try {
    const { data } = await axios.post(uploadUrl, form, {
      headers: {
        ...form.getHeaders() // header multipart con boundary generato
      }
    });

    console.log('Upload riuscito:', data);
    return data;
  } catch (err) {
    console.error('Errore durante upload:', err.message);
    throw err;
  }
}

Autenticazione nelle richieste HTTP

Le API moderne richiedono spesso meccanismi di autenticazione. I pattern più diffusi includono l'autenticazione Basic, i token Bearer e l'uso di chiavi API negli header.

const axios = require('axios');

// Autenticazione Basic (username e password in Base64)
async function fetchWithBasicAuth(url, username, password) {
  const { data } = await axios.get(url, {
    auth: {
      username, // nome utente
      password  // password
    }
  });
  return data;
}

// Autenticazione Bearer tramite token JWT
async function fetchWithBearerToken(url, token) {
  const { data } = await axios.get(url, {
    headers: {
      'Authorization': `Bearer ${token}` // token di accesso
    }
  });
  return data;
}

// Autenticazione tramite API key nell'header
async function fetchWithApiKey(url, apiKey) {
  const { data } = await axios.get(url, {
    headers: {
      'X-API-Key': apiKey // chiave API fornita dal servizio
    }
  });
  return data;
}

// Refresh automatico del token in caso di risposta 401
async function fetchWithTokenRefresh(url, getToken, refreshToken) {
  try {
    const token = await getToken();
    const { data } = await axios.get(url, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    return data;
  } catch (err) {
    if (err.response && err.response.status === 401) {
      // Token scaduto: si richiede un nuovo token
      const newToken = await refreshToken();
      const { data } = await axios.get(url, {
        headers: { 'Authorization': `Bearer ${newToken}` }
      });
      return data;
    }
    throw err;
  }
}

Gestione avanzata degli errori

Una gestione robusta degli errori distingue i diversi tipi di fallimento: errori di rete, timeout, errori del server e risposte con stato di errore HTTP.

const axios = require('axios');

// Classe personalizzata per gli errori HTTP
class HttpError extends Error {
  constructor(message, statusCode, responseData) {
    super(message);
    this.name = 'HttpError';
    this.statusCode = statusCode;   // codice di stato HTTP
    this.responseData = responseData; // corpo della risposta di errore
  }
}

// Funzione generica con gestione degli errori strutturata
async function safeRequest(url, options = {}) {
  try {
    const response = await axios({ url, ...options });
    return { success: true, data: response.data };
  } catch (err) {
    if (err.response) {
      // Il server ha risposto con un codice di errore
      throw new HttpError(
        `Richiesta fallita con stato ${err.response.status}`,
        err.response.status,
        err.response.data
      );
    }

    if (err.code === 'ECONNABORTED') {
      // Timeout della richiesta
      throw new Error('La richiesta ha impiegato troppo tempo');
    }

    if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') {
      // Il server non è raggiungibile
      throw new Error(`Impossibile raggiungere il server: ${url}`);
    }

    // Errore generico non classificato
    throw new Error(`Errore imprevisto: ${err.message}`);
  }
}

// Utilizzo con gestione differenziata per tipo di errore
safeRequest('https://jsonplaceholder.typicode.com/posts/1')
  .then(({ data }) => console.log(data))
  .catch((err) => {
    if (err instanceof HttpError) {
      console.error(`Errore HTTP ${err.statusCode}:`, err.responseData);
    } else {
      console.error('Errore di rete:', err.message);
    }
  });

Paginazione automatica

Molte API REST restituiscono i risultati in pagine. La gestione della paginazione può essere automatizzata raccogliendo tutte le pagine fino all'esaurimento dei risultati.

const axios = require('axios');

// Recupero di tutte le pagine di un endpoint paginato
async function fetchAllPages(baseUrl, pageParam = 'page', limitParam = 'limit', pageSize = 10) {
  const allResults = [];
  let currentPage = 1;
  let hasMore = true;

  while (hasMore) {
    const { data } = await axios.get(baseUrl, {
      params: {
        [pageParam]: currentPage,   // numero della pagina corrente
        [limitParam]: pageSize      // numero di elementi per pagina
      }
    });

    // Aggiunta dei risultati della pagina corrente all'array totale
    allResults.push(...data);

    // Si interrompe se la pagina è incompleta o vuota
    if (data.length < pageSize) {
      hasMore = false;
    } else {
      currentPage++;
    }
  }

  return allResults;
}

fetchAllPages('https://jsonplaceholder.typicode.com/posts', 'page', '_limit', 20)
  .then((posts) => console.log(`Totale post recuperati: ${posts.length}`))
  .catch(console.error);

Caching delle risposte

Per ridurre il numero di richieste verso API esterne è possibile implementare una cache in memoria basata sull'URL, con un tempo di scadenza configurabile.

const axios = require('axios');

// Cache in memoria con TTL per ridurre le chiamate ripetute
class RequestCache {
  constructor(ttlMs = 60000) {
    this.store = new Map(); // archivio delle risposte in cache
    this.ttl = ttlMs;       // durata di validità in millisecondi
  }

  // Recupero di una risposta dalla cache
  get(key) {
    const entry = this.store.get(key);
    if (!entry) return null;

    // Verifica se la voce è ancora valida
    if (Date.now() - entry.timestamp > this.ttl) {
      this.store.delete(key);
      return null;
    }

    return entry.data;
  }

  // Salvataggio di una risposta nella cache
  set(key, data) {
    this.store.set(key, { data, timestamp: Date.now() });
  }

  // Invalidazione manuale di una voce
  invalidate(key) {
    this.store.delete(key);
  }
}

const cache = new RequestCache(30000); // TTL di 30 secondi

async function cachedGet(url) {
  // Restituzione dalla cache se disponibile
  const cached = cache.get(url);
  if (cached) {
    console.log('[CACHE HIT]', url);
    return cached;
  }

  // Recupero dal server e salvataggio in cache
  const { data } = await axios.get(url);
  cache.set(url, data);
  console.log('[CACHE MISS]', url);
  return data;
}

// La seconda chiamata con lo stesso URL non esegue una nuova richiesta HTTP
cachedGet('https://jsonplaceholder.typicode.com/posts/1').then(console.log);
cachedGet('https://jsonplaceholder.typicode.com/posts/1').then(console.log);

Conclusioni

Node.js mette a disposizione uno spettro completo di strumenti per eseguire richieste HTTP. Il modulo nativo http/https garantisce il massimo controllo ed è adatto quando non si vuole introdurre dipendenze esterne. L'API fetch nativa, disponibile da Node.js 18, rappresenta la scelta moderna per la maggior parte dei casi d'uso grazie alla sua semplicità e alla coerenza con l'ambiente browser. Axios si distingue per la ricchezza di funzionalità, gli interceptor e la gestione strutturata degli errori, risultando ideale in applicazioni di medie e grandi dimensioni. got, infine, eccelle negli scenari che richiedono retry automatico, streaming avanzato e architetture modulari basate su plugin. La scelta tra questi strumenti dipende dai requisiti del progetto, dalla versione di Node.js in uso e dalla necessità di mantenere il numero di dipendenze sotto controllo.