Communiquer avec un point d'extrémité HTTP nécessitait autrefois d'utiliser un paquet tiers. Ce n'est plus vrai côté serveur. À partir de Node.js 18, la fonction globale fetch est incluse dans le runtime, vous pouvez donc faire des requêtes HTTP en Node.js avec l'API Fetch en utilisant exactement la même interface basée sur les promesses que vous connaissez déjà depuis le navigateur, sans installation de node-fetch et sans flag à activer.

Ce guide est un tour pratique de fetch côté serveur : GET et POST, chaînes de requête, envoi et lecture de JSON, la règle de gestion des erreurs qui piège presque tout le monde, les timeouts avec AbortController, l'exécution des requêtes en concurrence, et où Axios garde encore sa place. Il se termine par le seul scénario où une requête simple ne suffit pas, le scraping d'un vrai site web, et comment récupérer du HTML rendu via une API à la place.

Pourquoi fetch, et ce qui a changé dans Node

L'API Fetch est basée sur les promesses, elle se compose donc proprement avec async/await et évite l'imbrication de callbacks de l'ancien module http. Elle supporte chaque méthode dont vous avez besoin (GET, POST, PUT, PATCH, DELETE), vous donne un vrai objet Headers et gère le JSON avec un seul appel de méthode. L'interface est identique entre navigateur et serveur, ce qui signifie que le code et les modèles mentaux se transfèrent dans les deux sens.

L'essentiel pratique est la coupure de version. Sur Node.js 18 et versions ultérieures, fetch est un global, exactement comme setTimeout. Vous ne l'importez pas et n'installez rien. Confirmez votre runtime avant d'écrire du code :

bash
node -v
# v18.x or higher means global fetch is available
Avez-vous encore besoin de node-fetch ?

Seulement sur Node 16 ou plus ancien, où vous installeriez node-fetch et l'importeriez. Sur Node 18+, ce paquet est redondant, et l'ajouter ne fait que masquer l'implémentation intégrée plus rapide. Si vous êtes sur une version LTS actuelle, supprimez la dépendance et utilisez le global.

Faire des requêtes GET avec l'API Fetch

Une requête GET est un seul appel. fetch() renvoie une promesse qui se résout en objet Response dès l'arrivée des en-têtes. Le corps est lu séparément, avec response.json() pour le JSON ou response.text() pour le texte simple, et chacun renvoie sa propre promesse.

Le détail qui compte le plus : vérifiez toujours response.ok avant de lire le corps. Un 404 ou 500 est encore un échange HTTP réussi aux yeux de fetch, donc la promesse se résout normalement. response.ok vaut true uniquement pour les codes de statut dans la plage 200 à 299.

javascript
async function getPost(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

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

  const data = await response.json();
  return data;
}

getPost(1).then(console.log).catch(console.error);

Si le point d'extrémité renvoie du texte ou du HTML plutôt que du JSON, changez le lecteur de corps. Le reste du flux est inchangé.

javascript
const response = await fetch("https://example.com/");
const html = await response.text();
console.log(html.slice(0, 200));

Vous pouvez lire le corps exactement une fois. Appeler response.json() après response.text() sur la même réponse lève une exception, car le flux est déjà consommé. Choisissez un seul lecteur par réponse.

Lire et définir les en-têtes de réponse

L'objet Response expose une propriété headers qui se comporte comme une Map. Lisez un en-tête unique avec get(), ou itérez sur l'ensemble. Les noms d'en-têtes sont insensibles à la casse, donc "content-type" et "Content-Type" résolvent vers la même valeur.

javascript
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");

console.log(response.headers.get("content-type"));

for (const [key, value] of response.headers) {
  console.log(`${key}: ${value}`);
}

Un avantage de s'exécuter côté serveur : les restrictions CORS qui cachent la plupart des en-têtes dans le navigateur ne s'appliquent pas dans Node. Vous avez un accès complet à chaque en-tête de réponse que le serveur envoie.

Ajouter des chaînes de requête avec URLSearchParams

Vous pourriez concaténer les paramètres de requête à la main, mais cela signifie échapper vous-même les espaces, les esperluettes et autres caractères réservés, ce qui est facile à rater. URLSearchParams encode les valeurs correctement et se lit clairement.

javascript
const params = new URLSearchParams({
  userId: 1,
  _limit: 5,
});

const url = `https://jsonplaceholder.typicode.com/posts?${params}`;
const response = await fetch(url);
const posts = await response.json();

console.log(`Got ${posts.length} posts`);

Interpoler params dans un template literal appelle son toString(), qui produit une chaîne de requête correctement encodée. Ajouter ou supprimer un filtre ultérieurement est une modification d'une ligne dans l'objet, pas un exercice de chirurgie sur chaîne.

Envoyer des requêtes POST et des corps JSON

Pour envoyer des données, passez un deuxième argument : un objet d'options avec method, headers et body. Le corps doit être une chaîne, donc les charges JSON passent par JSON.stringify(), et vous définissez Content-Type: application/json pour que le serveur sache comment analyser ce qu'il reçoit.

javascript
async function createPost(payload) {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });

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

  return response.json();
}

const created = await createPost({
  title: "Fetch on the server",
  body: "Built into Node 18+",
  userId: 1,
});

console.log(created);

La même forme couvre PUT et PATCH pour les mises à jour et DELETE pour les suppressions : changez method et, si besoin, le body. Pour les soumissions de formulaires plutôt que JSON, passez une instance URLSearchParams ou FormData comme corps et omettez le Content-Type explicite ; fetch définit le bon en-tête pour ceux-là automatiquement.

Gestion des erreurs : la règle qui piège tout le monde

Voici la chose la plus importante à intérioriser sur fetch. Il ne rejette qu'en cas d'échecs au niveau réseau : la résolution DNS a échoué, la connexion s'est coupée, la requête a été abandonnée. Toute réponse HTTP du serveur, y compris 404 et 500, compte comme une promesse résolue. Un simple try/catch autour de fetch va joyeusement avaler un 500 et continuer comme si tout allait bien.

Le schéma robuste combine les deux vérifications : un try/catch pour la couche réseau, et un test explicite de response.ok pour la couche HTTP.

javascript
async function getJson(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      // HTTP error: server replied, but with a 4xx or 5xx
      throw new Error(`HTTP ${response.status} for ${url}`);
    }

    return await response.json();
  } catch (error) {
    // Network failure, abort, or the thrown HTTP error above
    console.error("Request failed:", error.message);
    throw error;
  }
}

Lever une exception sur !response.ok route les erreurs HTTP dans le même catch que les erreurs réseau, donc un seul bloc gère les deux classes d'échec. Sans cette vérification explicite, un 500 passe directement à response.json(), qui lève alors une erreur d'analyse confuse sur la page d'erreur HTML plutôt que le vrai statut.

Timeouts et annulation avec AbortController

fetch n'a pas de timeout intégré. Laissée seule, une requête peut se bloquer tant que la connexion reste ouverte, ce qui est inacceptable pour tout ce qui est visible par l'utilisateur. La solution standard est AbortController : créez-en un, passez son signal à fetch et appelez abort() quand un timer se déclenche.

javascript
async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      throw new Error(`Request to ${url} timed out after ${ms}ms`);
    }
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

Une requête abandonnée rejette avec un AbortError, c'est pourquoi le catch inspecte error.name pour donner un message de timeout clair. Effacer le timer dans finally empêche une réponse précoce réussie de déclencher un abandon parasite ultérieurement. Le même signal peut relier fetch à un bouton d'annulation piloté par l'utilisateur, pas seulement à un timer. Sur les versions récentes de Node, AbortSignal.timeout(ms) est un raccourci pour ce schéma exact de timer.

Exécuter des requêtes en concurrence avec Promise.all

Quand vous avez plusieurs requêtes indépendantes, les attendre l'une après l'autre fait perdre du temps : chacune attend la fin de la précédente. Promise.all les lance toutes en même temps et se résout quand la dernière se termine, donc le temps total est approximativement celui de la requête la plus lente plutôt que la somme de toutes.

javascript
const ids = [1, 2, 3, 4, 5];

const posts = await Promise.all(
  ids.map((id) =>
    getPost(id).catch((error) => ({ id, error: error.message })),
  ),
);

console.log(posts);

Un avertissement : Promise.all rejette dès qu'une seule promesse rejette, abandonnant les autres. Le .catch() en ligne ci-dessus transforme un échec par requête en valeur, pour qu'une mauvaise requête ne coule pas tout le lot. Si vous préférez inspecter chaque résultat explicitement, Promise.allSettled renvoie un enregistrement statut-et-valeur pour chacune. Et ne lancez pas des milliers de requêtes en même temps : c'est comme ça qu'on se fait limiter en débit ou qu'on épuise les sockets. Pour les grands travaux, limitez la concurrence en traitant la liste par morceaux de taille fixe.

API Fetch vs Axios : lequel choisir

Axios a précédé fetch côté serveur et a encore une base d'utilisateurs fidèles. La comparaison honnête est courte :

  • Optez pour fetch quand vous voulez zéro dépendance et une API standard. C'est intégré, c'est le même dans le navigateur, et pour la plupart des travaux GET et POST c'est tout ce dont vous avez besoin.
  • Optez pour Axios quand vous voulez les commodités qu'il fournit : analyse JSON automatique dans les deux sens, intercepteurs de requête et de réponse, configuration de timeout intégrée, rejet automatique sur les statuts non-2xx, et progression des uploads ou downloads. Sur une grande base de code, ces fonctionnalités économisent du vrai code répétitif.

L'écart est plus petit qu'avant. La plupart des avantages d'Axios, timeouts, vérifications de statut, gestion JSON, représentent quelques lignes de code helper par-dessus fetch, comme les wrappers dans cet article. Si vous démarrez de zéro sur Node 18+ et n'avez pas besoin d'intercepteurs, l'intégré est le choix le plus léger. Si un projet se standardise déjà sur Axios, il n'y a pas d'urgence à le remplacer.

Où fetch simple se heurte à un mur : le scraping de vrais sites

Tout ce qui précède fonctionne parfaitement contre des APIs et des serveurs coopératifs. Pointez fetch sur un site commercial moderne pour récupérer son HTML et vous vous heurtez rapidement à deux limites dures.

Premièrement, fetch ne récupère que le HTML initial que le serveur envoie. Il ne fait pas tourner un navigateur, donc il n'exécute jamais le JavaScript de la page. Sur un site qui rend son contenu côté client, le balisage récupéré est une coque quasi vide, avec les données voulues remplies ultérieurement par des scripts qui ne s'exécutent jamais. Deuxièmement, les sites qui se préoccupent du scraping prennent l'empreinte du trafic entrant. Une requête depuis une IP de datacenter avec un user agent Node par défaut et aucun en-tête ressemblant à un navigateur est mise au défi ou bloquée avant de renvoyer quoi que ce soit d'utile.

Vous pouvez attaquer les deux vous-même, avec un navigateur sans interface graphique pour rendre la page et un pool de proxies résidentiels rotatifs pour ressembler à de vrais visiteurs, mais maintenir ce stack en bon état représente l'essentiel du travail. La voie plus propre est d'envoyer votre appel fetch à une API qui fait le rendu et la rotation d'IP côté serveur et vous remet le HTML finalisé.

Crawlbase Crawling API

Scraper un vrai site nécessite une page rendue derrière une IP de confiance, en une seule requête. L'API Crawling prend votre token et une URL cible, exécute la page dans un vrai navigateur, effectue une rotation des IPs résidentielles pour vous et renvoie le HTML finalisé, pour que vous continuiez d'utiliser simplement fetch et évitez de gérer une flotte sans interface graphique et un pool de proxies. Commencez avec le niveau gratuit.

L'appel est le même fetch que vous connaissez déjà. Vous construisez l'URL du point d'extrémité avec votre token et la cible encodée en URL, envoyez un GET et lisez le HTML rendu depuis la réponse. À partir de là, un parseur comme cheerio transforme ce HTML en données structurées.

javascript
import * as cheerio from "cheerio";

const TOKEN = "YOUR_CRAWLBASE_TOKEN";

async function scrape(targetUrl) {
  const params = new URLSearchParams({
    token: TOKEN,
    url: targetUrl,
  });

  const response = await fetch(`https://api.crawlbase.com/?${params}`);
  if (!response.ok) {
    throw new Error(`Crawl failed: HTTP ${response.status}`);
  }

  const html = await response.text();
  const $ = cheerio.load(html);

  return {
    title: $("title").text().trim(),
    headings: $("h2").map((_, el) => $(el).text().trim()).get(),
  };
}

scrape("https://www.example.com/").then(console.log);

Pour les pages qui rendent le contenu avec JavaScript, ajoutez l'équivalent de "&javascript=true" en passant un token de rendu JS, qui exécute la cible dans un vrai navigateur avant de renvoyer le HTML. Comme la requête n'est que fetch, toutes les techniques de cet article s'appliquent encore : encapsulez-la dans le helper de timeout, exécutez une liste d'URLs via Promise.all avec un plafond de concurrence raisonnable, et vérifiez response.ok à chaque appel. Pour un tutoriel de scraping Node plus complet, consultez comment construire un scraper web avec Node.js, et pour le problème du rendu spécifiquement, comment crawler des sites JavaScript.

Récapitulatif

Points clés

  • fetch est intégré à Node 18+. Pas de node-fetch, pas de flag, pas d'import. C'est un global, comme dans le navigateur.
  • Vérifiez toujours response.ok. fetch ne rejette qu'en cas d'erreurs réseau, donc un 404 ou 500 se résout normalement et passe un simple try/catch.
  • POST est un deuxième argument. Définissez method, un en-tête Content-Type et un body sérialisé avec JSON.stringify ; construisez les chaînes de requête avec URLSearchParams.
  • Ajoutez des timeouts avec AbortController. Il n'y a pas de timeout intégré ; passez un signal et abandonnez sur un timer ou une action d'annulation.
  • Parallélisez avec Promise.all. Lancez les requêtes indépendantes ensemble, protégez chacune avec .catch() et limitez la concurrence pour les grands lots.
  • fetch simple ne peut pas scraper les sites modernes. Il ne rend pas le JavaScript et est bloqué ; routez le même fetch via l'API Crawling pour du HTML rendu, puis analysez avec cheerio.

Foire aux questions

Ai-je encore besoin de node-fetch dans Node.js ?

Pas sur Node.js 18 ou version ultérieure. fetch est un global là, vous pouvez donc l'appeler directement sans installation ni import. Vous n'avez besoin de node-fetch que sur Node 16 ou plus ancien. Si vous êtes sur une version LTS actuelle, supprimer la dépendance est sûr et vous permet d'utiliser l'implémentation intégrée plus rapide.

Pourquoi fetch ne lève pas d'exception sur un 404 ou 500 ?

Parce que fetch traite tout échange HTTP complété comme un succès, même quand le code de statut est une erreur. La promesse ne rejette qu'en cas d'échecs au niveau réseau comme une connexion coupée, une défaillance DNS ou un abandon. Pour intercepter les erreurs HTTP vous devez vérifier response.ok (vrai uniquement pour 200 à 299) vous-même et lever une exception quand c'est faux, puis laisser votre catch le gérer.

Comment envoyer du JSON dans une requête POST avec fetch ?

Passez un objet d'options comme deuxième argument avec method: "POST", un objet headers qui définit "Content-Type": "application/json", et un body produit par JSON.stringify(). Le corps doit être une chaîne, c'est pourquoi vous sérialisez d'abord l'objet. Lisez la réponse du serveur avec await response.json() après avoir confirmé response.ok.

Comment ajouter un timeout à une requête fetch ?

Utilisez un AbortController. Créez-en un, passez son signal dans les options de fetch et appelez controller.abort() depuis un setTimeout. Une requête abandonnée rejette avec un AbortError, donc vérifiez error.name dans votre catch pour signaler un timeout clairement. Sur les versions récentes de Node, AbortSignal.timeout(ms) encapsule ce schéma en un seul appel.

Dois-je utiliser fetch ou Axios dans Node ?

Utilisez fetch pour des requêtes sans dépendance et conformes aux standards ; il couvre la plupart des travaux GET et POST dès la sortie de la boîte sur Node 18+. Choisissez Axios quand vous voulez ses commodités intégrées comme les intercepteurs, la gestion JSON automatique, les timeouts configurés et le rejet automatique sur les réponses non-2xx. Pour un nouveau projet qui n'en a pas besoin, l'intégré est le choix le plus léger ; une base de code Axios existante n'a pas de raison urgente de changer.

Puis-je utiliser fetch pour scraper un site web ?

Vous pouvez récupérer le HTML brut d'une page, mais fetch simple a deux limites sur les vrais sites : il n'exécute pas JavaScript, donc le contenu rendu côté client revient vide, et les requêtes depuis des IPs de datacenters sont mises au défi ou bloquées. La solution est d'envoyer votre appel fetch à l'API Crawling, qui rend la page dans un vrai navigateur derrière des IPs résidentielles rotatives et renvoie le HTML finalisé que vous pouvez analyser avec cheerio.

Commencer à construire

Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.

Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.

En libre-service · Sans appel commercial requis · Volumes de crawl entreprise disponibles