Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

L'API fetch : récupérer des données (GET)

Maîtrisez l'API fetch pour récupérer des données : syntaxe avec then et async/await, objet Response (ok, status, headers, json, text, blob), piège des erreurs HTTP non rejetées, fonction getJson réuti...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Maîtrisez l'API fetch pour récupérer des données : syntaxe avec then et async/await, objet Response (ok, status, headers, json, text, blob), piège des erreurs HTTP non rejetées, fonction getJson réutilisable, tableau de toutes les erreurs, URLSearchParams, délai maximal et annulation avec AbortSignal et AbortController, requêtes en série et en parallèle, exemple complet d'annuaire. Avec des activités corrigées.
  1. Objectifs

    • Récupérer des données avec fetch, avec then() puis avec async/await.
    • Exploiter l'objet Response: ok, status, en-têtes, json(), text(), blob().
    • Gérer toutes les erreurs : erreurs HTTP, réseau, délai dépassé, JSON invalide.
    • Construire des adresses avec URLSearchParams, lancer des requêtes en parallèle et annuler une requête.
    • Afficher les données dans la page, avec un état de chargement.
  2. La fonction fetch

  3. const promesse = fetch(url, options);
  • url: l'adresse de la ressource, absolue (https://…) ou relative à la page (stagiaires.json, /api/stagiaires).
  • options (facultatif) : un objet qui précise la méthode, les en-têtes, le corps… Sans options, fetch envoie une requête GET.
  • fetch renvoie immédiatement une promesse (chapitre 18) ; elle sera tenue avec un objet Response dès que les en-têtes de la réponse seront arrivés.
  • Lire le corps de la réponse est une deuxième opération asynchrone : reponse.json() renvoie une autre promesse, tenue avec les données converties.

fetch : la promesse, l'objet Response, la lecture du corps et les cas d'erreur

  • Premier exemple : then() ou async/await

  • // Version avec then()
    fetch("stagiaires.json")
      .then((reponse) => reponse.json())
      .then((stagiaires) => console.log(stagiaires))
      .catch((erreur) => console.error("Échec :", erreur));
    
    // Même chose avec async / await
    async function chargerStagiaires() {
      const reponse = await fetch("stagiaires.json");
      const stagiaires = await reponse.json();
      console.log(stagiaires);
    }
    chargerStagiaires();
    • Les deux écritures font exactement la même chose. Dans la suite du cours, on utilise async/await, plus lisible dès qu'il y a plusieurs étapes.
    • Rappel : await n'est permis que dans une fonction async, ou au premier niveau d'un module (<script type="module">).
    • Comme pour XMLHttpRequest, testez avec Live Server et non en ouvrant le fichier directement.
  • L'objet Response

  • const reponse = await fetch("stagiaires.json");
    
    console.log(reponse.ok); // true si le statut est entre 200 et 299
    console.log(reponse.status); // 200
    console.log(reponse.statusText); // "OK"
    console.log(reponse.url); // adresse complète finale (après redirections)
    console.log(reponse.headers.get("Content-Type")); // "application/json"
    
    const donnees = await reponse.json(); // le corps ne peut être lu qu'UNE fois
    MéthodeRenvoie (dans une promesse)Pour…
    json()Un objet ou un tableau JavaScriptLes API (cas le plus courant)
    text()Une chaîneTexte, HTML, CSV, Markdown
    blob()Un Blob (fichier binaire)Images, PDF, téléchargements (URL.createObjectURL(blob))
    formData()Un objet FormDataRéponses de formulaires (rare)
    arrayBuffer()Des octets brutsTraitements binaires (audio, fichiers)
    • Le corps est un flux: il ne peut être lu qu'une seule fois. Appeler json() puis text() sur la même réponse provoque l'erreur « body stream already read ». Si vous avez besoin des deux, utilisez reponse.clone().
    • reponse.headers est un objet Headers: get(nom) (insensible à la casse), has(nom), et on peut le parcourir avec for…of. Par sécurité, une page ne voit que certains en-têtes des réponses venant d'un autre site.
  • Le piège n° 1 : fetch ne rejette pas sur un 404

    • La promesse de fetch est tenue dès que le serveur a répondu, quel que soit le code: 200, mais aussi 404 ou 500. Elle n'est rejetée que si aucune réponse n'a pu être obtenue : réseau coupé, nom de domaine inexistant, refus CORS, annulation.
    • Il faut donc toujours tester reponse.ok (vrai pour les statuts 200 à 299). Sinon, reponse.json() essaiera de lire une page d'erreur HTML et lèvera une SyntaxError incompréhensible pour l'utilisateur.
    • On écrit une fois pour toutes une petite fonction réutilisable :
    // Fonction réutilisable : GET + vérification du statut + JSON
    async function getJson(url, options = {}) {
      const reponse = await fetch(url, options);
      if (!reponse.ok) {
        throw new Error(`Erreur HTTP ${reponse.status} pour ${url}`);
      }
      return reponse.json();
    }
    
    try {
      const stagiaires = await getJson("stagiaires.json");
      console.log(stagiaires.length, "stagiaires");
      await getJson("inexistant.json"); // lève « Erreur HTTP 404 »
    } catch (erreur) {
      console.error(erreur.message);
    }
    1. Toutes les erreurs possibles

    2. SituationCe qui se passeComment la reconnaître
      Réponse 4xx ou 5xxPromesse tenue!reponse.ok, reponse.status
      Pas de réseau, serveur injoignable, refus CORSPromesse rejetéeTypeError (« Failed to fetch »)
      Délai dépassé (AbortSignal.timeout())Promesse rejetéeerreur.name === "TimeoutError"
      Annulation volontaire (AbortController)Promesse rejetéeerreur.name === "AbortError"
      Corps qui n'est pas du JSON validePromesse de json() rejetéeSyntaxError
    • Pour des raisons de sécurité, le navigateur ne dit pas à JavaScript pourquoi une requête a échoué (réseau ou CORS) : le détail n'apparaît que dans la console.
    • Montrez toujours à l'utilisateur un message compréhensible (« Impossible de joindre le service, vérifiez votre connexion ») et gardez le détail technique pour la console.
  • Construire l'adresse : URLSearchParams

    • Les paramètres d'une requête GET se placent dans l'URL. Ne les assemblez pas à la main : un espace, un accent ou un & dans une valeur casserait l'adresse. URLSearchParams encode tout correctement :
    const params = new URLSearchParams({
      q: "développeur web & mobile",
      ville: "Gabès",
      page: 2,
    });
    params.append("tri", "date"); // ajouter un paramètre
    
    const url = `https://api.emploi.tn/offres?${params}`;
    // https://api.emploi.tn/offres?q=d%C3%A9veloppeur+web+%26+mobile&ville=Gab%C3%A8s&page=2&tri=date
    
    const offres = await getJson(url);
    • Pour encoder une seule valeur, encodeURIComponent("Gabès") donne "Gab%C3%A8s".
  • Délai maximal et annulation

    • Sans précaution, une requête peut rester en attente très longtemps sur un réseau mobile faible. Et une requête lancée peut devenir inutile, par exemple quand l'utilisateur tape une nouvelle lettre dans un champ de recherche : sa réponse, si elle arrive en retard, pourrait même écraser la bonne.
    • L'option signal permet d'abandonner une requête :
    // 1. Abandonner automatiquement au bout de 5 secondes
    try {
      const reponse = await fetch("https://api.exemple.tn/lent", {
        signal: AbortSignal.timeout(5000),
      });
      console.log(await reponse.json());
    } catch (erreur) {
      if (erreur.name === "TimeoutError") {
        console.error("Trop lent : requête abandonnée.");
      }
    }
    
    // 2. Annuler soi-même une requête devenue inutile
    const controleur = new AbortController();
    fetch("https://api.exemple.tn/recherche?q=tun", { signal: controleur.signal }).catch((erreur) => {
      if (erreur.name === "AbortError") {
        console.log("Requête annulée");
      }
    });
    controleur.abort(); // par exemple parce que l'utilisateur a tapé une autre lettre
    • AbortSignal.timeout(ms) crée un signal qui se déclenche tout seul après le délai.
    • AbortController crée un signal que l'on déclenche soi-même avec abort().
    • AbortSignal.any([signal1, signal2]) combine les deux : la requête s'arrête au premier des deux événements. Ces trois outils fonctionnent dans tous les navigateurs récents ; le TP 19 les utilise.
  • Plusieurs requêtes : en série ou en parallèle

  • // En SÉRIE : la 2e attend la 1re (utile si elle en dépend)
    const utilisateur = await getJson(`${API}/users/1`);
    const articles = await getJson(`${API}/posts?userId=${utilisateur.id}`);
    
    // En PARALLÈLE : les deux partent en même temps → deux fois plus rapide
    const [articles2, taches] = await Promise.all([
      getJson(`${API}/users/2/posts`),
      getJson(`${API}/users/2/todos`),
    ]);
    
    // Promise.allSettled : attend tout, même si certaines échouent (leçon 6)
    • Promise.all échoue dès qu'une des requêtes échoue. Utilisez Promise.allSettled quand chaque résultat est utile même si les autres ont échoué.
  • Exemple complet : un annuaire

  • Cet exemple interroge l'API de démonstration gratuite JSONPlaceholder (de faux utilisateurs, articles et tâches, avec CORS autorisé). Il charge la liste des utilisateurs, permet de la filtrer, et affiche le détail d'un utilisateur en lançant deux requêtes en parallèle. Si l'on clique sur un autre utilisateur avant la fin, le chargement précédent est annulé.

    index.html:

    <!doctype html>
    <html lang="fr">
      <head>
        <meta charset="utf-8" />
        <title>Annuaire – fetch GET</title>
        <link rel="stylesheet" href="style.css" />
        <script type="module" src="annuaire.js"></script>
      </head>
      <body>
        <h1>Annuaire des utilisateurs</h1>
        <input id="recherche" type="search" placeholder="Filtrer par nom ou ville…" />
        <p id="etat">Chargement…</p>
        <div class="colonnes">
          <ul id="utilisateurs"></ul>
          <section id="detail"></section>
        </div>
      </body>
    </html>

    annuaire.js:

    const API = "https://jsonplaceholder.typicode.com";
    const etat = document.getElementById("etat");
    const listeUtilisateurs = document.getElementById("utilisateurs");
    const detail = document.getElementById("detail");
    let utilisateurs = [];
    let controleur = null;
    
    async function getJson(url, signal) {
      const reponse = await fetch(url, { signal });
      if (!reponse.ok) {
        throw new Error(`Erreur HTTP ${reponse.status}`);
      }
      return reponse.json();
    }
    
    function creer(balise, texte) {
      const element = document.createElement(balise);
      element.textContent = texte;
      return element;
    }
    
    function afficherListe(filtre = "") {
      const mot = filtre.trim().toLowerCase();
      const visibles = utilisateurs.filter(
        (utilisateur) =>
          utilisateur.name.toLowerCase().includes(mot) ||
          utilisateur.address.city.toLowerCase().includes(mot),
      );
      listeUtilisateurs.replaceChildren(
        ...visibles.map((utilisateur) => {
          const li = creer("li", `${utilisateur.name} – ${utilisateur.address.city}`);
          li.dataset.id = utilisateur.id;
          return li;
        }),
      );
      etat.textContent = `${visibles.length} utilisateur(s) sur ${utilisateurs.length}`;
    }
    
    // Détail : deux requêtes lancées EN PARALLÈLE avec Promise.all
    async function afficherDetail(id) {
      controleur?.abort(); // on annule le chargement du détail précédent
      controleur = new AbortController();
      detail.replaceChildren(creer("p", "Chargement du détail…"));
      try {
        const [articles, taches] = await Promise.all([
          getJson(`${API}/users/${id}/posts`, controleur.signal),
          getJson(`${API}/users/${id}/todos?completed=false`, controleur.signal),
        ]);
        const utilisateur = utilisateurs.find((candidat) => candidat.id === id);
        const titres = creer("ol", "");
        titres.append(...articles.slice(0, 5).map((article) => creer("li", article.title)));
        detail.replaceChildren(
          creer("h2", utilisateur.name),
          creer("p", `✉️ ${utilisateur.email} · 🏢 ${utilisateur.company.name}`),
          creer("h3", `Derniers articles (${articles.length} au total)`),
          titres,
          creer("p", `📝 ${taches.length} tâche(s) non terminée(s)`),
        );
      } catch (erreur) {
        if (erreur.name !== "AbortError") {
          detail.replaceChildren(creer("p", `Impossible de charger le détail : ${erreur.message}`));
        }
      }
    }
    
    listeUtilisateurs.addEventListener("click", (event) => {
      const li = event.target.closest("li");
      if (li) {
        listeUtilisateurs.querySelector(".actif")?.classList.remove("actif");
        li.classList.add("actif");
        afficherDetail(Number(li.dataset.id));
      }
    });
    
    document.getElementById("recherche").addEventListener("input", (event) => {
      afficherListe(event.target.value);
    });
    
    try {
      utilisateurs = await getJson(`${API}/users`);
      afficherListe();
    } catch (erreur) {
      etat.textContent = `Chargement impossible : ${erreur.message}`;
    }
    • La liste complète est chargée une seule fois; le filtre travaille ensuite sur la variable utilisateurs, sans nouvelle requête.
    • Toutes les données sont insérées avec textContent (fonction creer()) : même si l'API renvoyait du code HTML malveillant, il serait affiché comme du texte (protection contre les failles XSS).
    • Un message de chargement est affiché pendant l'attente, puis remplacé par le résultat ou par l'erreur.
  • Bonnes pratiques

    • États de l'interface : prévoyez toujours trois affichages : chargement (indicateur, bouton désactivé), succès, erreur (message et, si possible, bouton « Réessayer »). Pensez aussi au cas « aucun résultat ».
    • Ne bloquez pas inutilement : lancez en parallèle les requêtes indépendantes.
    • N'insérez jamais de données reçues avec innerHTML.
    • Centralisez les appels dans un module (api.js) : l'adresse de l'API et la gestion des erreurs sont écrites une seule fois.
    • Le cache HTTP fonctionne aussi avec fetch: une réponse avec un en-tête Cache-Control peut être réutilisée sans nouvel appel. L'option cache: "no-store" force une nouvelle requête.
  • Activités

  • Activité 1 : premiers pas

    • Dans le dossier fetch-get de votre dépôt js-ajax-fetch, chargez stagiaires.json avec fetch et affichez chaque stagiaire dans un tableau HTML (nom, groupe, moyenne).
    • Affichez dans la console reponse.status, reponse.ok et l'en-tête Content-Type. Puis demandez stagiaire.json (sans « s ») : que valent ok et status? Que se passe-t-il si vous appelez quand même reponse.json()?

    Activité 2 : l'annuaire

    • Reproduisez l'exemple complet. Avec DevTools (Réseau → Slow 3G), cliquez rapidement sur trois utilisateurs : combien de requêtes sont annulées (statut canceled) ?
    • Ajoutez un bouton « Réessayer » quand le chargement de la liste échoue (testez avec le mode Offline).
    • Ajoutez dans le détail la liste des albums de l'utilisateur (/users/{id}/albums), chargée en parallèle avec les deux autres requêtes.

    Activité 3 : une image

    • Avec fetch et blob(), téléchargez une image de votre site et affichez-la dans un <img> grâce à URL.createObjectURL(). Affichez aussi sa taille en Ko (blob.size) et son type (blob.type).

    Activité 4 : questions

    • La promesse de fetch est-elle rejetée pour une réponse 500 ? Que faut-il tester ?
    • Pourquoi ne peut-on pas appeler deux fois reponse.json()?
    • Deux requêtes indépendantes prennent chacune 800 ms. Combien de temps faut-il environ en série ? Et avec Promise.all?
    • À quoi sert URLSearchParams?
    Corrigés. Activité 1 : pour stagiaire.json, ok vaut false et status 404 ; reponse.json() lève une SyntaxError car le corps est une page d'erreur et non du JSON. Activité 2 : chaque clic annule la requête précédente ; seules les requêtes du dernier utilisateur arrivent à leur terme. Activité 3 : const blob = await (await fetch("logo.png")).blob(); img.src = URL.createObjectURL(blob);. Activité 4 : non, elle est tenue ; il faut tester reponse.ok ou reponse.status; le corps est un flux qui ne se lit qu'une fois ; environ 1,6 s en série et 0,8 s en parallèle ; à construire des paramètres d'URL correctement encodés.
  • À retenir

    • const reponse = await fetch(url) puis const donnees = await reponse.json().
    • Testez toujours reponse.ok: un 404 ou un 500 ne rejette pas la promesse.
    • URLSearchParams pour les paramètres, AbortSignal.timeout() et AbortController pour le délai et l'annulation, Promise.all pour le parallèle.
    • Affichez un état de chargement, des erreurs compréhensibles, et insérez les données avec textContent.

    Sources : MDN Web Docs, WHATWG (XMLHttpRequest, Fetch), IETF, open-meteo.com.