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

Envoyer des données avec fetch (POST, PUT, DELETE)

Apprenez à envoyer des données avec fetch : options method, headers et body, fausse API REST locale avec json-server, module api.js pour toutes les méthodes, exemple complet de gestion des stagiaires ...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Apprenez à envoyer des données avec fetch : options method, headers et body, fausse API REST locale avec json-server, module api.js pour toutes les méthodes, exemple complet de gestion des stagiaires (création, modification, suppression), différence entre PUT et PATCH, FormData et envoi de fichiers, erreurs de validation 400 et 422, API protégées (401, 403, jetons, CSRF) et bonnes pratiques d'interface. Avec des activités corrigées.
  1. Objectifs

    • Envoyer des données avec fetch: options method, headers et body.
    • Créer, remplacer, modifier et supprimer une ressource (POST, PUT, PATCH, DELETE).
    • Envoyer du JSON, un formulaire et un fichier (FormData).
    • Traiter les réponses : 201, 204, erreurs de validation 400 et 422, authentification 401 et 403.
    • Mettre en place une fausse API REST locale avec json-server pour s'entraîner.
  2. Les options de fetch

  3. const reponse = await fetch(url, {
      method: "POST",                         // GET par défaut
      headers: {                              // en-têtes de la requête
        "Content-Type": "application/json",   // format du corps envoyé
        Accept: "application/json",           // format de réponse souhaité
      },
      body: JSON.stringify(donnees),          // le corps : du TEXTE, pas un objet
    });
    OptionValeursRôle
    method"GET", "POST", "PUT", "PATCH", "DELETE"…L'action demandée (écrivez PATCH en majuscules : c'est la seule méthode que le navigateur ne corrige pas)
    headersObjet ou new Headers()En-têtes à envoyer
    bodyTexte, FormData, URLSearchParams, BlobDonnées envoyées (interdit avec GET et HEAD)
    signalAbortSignalDélai maximal et annulation (leçon 4)
    credentials"same-origin" (défaut), "include", "omit"Envoi des cookies ; "include" pour une API d'un autre domaine qui utilise des cookies
    cache"default", "no-store", "reload"…Utilisation du cache HTTP
    keepalivetrueLa requête continue même si la page est fermée (statistiques, sauvegarde finale)
    Les deux oublis les plus fréquents : envoyer l'objet sans JSON.stringify() (le serveur reçoit le texte "[object Object]"), et oublier l'en-tête Content-Type: application/json (le serveur ne sait pas lire le corps et répond souvent 400 ou 415).
  4. S'entraîner avec une vraie API : json-server

    • Les API publiques de démonstration (comme JSONPlaceholder) font semblant d'enregistrer les modifications : elles répondent correctement, mais ne gardent rien. Pour un vrai CRUD, on lance une API locale en une commande avec json-server, qui transforme un fichier JSON en API REST complète.
    • Créez un dossier fetch-crud contenant ce fichier db.json:
    {
      "stagiaires": [
        { "id": "1", "nom": "Amira Ben Ali", "groupe": "DEV-101", "moyenne": 15.5 },
        { "id": "2", "nom": "Youssef Trabelsi", "groupe": "DEV-102", "moyenne": 12 },
        { "id": "3", "nom": "Sana Gharbi", "groupe": "DEV-101", "moyenne": 17.25 }
      ]
    }
    • Dans un terminal ouvert dans ce dossier (Node.js doit être installé) :
    npx json-server db.json
    # JSON Server started on PORT :3000
    # http://localhost:3000/stagiaires
    • L'API répond aussitôt à toutes les routes REST, avec CORS autorisé, et écrit les modifications dans db.json:
    RequêteEffetRéponse
    GET /stagiairesListe200 + tableau
    GET /stagiaires?groupe=DEV-101Liste filtrée200 + tableau
    GET /stagiaires/2Un stagiaire200 + objet, ou 404
    POST /stagiairesCréation (identifiant généré)201 + objet créé
    PUT /stagiaires/2Remplacement complet200 + objet
    PATCH /stagiaires/2Modification partielle200 + objet
    DELETE /stagiaires/2Suppression200 + objet supprimé
    • json-server est un outil de développement (maquettes, cours, tests) : en production, l'API est écrite avec un vrai serveur (PHP/Symfony, Node.js, Python…) et une base de données.
  5. Une fonction pour toutes les méthodes

  6. Toutes les requêtes se ressemblent : on les centralise dans un module api.js.

    // Toutes les requêtes vers l'API REST des stagiaires
    const API = "http://localhost:3000/stagiaires";
    
    async function requete(url, methode = "GET", donnees) {
      const options = { method: methode, headers: { Accept: "application/json" } };
      if (donnees !== undefined) {
        options.headers["Content-Type"] = "application/json";
        options.body = JSON.stringify(donnees);
      }
      const reponse = await fetch(url, options);
      if (!reponse.ok) {
        throw new Error(`${methode} ${url} : erreur HTTP ${reponse.status}`);
      }
      // 204 No Content : pas de corps à lire
      return reponse.status === 204 ? null : reponse.json();
    }
    
    export const lister = () => requete(API); // GET    /stagiaires
    export const lire = (id) => requete(`${API}/${id}`); // GET    /stagiaires/2
    export const creer = (stagiaire) => requete(API, "POST", stagiaire); // POST   /stagiaires
    export const remplacer = (id, stagiaire) => requete(`${API}/${id}`, "PUT", stagiaire); // PUT    /stagiaires/2
    export const modifier = (id, champs) => requete(`${API}/${id}`, "PATCH", champs); // PATCH  /stagiaires/2
    export const supprimer = (id) => requete(`${API}/${id}`, "DELETE"); // DELETE /stagiaires/2
  • Le corps et l'en-tête Content-Type ne sont ajoutés que s'il y a des données à envoyer.
  • Une réponse 204 No Content n'a pas de corps : appeler json() dessus provoquerait une erreur.
  • Chaque opération CRUD tient ensuite en une ligne, et le reste de l'application ne connaît ni l'adresse de l'API ni le détail des requêtes.
  1. Exemple complet : gestion des stagiaires

  2. Une page qui liste les stagiaires de l'API, permet d'en ajouter (POST), de modifier la moyenne (PATCH) et de supprimer (DELETE). Après chaque opération, la liste est rechargée depuis le serveur.

    index.html:

    <!doctype html>
    <html lang="fr">
      <head>
        <meta charset="utf-8" />
        <title>Gestion des stagiaires – CRUD avec fetch</title>
        <link
          href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.9/dist/css/bootstrap.min.css"
          rel="stylesheet"
        />
        <script type="module" src="crud.js"></script>
      </head>
      <body class="container py-4" style="max-width: 760px">
        <h1 class="h3">Gestion des stagiaires</h1>
    
        <form id="form-stagiaire" class="row g-2 mb-3">
          <div class="col-sm-5">
            <input id="nom" class="form-control" placeholder="Nom et prénom" required />
          </div>
          <div class="col-sm-3">
            <select id="groupe" class="form-select">
              <option>DEV-101</option>
              <option>DEV-102</option>
            </select>
          </div>
          <div class="col-sm-2">
            <input
              id="moyenne"
              class="form-control"
              type="number"
              min="0"
              max="20"
              step="0.25"
              placeholder="Moy."
              required
            />
          </div>
          <div class="col-sm-2">
            <button class="btn btn-primary w-100" type="submit">Ajouter</button>
          </div>
        </form>
    
        <div id="message" class="alert d-none"></div>
    
        <table class="table table-hover align-middle">
          <thead>
            <tr>
              <th>Nom</th>
              <th>Groupe</th>
              <th>Moyenne</th>
              <th class="text-end">Actions</th>
            </tr>
          </thead>
          <tbody id="lignes"></tbody>
        </table>
      </body>
    </html>

    crud.js:

    import { lister, creer, modifier, supprimer } from "./api.js";
    
    const lignes = document.getElementById("lignes");
    const message = document.getElementById("message");
    
    function informer(texte, type = "success") {
      message.className = `alert alert-${type}`;
      message.textContent = texte;
    }
    
    function bouton(texte, classe, action) {
      const element = document.createElement("button");
      element.type = "button";
      element.className = `btn btn-sm ${classe} ms-1`;
      element.textContent = texte;
      element.dataset.action = action;
      return element;
    }
    
    async function afficher() {
      try {
        const stagiaires = await lister();
        lignes.replaceChildren(
          ...stagiaires.map((stagiaire) => {
            const tr = document.createElement("tr");
            tr.dataset.id = stagiaire.id;
            for (const valeur of [stagiaire.nom, stagiaire.groupe, `${stagiaire.moyenne}/20`]) {
              const td = document.createElement("td");
              td.textContent = valeur;
              tr.append(td);
            }
            const actions = document.createElement("td");
            actions.className = "text-end";
            actions.append(
              bouton("✏️ Note", "btn-outline-primary", "noter"),
              bouton("🗑️", "btn-outline-danger", "supprimer"),
            );
            tr.append(actions);
            return tr;
          }),
        );
      } catch (erreur) {
        informer(
          `Serveur injoignable : lancez « npx json-server db.json ». (${erreur.message})`,
          "danger",
        );
      }
    }
    
    // CREATE : POST
    document.getElementById("form-stagiaire").addEventListener("submit", async (event) => {
      event.preventDefault();
      const formulaire = event.target;
      try {
        const cree = await creer({
          nom: document.getElementById("nom").value.trim(),
          groupe: document.getElementById("groupe").value,
          moyenne: Number(document.getElementById("moyenne").value),
        });
        informer(`« ${cree.nom} » ajouté (identifiant ${cree.id}).`);
        formulaire.reset();
        afficher();
      } catch (erreur) {
        informer(erreur.message, "danger");
      }
    });
    
    // UPDATE (PATCH) et DELETE, par délégation
    lignes.addEventListener("click", async (event) => {
      const action = event.target.dataset.action;
      const id = event.target.closest("tr")?.dataset.id;
      if (!action || !id) {
        return;
      }
      try {
        if (action === "noter") {
          const saisie = prompt("Nouvelle moyenne (0 à 20) :");
          const moyenne = Number(saisie);
          if (
            saisie === null ||
            saisie.trim() === "" ||
            Number.isNaN(moyenne) ||
            moyenne < 0 ||
            moyenne > 20
          ) {
            return;
          }
          await modifier(id, { moyenne });
          informer("Moyenne mise à jour.");
        } else if (action === "supprimer" && confirm("Supprimer ce stagiaire ?")) {
          await supprimer(id);
          informer("Stagiaire supprimé.");
        }
        afficher();
      } catch (erreur) {
        informer(erreur.message, "danger");
      }
    });
    
    afficher();
  • Lancez npx json-server db.json, puis la page avec Live Server. Ouvrez db.json dans VS Code : il change à chaque action.
  • Dans l'onglet Réseau, observez pour chaque action la méthode, le corps envoyé (Payload), le statut et la réponse. Pour PATCH et DELETE, vous verrez aussi une requête OPTIONS: c'est la vérification CORS (preflight), car la page (port 5500) et l'API (port 3000) n'ont pas la même origine.
  • Les boutons de chaque ligne sont gérés par délégation (un seul écouteur sur le tableau) avec data-action, et l'identifiant est lu dans data-id sur la ligne.
  • PUT ou PATCH ?

  • // Le stagiaire 2 vaut : { id: "2", nom: "Youssef Trabelsi", groupe: "DEV-102", moyenne: 12 }
    
    await modifier("2", { moyenne: 13 });
    // PATCH → { id: "2", nom: "Youssef Trabelsi", groupe: "DEV-102", moyenne: 13 }
    
    await remplacer("2", { moyenne: 13 });
    // PUT → { id: "2", moyenne: 13 }   ← le nom et le groupe ont disparu !
    • PUT remplace toute la ressource par ce que vous envoyez : il faut envoyer l'objet complet.
    • PATCH ne modifie que les champs envoyés : c'est généralement ce que l'on veut pour un formulaire de modification.
  • Envoyer un formulaire et des fichiers : FormData

    • Pour un formulaire HTML classique, inutile de lire chaque champ : new FormData(formulaire) récupère tous les champs qui ont un attribut name, fichiers compris.
    // Envoyer un formulaire HTML complet, fichier compris
    const formulaire = document.getElementById("form-inscription"); // contient <input type="file" name="cv">
    
    formulaire.addEventListener("submit", async (event) => {
      event.preventDefault();
      const donnees = new FormData(formulaire); // tous les champs, d'après leur attribut name
    
      const reponse = await fetch("/api/inscriptions", {
        method: "POST",
        body: donnees, // surtout PAS de Content-Type : le navigateur écrit multipart/form-data
      });
    
      if (reponse.status === 422) {
        const { erreurs } = await reponse.json(); // le serveur détaille les champs invalides
        console.table(erreurs);
      }
    });
    • Avec FormData, n'écrivez pas l'en-tête Content-Type: le navigateur le génère lui-même (multipart/form-data) avec un séparateur indispensable au serveur.
    • Pour transformer un formulaire en objet JSON : Object.fromEntries(new FormData(formulaire)).
    • Pour envoyer des données « comme un formulaire » sans fichier, body: new URLSearchParams({ email, motDePasse }) produit le format application/x-www-form-urlencoded, lu directement par PHP dans $_POST.
  • Les erreurs de validation : 400 et 422

    • Un serveur sérieux revérifie toujours les données (la validation dans le navigateur peut être contournée). En cas de problème, il répond 400 ou 422 avec un corps qui détaille les erreurs, par exemple :
    HTTP/1.1 422 Unprocessable Content
    Content-Type: application/json
    
    {
      "message": "Les données envoyées sont invalides.",
      "erreurs": {
        "moyenne": "La moyenne doit être comprise entre 0 et 20.",
        "nom": "Le nom est obligatoire."
      }
    }
    • Côté client, on lit ce corps pour afficher chaque message sous le bon champ :
    const reponse = await fetch(API, { method: "POST", headers, body: JSON.stringify(stagiaire) });
    if (reponse.status === 422) {
      const { erreurs } = await reponse.json();
      for (const [champ, message] of Object.entries(erreurs)) {
        const element = document.getElementById(champ);
        element.classList.add("is-invalid"); // style Bootstrap
        element.nextElementSibling.textContent = message; // <div class="invalid-feedback">
      }
    }
  • Les API protégées : 401 et 403

    • La plupart des API demandent une preuve d'identité. Deux méthodes courantes :
      • un cookie de session HttpOnly posé à la connexion : le navigateur l'envoie tout seul (ajoutez credentials: "include" si l'API est sur un autre domaine) ;
      • un jeton (token, souvent au format JWT) reçu à la connexion, envoyé dans l'en-tête Authorization:
    // Appel d'une API protégée par un jeton (token)
    const reponse = await fetch("https://api.centre.tn/v1/notes", {
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${jeton}`, // jeton reçu à la connexion
      },
    });
    
    if (reponse.status === 401) {
      // non authentifié (jeton absent ou expiré) → retour à la page de connexion
      location.href = "/connexion";
    } else if (reponse.status === 403) {
      // authentifié mais pas autorisé
      console.error("Accès refusé à cette ressource.");
    }
    • 401: pas ou plus authentifié → rediriger vers la connexion. 403: identifié mais pas autorisé → afficher un message, ne pas redemander le mot de passe.
    • Rappel (chapitre 20) : un jeton stocké dans le localStorage peut être volé par une faille XSS ; les cookies HttpOnly sont plus sûrs pour les sessions.
    • Une page qui envoie des requêtes modifiant des données avec des cookies doit aussi être protégée contre les attaques CSRF (un autre site qui déclenche une action à votre insu) : cookies SameSite et jeton anti-CSRF fourni par le serveur, que les frameworks comme Symfony gèrent pour vous.
  • Bonnes pratiques pour l'interface

    • Désactivez le bouton pendant l'envoi, pour éviter un double clic qui créerait deux fois la même ressource (POST n'est pas idempotent).
    • Confirmez les suppressions et affichez un message de réussite ou d'échec.
    • Mise à jour optimiste: pour une interface très réactive, on peut modifier l'affichage avant la réponse du serveur, puis revenir en arrière si la requête échoue. C'est ce que font les réseaux sociaux pour le bouton « J'aime ».
    • Validez dans le navigateur (required, min, max) pour le confort de l'utilisateur, mais ne comptez jamais dessus pour la sécurité.
  • Activités

  • Activité 1 : l'API avec curl ou un client HTTP

    • Lancez json-server avec le db.json de la leçon. Avec curl, Bruno ou l'extension REST Client de VS Code, envoyez : la liste ; le stagiaire 3 ; une création ; un PATCH de la moyenne du stagiaire 1 ; une suppression ; une demande du stagiaire 999. Notez les codes de statut et vérifiez db.json.

    Activité 2 : la page de gestion

    • Reproduisez l'exemple complet. Désactivez le bouton « Ajouter » pendant l'envoi et affichez « Envoi… » à la place de son texte.
    • Remplacez le prompt() de modification par un petit formulaire dans la ligne (champ numérique + boutons Enregistrer et Annuler).
    • Ajoutez un filtre par groupe qui utilise le paramètre ?groupe= de l'API.

    Activité 3 : une deuxième ressource

    • Ajoutez dans db.json une collection formations (id, intitule, duree, niveau) et créez une page qui permet de les lister, d'en ajouter et d'en supprimer, en réutilisant la fonction requete().

    Activité 4 : questions

    • Pourquoi faut-il JSON.stringify() sur le corps ? Que reçoit le serveur sans lui ?
    • Quelle est la différence entre PUT et PATCH ?
    • Pourquoi ne faut-il pas écrire l'en-tête Content-Type quand on envoie un FormData?
    • Que signifient les codes 201, 204, 401, 403 et 422 ?
    Corrigés. Activité 1 : 200, 200, 201, 200, 200 (json-server renvoie l'objet supprimé ; d'autres API renvoient 204) et 404. Activité 2 : bouton.disabled = true; bouton.textContent = "Envoi…"; avant l'await, et retour à l'état initial dans un finally. Activité 4 : body doit être du texte ; un objet serait converti en "[object Object]"; PUT remplace toute la ressource, PATCH ne modifie que les champs envoyés ; le navigateur doit écrire lui-même multipart/form-data avec la limite (boundary) qui sépare les champs ; 201 ressource créée, 204 succès sans contenu, 401 non authentifié, 403 non autorisé, 422 données invalides.
  • À retenir

    • fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(donnees) }).
    • POST crée (201), PUT remplace, PATCH modifie en partie, DELETE supprime (204 ou 200).
    • FormData pour les formulaires et les fichiers, sans en-tête Content-Type.
    • Lisez le corps des erreurs 400/422 pour guider l'utilisateur ; 401 → connexion, 403 → accès refusé ; json-server permet de s'entraîner sur une vraie API REST.

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