Cours JavaScript et React · Module 11: Récupération de données et API

POST, PUT, DELETE - Envoyer des données au serveur

6 min de lecture
Dans cette leçon6

Jusqu'ici, nous n'avons fait qu'écouter le Centre de contrôle : nous récupérions des données avec GET. Mais l'équipage veut aussi signaler des choses : enregistrer une nouvelle mission, corriger sa destination ou l'annuler. GET seul ne peut pas faire ça. Nous allons maintenant apprendre à envoyer des données au serveur : créer de nouvelles ressources (POST), mettre à jour celles qui existent (PUT) et les supprimer (DELETE). C'est comme envoyer des ordres depuis notre vaisseau spatial au Centre de contrôle.

Les méthodes HTTP

Une méthode HTTP indique au serveur ce que tu veux faire de la ressource à une adresse donnée. La même adresse /missions/7 peut signifier « afficher », « modifier » ou « supprimer », selon la méthode :

1// GET    - Récupérer des données (méthode par défaut de fetch)
2// POST   - Créer une nouvelle ressource
3// PUT    - Mettre à jour toute la ressource
4// PATCH  - Mettre à jour une partie de la ressource
5// DELETE - Supprimer une ressource

La différence entre PUT et PATCH est importante : PUT envoie l'objet complet et remplace l'ancien, PATCH n'envoie que les champs modifiés. Si tu oublies un champ dans un PUT, un serveur qui suit cette convention peut l'effacer.

Requête POST - Créer de nouvelles ressources

Par défaut, fetch() envoie un GET. Pour envoyer des données, on passe un second argument, un objet d'options, avec trois champs : method (la méthode), headers (les en-têtes) et body (le contenu de la requête) :

1async function createMission(missionData) {
2  const response = await fetch(
3    'https://api.space-center.com/missions',
4    {
5      method: 'POST',
6      headers: {
7        'Content-Type': 'application/json',
8      },
9      body: JSON.stringify(missionData),
10    }
11  );
12
13  if (!response.ok) {
14    throw new Error('Impossible de créer la mission');
15  }
16
17  const newMission = await response.json();
18  return newMission;
19}
20
21// Utilisation :
22const mission = await createMission({
23  name: 'Alpha Centauri Exploration',
24  crew: ['Capitaine Nova', 'Dr. Astro'],
25  priority: 'high',
26});

Pourquoi JSON.stringify ? Le champ body accepte du texte, un FormData, un Blob et quelques autres types, mais il ne sérialise pas un objet JavaScript ordinaire. Si tu passais l'objet directement, c'est la chaîne "[object Object]" qui partirait. La vérification de response.ok reste la même qu'avec GET, car fetch() ne lève pas d'erreur pour les codes 4xx/5xx avec POST non plus.

Les en-têtes de requête

L'en-tête Content-Type indique au serveur dans quel format nous envoyons les données. Sans lui, le serveur risque de ne pas reconnaître le JSON et de rejeter la requête :

1// JSON - le format le plus utilisé
2headers: {
3  'Content-Type': 'application/json',
4}
5
6// Données de formulaire
7headers: {
8  'Content-Type': 'application/x-www-form-urlencoded',
9}
10
11// Autorisation - token d'accès
12headers: {
13  'Content-Type': 'application/json',
14  'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
15}

Ne le confonds pas avec l'en-tête Accept : celui-ci indique le format que tu veux recevoir dans la réponse. Un piège : quand tu envoies un FormData (par exemple avec un fichier), ne définis pas Content-Type à la main, car le navigateur l'ajoute lui-même avec la limite entre les parties (boundary).

Formulaire avec POST en React

Dans une vraie application, les données d'un POST viennent d'un formulaire. Il nous faut des états pour les champs, un drapeau submitting (pour désactiver le bouton) et des états pour le résultat et l'erreur. D'abord, la logique d'envoi :

1import React, { useState } from 'react';
2
3function MissionForm() {
4  const [name, setName] = useState('');
5  const [destination, setDestination] = useState('');
6  const [submitting, setSubmitting] = useState(false);
7  const [result, setResult] = useState(null);
8  const [error, setError] = useState(null);
9
10  const handleSubmit = async (e) => {
11    e.preventDefault();
12    setSubmitting(true);
13    setError(null);
14
15    try {
16      const response = await fetch(
17        'https://api.space-center.com/missions',
18        {
19          method: 'POST',
20          headers: { 'Content-Type': 'application/json' },
21          body: JSON.stringify({ name, destination }),
22        }
23      );
24
25      if (!response.ok) {
26        throw new Error(`Erreur : ${response.status}`);
27      }
28
29      const data = await response.json();
30      setResult(data);
31      setName('');
32      setDestination('');
33    } catch (err) {
34      setError(err.message);
35    } finally {
36      setSubmitting(false);
37    }
38  };

e.preventDefault() empêche le rechargement de page par défaut du formulaire. On ne vide les champs qu'après un succès, donc en cas d'erreur l'utilisateur ne perd pas ce qu'il a saisi. La seconde partie du même composant, c'est la vue :

1  return (
2    <form onSubmit={handleSubmit}>
3      <input
4        value={name}
5        onChange={e => setName(e.target.value)}
6        placeholder="Nom de la mission"
7        required
8      />
9      <input
10        value={destination}
11        onChange={e => setDestination(e.target.value)}
12        placeholder="Destination"
13        required
14      />
15      <button type="submit" disabled={submitting}>
16        {submitting ? 'Création de la mission...' : 'Créer la mission'}
17      </button>
18
19      {error && <p className="error">{error}</p>}
20      {result && (
21        <p className="success">
22          Mission "{result.name}" créée ! ID : {result.id}
23        </p>
24      )}
25    </form>
26  );
27}

disabled={submitting} protège contre un double clic qui enverrait deux missions identiques. React 19 a aussi ajouté les Actions (useActionState, la prop action sur <form>), qui prennent en charge une partie de ce travail d'état, mais la requête HTTP elle-même reste exactement la même.

PUT - Mettre à jour une ressource

Une mise à jour ressemble presque à une création. Deux choses changent : la méthode devient PUT et l'adresse pointe maintenant vers une mission précise grâce à son id :

1async function updateMission(id, updatedData) {
2  const response = await fetch(
3    `https://api.space-center.com/missions/${id}`,
4    {
5      method: 'PUT',
6      headers: { 'Content-Type': 'application/json' },
7      body: JSON.stringify(updatedData),
8    }
9  );
10
11  if (!response.ok) {
12    throw new Error('La mise à jour a échoué');
13  }
14
15  return await response.json();
16}

L'en-tête et JSON.stringify restent inchangés. Souviens-toi simplement que updatedData doit contenir l'objet mission complet.

DELETE - Supprimer une ressource

DELETE est le plus simple, car il n'envoie généralement aucun contenu : la méthode et l'adresse suffisent. Le serveur répond souvent avec le code 204 No Content, c'est-à-dire « succès, mais sans contenu », donc dans ce cas on n'appelle pas response.json() :

1async function deleteMission(id) {
2  const response = await fetch(
3    `https://api.space-center.com/missions/${id}`,
4    {
5      method: 'DELETE',
6    }
7  );
8
9  if (!response.ok) {
10    throw new Error('La suppression a échoué');
11  }
12
13  // DELETE renvoie souvent 204 No Content (pas de body)
14  return response.status === 204;
15}

Appeler response.json() sur une réponse 204 vide lèverait une erreur de parsing, c'est pourquoi on retourne seulement une information sur le statut. Branchons maintenant cette fonction sur un bouton dans un composant :

1// Dans un composant React
2function MissionItem({ mission, onDelete }) {
3  const [deleting, setDeleting] = useState(false);
4
5  const handleDelete = async () => {
6    if (!window.confirm('Es-tu sûr de vouloir supprimer cette mission ?')) return;
7
8    setDeleting(true);
9    try {
10      await deleteMission(mission.id);
11      onDelete(mission.id);
12    } catch (err) {
13      alert('Erreur : ' + err.message);
14    } finally {
15      setDeleting(false);
16    }
17  };
18
19  return (
20    <div>
21      <h3>{mission.name}</h3>
22      <button onClick={handleDelete} disabled={deleting}>
23        {deleting ? 'Suppression...' : 'Supprimer la mission'}
24      </button>
25    </div>
26  );
27}

Le composant demande une confirmation, car une suppression ne peut pas être annulée, et le parent met à jour la liste via onDelete seulement après le succès côté serveur. Je te recommande cet ordre pour commencer. Dans la leçon sur la mise en cache, tu découvriras une variante plus audacieuse : les optimistic updates, qui modifient l'UI avant même la réponse.

Retiens : GET, c'est l'écoute, alors que POST, PUT et DELETE sont des ordres envoyés au Centre de contrôle, chacun avec la bonne méthode, le bon en-tête et une vérification de response.ok.

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2
3// Simulation du serveur d'API
4const mockDatabase = {
5  missions: [
6    { id: 1, name: "Mission Alpha", destination: "Mars", status: "active" },
7    { id: 2, name: "Reconnaissance Beta", destination: "Europe", status: "active" },
8    { id: 3, name: "Étude Gamma", destination: "Titan", status: "completed" },
9  ],
10  nextId: 4,
11};
12
13function fakeFetchApi(url, options = {}) {
14  return new Promise((resolve, reject) => {
15    setTimeout(() => {
16      const method = (options.method || 'GET').toUpperCase();
17
18      if (method === 'GET') {
19        resolve({
20          ok: true,
21          json: () => Promise.resolve({ missions: [...mockDatabase.missions] }),
22        });
23      } else if (method === 'POST') {
24        const body = JSON.parse(options.body);
25        const newMission = { ...body, id: mockDatabase.nextId++, status: 'active' };
26        mockDatabase.missions.push(newMission);
27        resolve({
28          ok: true,
29          status: 201,
30          json: () => Promise.resolve(newMission),
31        });
32      } else if (method === 'DELETE') {
33        const id = parseInt(url.split('/').pop());
34        mockDatabase.missions = mockDatabase.missions.filter(m => m.id !== id);
35        resolve({ ok: true, status: 204 });
36      }
37    }, 800);
38  });
39}
40
41function MissionControl() {
42  const [missions, setMissions] = useState([]);
43  const [name, setName] = useState('');
44  const [destination, setDestination] = useState('');
45  const [submitting, setSubmitting] = useState(false);
46  const [loading, setLoading] = useState(false);
47  const [message, setMessage] = useState(null);
48
49  const loadMissions = async () => {
50    setLoading(true);
51    const response = await fakeFetchApi('/api/missions');
52    const data = await response.json();
53    setMissions(data.missions);
54    setLoading(false);
55  };
56
57  const createMission = async (e) => {
58    e.preventDefault();
59    if (!name || !destination) return;
60    setSubmitting(true);
61    setMessage(null);
62
63    try {
64      const response = await fakeFetchApi('/api/missions', {
65        method: 'POST',
66        headers: { 'Content-Type': 'application/json' },
67        body: JSON.stringify({ name, destination }),
68      });
69      const newMission = await response.json();
70      setMissions(prev => [...prev, newMission]);
71      setName('');
72      setDestination('');
73      setMessage({ type: 'success', text: `Mission "${newMission.name}" créée !` });
74    } catch (err) {
75      setMessage({ type: 'error', text: err.message });
76    } finally {
77      setSubmitting(false);
78    }
79  };
80
81  const deleteMission = async (id) => {
82    try {
83      await fakeFetchApi(`/api/missions/${id}`, { method: 'DELETE' });
84      setMissions(prev => prev.filter(m => m.id !== id));
85      setMessage({ type: 'success', text: 'Mission supprimée' });
86    } catch (err) {
87      setMessage({ type: 'error', text: err.message });
88    }
89  };
90
91  React.useEffect(() => { loadMissions(); }, []);
92
93  return (
94    <div className="control">
95      <h1>Centre de contrôle des missions</h1>
96
97      <form className="mission-form" onSubmit={createMission}>
98        <h3>Nouvelle mission (POST)</h3>
99        <input value={name} onChange={e => setName(e.target.value)} placeholder="Nom de la mission" />
100        <input value={destination} onChange={e => setDestination(e.target.value)} placeholder="Destination" />
101        <button type="submit" disabled={submitting}>
102          {submitting ? 'Création...' : 'Créer la mission (POST)'}
103        </button>
104      </form>
105
106      {message && (
107        <div className={"message " + message.type}>{message.text}</div>
108      )}
109
110      <h3>Missions actives ({missions.length})</h3>
111      {loading ? <p>Chargement...</p> : (
112        <div className="mission-list">
113          {missions.map(m => (
114            <div key={m.id} className="mission-card">
115              <div>
116                <h4>{m.name}</h4>
117                <p>Destination : {m.destination} | Statut : {m.status}</p>
118              </div>
119              <button className="delete-btn" onClick={() => deleteMission(m.id)}>
120                Supprimer (DELETE)
121              </button>
122            </div>
123          ))}
124        </div>
125      )}
126    </div>
127  );
128}
129
130export default MissionControl;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Pourquoi utilisons-nous JSON.stringify() pour passer des données dans le body d'une requête POST ?

  2. 2. Quel en-tête HTTP informe le serveur que nous envoyons des données au format JSON ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les étapes du cycle requête-réponse HTTP :

  • Éditeur de code

    Formulaire avec requête POST

  • Éditeur de code

    Opérations CRUD

  • Tri vertical

    Ordonne la syntaxe d'un appel fetch avec la méthode POST et l'en-tête Content-Type :

Articles utiles