Séance 13 — Des données depuis Internet

fetch, les API, et un vrai mini-projet

  • Comprendre ce qu’est une API
  • Récupérer des données avec fetch et await
  • Gérer les cas où ça ne répond pas

1. Une API, qu’est-ce que c’est ?

Jusqu’ici, toutes vos données étaient écrites dans votre code. Aujourd’hui, on va les chercher ailleurs, sur Internet.

Beaucoup de sites proposent une adresse spéciale qui ne renvoie pas une page web, mais du JSON. On appelle ça une API.

AstuceEssayez tout de suite

Ouvrez cette adresse dans un nouvel onglet :

https://dog.ceo/api/breeds/image/random

Vous voyez quelque chose comme :

{
  "message": "https://images.dog.ceo/breeds/hound/n02088094_1003.jpg",
  "status": "success"
}

Ce n’est pas une page web : c’est du JSON, exactement le format de la séance 12. Rechargez : ce n’est plus le même chien.

Votre travail aujourd’hui : demander ça depuis votre code, et en faire quelque chose.

2. fetch : la recette

async function chargerChien() {
  const reponse = await fetch("https://dog.ceo/api/breeds/image/random");
  const donnees = await reponse.json();

  console.log(donnees.message);
}

chargerChien();

Trois lignes suffisent. Décortiquons-les.

Ligne Ce qu’elle fait
await fetch(url) Va chercher l’adresse, et attends la réponse
await reponse.json() Lis le JSON de la réponse et transforme-le en objet
donnees.message Utilise l’objet comme n’importe quel objet (séance 11)
Importantasync et await : pourquoi il faut attendre

Aller chercher une information sur Internet prend du temps — parfois une seconde, parfois dix. Pendant ce temps, le navigateur ne doit pas se figer.

  • await veut dire : « mets cette ligne en pause jusqu’à ce que la réponse arrive, puis continue ».
  • async doit être écrit devant la fonction qui contient un await. C’est une obligation du langage.

Règle simple : si vous écrivez await quelque part, remontez et vérifiez que la fonction commence par async.

async function nom() {
  const x = await quelqueChose();
}
AvertissementLes 2 await sont tous les deux obligatoires
const reponse = fetch(url);          // ❌ await oublié
const donnees = reponse.json();      // ❌ await oublié
console.log(donnees.message);        // undefined ou Promise bizarre

Si vous voyez Promise { <pending> } dans la console, c’est toujours un await oublié. C’est l’erreur n°1 de la séance.

Étape 1 — Le générateur de chiens 🐕

chien.html
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <title>Un chien au hasard</title>
    <link rel="stylesheet" href="chien.css" />
  </head>
  <body>
    <h1>Un chien au hasard</h1>

    <button id="nouveau">Nouveau chien</button>
    <p id="etat"></p>
    <img id="photo" src="" alt="Un chien choisi au hasard" />

    <script src="chien.js"></script>
  </body>
</html>
chien.css
body {
  font-family: system-ui, Arial, sans-serif;
  max-width: 700px;
  margin: 0 auto;
  padding: 24px;
  text-align: center;
}

#photo {
  max-width: 100%;
  border-radius: 12px;
  margin-top: 16px;
}
chien.js
const nouveau = document.querySelector("#nouveau");
const photo = document.querySelector("#photo");
const etat = document.querySelector("#etat");

async function chargerChien() {
  etat.textContent = "Chargement…";

  const reponse = await fetch("https://dog.ceo/api/breeds/image/random");
  const donnees = await reponse.json();

  photo.src = donnees.message;
  etat.textContent = "";
}

nouveau.addEventListener("click", () => {
  chargerChien();
});

chargerChien();

Cliquez plusieurs fois. Vous venez d’écrire votre première application qui parle à Internet. 🎉

AstuceLe message « Chargement… »

Il n’est pas décoratif. Sur une connexion lente, sans lui, l’utilisateur voit une page figée et clique dix fois. Toute action qui prend du temps doit se signaler. C’est une règle de base de l’expérience utilisateur.

3. Explorer une API inconnue

La difficulté n’est jamais fetch : c’est de savoir quelle propriété lire dans la réponse.

ImportantLa méthode, à appliquer systématiquement
  1. Ouvrez l’URL dans le navigateur et regardez la structure du JSON
  2. Ou affichez tout dans la console avant d’écrire quoi que ce soit :
const donnees = await reponse.json();
console.log(donnees);   // ← REGARDEZ ce qu'il y a dedans

Dans la console, cliquez sur les petites flèches ▸ pour déplier l’objet. Vous verrez exactement les noms des propriétés.

Ne devinez jamais un nom de propriété. Regardez.

Quand la donnée est imbriquée

{
  "current": {
    "temperature_2m": 18.3,
    "wind_speed_10m": 12
  }
}

On enchaîne les points :

console.log(donnees.current.temperature_2m); // 18.3

Et quand c’est un tableau, on combine avec les crochets :

{ "results": [ { "name": "Paris" }, { "name": "Lyon" } ] }
console.log(donnees.results[0].name); // "Paris"

Exercice 13.1 — Lire un JSON

Voici la réponse d’une API :

{
  "name": "pikachu",
  "height": 4,
  "types": [
    { "slot": 1, "type": { "name": "electric" } }
  ],
  "sprites": {
    "front_default": "https://.../25.png"
  }
}

Écrivez l’expression qui donne :

  1. le nom
  2. l’adresse de l’image
  3. le type ("electric")
a)  donnees.name
b)  donnees.sprites.front_default
c)  donnees.types[0].type.name

Pour c), on lit de gauche à droite : types est un tableau → [0] prend le premier élément → cet élément est un objet qui a une propriété type → qui est elle-même un objet avec name.

C’est le genre de chemin qu’on ne devine pas : on le lit dans la console.

4. Quand ça se passe mal

Internet est peu fiable : serveur en panne, wifi coupé, adresse fautive. Votre code doit tenir. Deux protections à mettre systématiquement.

1. Vérifier que le serveur est content

const reponse = await fetch(url);

if (!reponse.ok) {
  throw new Error(`Le serveur a répondu ${reponse.status}`);
}

reponse.ok vaut true si tout va bien, false sinon. reponse.status donne le code :

Code Sens
200 OK
404 Pas trouvé
429 Trop de demandes, ralentissez
500 Le serveur a planté
Avertissementfetch ne plante PAS sur une erreur 404

C’est contre-intuitif : si le serveur répond « 404 pas trouvé », fetch considère qu’il a réussi — il a bien obtenu une réponse !

Sans le test reponse.ok, votre code continue avec une page d’erreur au lieu des données attendues, et l’erreur apparaîtra beaucoup plus loin, incompréhensible.

Testez toujours reponse.ok.

2. Entourer d’un try / catch

Vous savez déjà faire (séance 12) :

async function chargerChien() {
  etat.textContent = "Chargement…";

  try {
    const reponse = await fetch("https://dog.ceo/api/breeds/image/random");

    if (!reponse.ok) {
      throw new Error(`Erreur ${reponse.status}`);
    }

    const donnees = await reponse.json();
    photo.src = donnees.message;
    etat.textContent = "";
  } catch (erreur) {
    etat.textContent = "Impossible de charger le chien. Réessayez.";
    console.log(erreur.message);
  }
}
AstuceLe squelette à réutiliser toute votre vie
async function charger() {
  etat.textContent = "Chargement…";        // 1. prévenir

  try {
    const reponse = await fetch(URL);
    if (!reponse.ok) throw new Error(reponse.status);  // 2. vérifier
    const donnees = await reponse.json();

    // 3. utiliser les données
    etat.textContent = "";
  } catch (erreur) {                        // 4. plan B
    etat.textContent = "Une erreur est survenue.";
    console.log(erreur);
  }
}

Copiez-le, adaptez-le. C’est la structure standard.

Vous testerez peut-être une API et verrez dans la console :

Access to fetch at '…' has been blocked by CORS policy

Ce n’est pas une erreur de votre code. Par sécurité, un navigateur interdit à une page de lire les données d’un autre site — sauf si ce site l’autorise explicitement. Cette autorisation s’appelle CORS.

Une API publique bien configurée autorise tout le monde. Une API privée, non. Si vous tombez sur ce message, changez d’API : vous ne pouvez rien y faire depuis le navigateur.

5. Lire un fichier local avec fetch

fetch ne sert pas qu’à Internet. Il sait aussi lire un fichier de votre propre dossier — et c’est extrêmement pratique.

Étape 2 — Des données dans un fichier

Créez fruits.json à côté de votre page :

fruits.json
[
  { "nom": "Pomme", "prix": 2.5, "bio": true },
  { "nom": "Banane", "prix": 1.8, "bio": false },
  { "nom": "Fraise", "prix": 6.9, "bio": true }
]
fruits.html
<h1>Les fruits</h1>
<p id="etat"></p>
<ul id="liste"></ul>
fruits.js
const etat = document.querySelector("#etat");
const liste = document.querySelector("#liste");

async function charger() {
  etat.textContent = "Chargement…";

  try {
    const reponse = await fetch("fruits.json");

    if (!reponse.ok) {
      throw new Error(`Erreur ${reponse.status}`);
    }

    const fruits = await reponse.json();

    liste.innerHTML = "";
    for (const fruit of fruits) {
      const li = document.createElement("li");
      li.textContent = `${fruit.nom} — ${fruit.prix} €`;
      liste.appendChild(li);
    }

    etat.textContent = "";
  } catch (erreur) {
    etat.textContent = "Impossible de lire les données.";
    console.log(erreur.message);
  }
}

charger();

C’est exactement le même code qu’avec une API. Seule l’adresse change.

AvertissementObligatoire : lancer la page avec Live Server

fetch sur un fichier local ne marche pas si vous ouvrez la page par un double-clic. Vous verriez dans la console :

CORS request not http

Pourquoi ? Une page ouverte en file:// n’a pas le droit de lire les autres fichiers de votre disque : ce serait une faille de sécurité énorme (n’importe quelle page téléchargée pourrait lire vos documents).

Solution : clic droit → Open with Live Server. L’adresse doit commencer par http://127.0.0.1:5500. C’est tout.

AstuceL’avantage : on peut provoquer les pannes

Avec une vraie API, difficile de tester le cas « le serveur répond 404 ». Avec un fichier local, c’est immédiat :

Manipulation Ce qui se passe
Renommer fruits.json en autre.json Le fichier n’existe plus → 404, donc reponse.ok est faux
Remplacer le contenu par n'importe quoi reponse.json() échoue → on tombe dans le catch
Remplacer le contenu par [] Aucune erreur, mais une liste vide à gérer

Faites ces trois tests maintenant. Un code qui ne marche que dans le cas idéal n’est pas un code fini.

Exercice 13.2 — Les trois pannes

Reprenez fruits.js et complétez-le pour qu’il affiche un message différent dans chacun des trois cas ci-dessus :

Cas Message
Fichier absent Fichier de données introuvable.
Fichier illisible Les données sont abîmées.
Tableau vide Aucun fruit pour le moment.
fruits.js
async function charger() {
  etat.textContent = "Chargement…";

  try {
    const reponse = await fetch("fruits.json");

    if (!reponse.ok) {
      etat.textContent = "Fichier de données introuvable.";
      return;
    }

    const fruits = await reponse.json(); // peut échouer si le fichier est abîmé

    if (fruits.length === 0) {
      etat.textContent = "Aucun fruit pour le moment.";
      return;
    }

    liste.innerHTML = "";
    for (const fruit of fruits) {
      const li = document.createElement("li");
      li.textContent = `${fruit.nom} — ${fruit.prix} €`;
      liste.appendChild(li);
    }

    etat.textContent = "";
  } catch (erreur) {
    etat.textContent = "Les données sont abîmées.";
    console.log(erreur.message);
  }
}

Le point clé : le cas « fichier absent » se traite avec un return dans le try, et non avec un throw. Sinon les deux messages se confondraient, car tout finirait dans le catch.

Trois issues possibles, trois messages distincts : c’est ce qu’on attend d’une application sérieuse.

6. Un exemple utile : la météo

L’API Open-Meteo est gratuite et ne demande aucune inscription.

Étape 3 — La météo à Palaiseau

meteo.html
<h1>Météo</h1>
<button id="rafraichir">Actualiser</button>
<p id="etat"></p>
<p id="resultat"></p>
meteo.js
const URL_METEO =
  "https://api.open-meteo.com/v1/forecast" +
  "?latitude=48.71&longitude=2.24" +
  "&current=temperature_2m,wind_speed_10m";

const etat = document.querySelector("#etat");
const resultat = document.querySelector("#resultat");

async function chargerMeteo() {
  etat.textContent = "Chargement…";

  try {
    const reponse = await fetch(URL_METEO);

    if (!reponse.ok) {
      throw new Error(`Erreur ${reponse.status}`);
    }

    const donnees = await reponse.json();
    console.log(donnees); // ← toujours regarder d'abord !

    const temperature = donnees.current.temperature_2m;
    const vent = donnees.current.wind_speed_10m;

    resultat.textContent = `Il fait ${temperature} °C, vent ${vent} km/h.`;
    etat.textContent = "";
  } catch (erreur) {
    etat.textContent = "Météo indisponible.";
    console.log(erreur.message);
  }
}

document.querySelector("#rafraichir").addEventListener("click", () => {
  chargerMeteo();
});

chargerMeteo();
NoteLire une adresse d’API
https://api.open-meteo.com/v1/forecast ? latitude=48.71 & longitude=2.24
└──────────────┬──────────────────────┘ │ └─────┬─────┘ │ └──────┬──────┘
          l'adresse de base             │   paramètre   │   paramètre
                                        │               │
                            « les paramètres commencent »
                                                 « et se séparent par & »

Changez les coordonnées et vous avez la météo ailleurs. Les paramètres, c’est la façon dont on pose une question précise à une API.

7. À toi de jouer

Exercice 13.3 — Trouver les 4 erreurs

function charger() {
  const reponse = fetch("https://dog.ceo/api/breeds/image/random");
  const donnees = reponse.json();
  photo.src = donnees.image;
}
  1. async manquant devant function charger(). Sans lui, impossible d’utiliser await.
  2. await manquant devant fetch(…) → reponse contient une promesse, pas une réponse.
  3. await manquant devant reponse.json() → même problème.
  4. donnees.image n’existe pas : cette API renvoie la propriété message. Il fallait regarder le JSON avant d’écrire le nom.
async function charger() {
  const reponse = await fetch("https://dog.ceo/api/breeds/image/random");
  const donnees = await reponse.json();
  photo.src = donnees.message;
}

Et il manque encore le if (!reponse.ok) et le try/catch pour que ce soit du code sérieux.

Exercice 13.4 — L’âge d’un prénom 🔮

L’API https://api.agify.io/?name=camille devine l’âge moyen des personnes portant un prénom. Elle répond :

{ "count": 12345, "name": "camille", "age": 42 }

Faites une page avec un formulaire (un champ prénom) qui affiche : D'après l'API, les Camille ont en moyenne 42 ans.

Gérez : champ vide, erreur réseau, et le cas où age vaut null (prénom inconnu).

age.html
<h1>L'âge d'un prénom</h1>

<form id="formulaire">
  <label for="prenom">Prénom :</label>
  <input type="text" id="prenom" placeholder="Camille" />
  <button type="submit">Deviner</button>
</form>

<p id="resultat"></p>
age.js
const formulaire = document.querySelector("#formulaire");
const prenom = document.querySelector("#prenom");
const resultat = document.querySelector("#resultat");

async function deviner(nom) {
  resultat.textContent = "Recherche…";

  try {
    const reponse = await fetch(`https://api.agify.io/?name=${nom}`);

    if (!reponse.ok) {
      throw new Error(`Erreur ${reponse.status}`);
    }

    const donnees = await reponse.json();

    if (donnees.age === null) {
      resultat.textContent = `Aucune donnée pour le prénom « ${nom} ».`;
      return;
    }

    resultat.textContent =
      `D'après l'API, les ${donnees.name} ont en moyenne ${donnees.age} ans ` +
      `(sur ${donnees.count} personnes).`;
  } catch (erreur) {
    resultat.textContent = "Impossible de contacter le service.";
    console.log(erreur.message);
  }
}

formulaire.addEventListener("submit", (event) => {
  event.preventDefault();

  const valeur = prenom.value.trim();

  if (valeur === "") {
    resultat.textContent = "Merci d'entrer un prénom.";
    return;
  }

  deviner(valeur);
});

Remarquez le gabarit dans l’URL : `https://api.agify.io/?name=${nom}`. C’est ainsi qu’on construit une requête à partir d’une saisie.

Et remarquez les trois cas d’échec traités : champ vide, prénom inconnu, réseau en panne. C’est ça, du code qui tient debout.

Exercice 13.5 — Projet : le Pokédex 🎮

C’est le projet de fin de cours. Il mobilise tout le semestre.

API : https://pokeapi.co/api/v2/pokemon/pikachu (remplacez pikachu par n’importe quel nom en minuscules)

Ce qu’il faut réaliser

  1. Un formulaire pour saisir un nom de Pokémon
  2. À la validation, afficher une carte avec :
    • le nom (avec une majuscule)
    • l’image (sprites.front_default)
    • la taille et le poids
    • le ou les types
  3. Un message Chargement… pendant l’attente
  4. Un message clair si le Pokémon n’existe pas (l’API répond 404)
  5. Un historique des recherches réussies, affiché en dessous
  6. L’historique sauvegardé dans localStorage (séance 12), restauré au rechargement
  7. Une classe Pokemon avec une méthode resume()
NoteIndices
  • Les types sont dans un tableau : donnees.types[0].type.name. Pour tous les avoir, faites une boucle.
  • Mettre une majuscule : nom[0].toUpperCase() + nom.slice(1)
  • L’API attend le nom en minuscules : nom.toLowerCase()
pokedex.html
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <title>Pokédex</title>
    <link rel="stylesheet" href="pokedex.css" />
  </head>
  <body>
    <h1>Pokédex</h1>

    <form id="formulaire">
      <label for="nom">Nom du Pokémon :</label>
      <input type="text" id="nom" placeholder="pikachu" />
      <button type="submit">Chercher</button>
    </form>

    <p id="etat"></p>
    <div id="carte" class="carte"></div>

    <h2>Historique</h2>
    <ul id="historique"></ul>
    <button id="vider">Vider l'historique</button>

    <script src="pokedex.js"></script>
  </body>
</html>
pokedex.css
body {
  font-family: system-ui, Arial, sans-serif;
  max-width: 700px;
  margin: 0 auto;
  padding: 24px;
}

.carte {
  border: 2px solid #2c6fa8;
  border-radius: 12px;
  padding: 16px;
  margin-top: 16px;
  text-align: center;
}

.carte img {
  width: 160px;
}

.erreur {
  color: #c0392b;
  font-weight: bold;
}
pokedex.js
class Pokemon {
  constructor(nom, image, taille, poids, types) {
    this.nom = nom;
    this.image = image;
    this.taille = taille;
    this.poids = poids;
    this.types = types;
  }

  resume() {
    return `${this.nom} — ${this.types.join(", ")}`;
  }
}

const CLE = "pokedex-historique";

const formulaire = document.querySelector("#formulaire");
const champNom = document.querySelector("#nom");
const etat = document.querySelector("#etat");
const carte = document.querySelector("#carte");
const historiqueEl = document.querySelector("#historique");
const vider = document.querySelector("#vider");

let historique = [];

function majuscule(mot) {
  return mot[0].toUpperCase() + mot.slice(1);
}

function sauvegarder() {
  localStorage.setItem(CLE, JSON.stringify(historique));
}

function charger() {
  const texte = localStorage.getItem(CLE);
  if (texte === null) {
    historique = [];
    return;
  }

  try {
    historique = JSON.parse(texte);
  } catch (erreur) {
    console.log("Historique corrompu :", erreur.message);
    historique = [];
    localStorage.removeItem(CLE);
  }
}

function afficherHistorique() {
  historiqueEl.innerHTML = "";

  for (const nom of historique) {
    const li = document.createElement("li");
    li.textContent = nom;
    historiqueEl.appendChild(li);
  }
}

function afficherCarte(pokemon) {
  carte.innerHTML = "";

  const titre = document.createElement("h2");
  titre.textContent = pokemon.nom;

  const img = document.createElement("img");
  img.src = pokemon.image;
  img.alt = `Illustration de ${pokemon.nom}`;

  const infos = document.createElement("p");
  infos.textContent =
    `Taille : ${pokemon.taille / 10} m — ` +
    `Poids : ${pokemon.poids / 10} kg — ` +
    `Type(s) : ${pokemon.types.join(", ")}`;

  carte.appendChild(titre);
  carte.appendChild(img);
  carte.appendChild(infos);
}

async function chercher(nomSaisi) {
  etat.textContent = "Chargement…";
  etat.className = "";
  carte.innerHTML = "";

  const nom = nomSaisi.toLowerCase();

  try {
    const reponse = await fetch(`https://pokeapi.co/api/v2/pokemon/${nom}`);

    if (reponse.status === 404) {
      throw new Error(`Aucun Pokémon nommé « ${nomSaisi} ».`);
    }
    if (!reponse.ok) {
      throw new Error(`Erreur ${reponse.status}, réessayez plus tard.`);
    }

    const donnees = await reponse.json();

    const types = [];
    for (const t of donnees.types) {
      types.push(t.type.name);
    }

    const pokemon = new Pokemon(
      majuscule(donnees.name),
      donnees.sprites.front_default,
      donnees.height,
      donnees.weight,
      types
    );

    afficherCarte(pokemon);
    etat.textContent = "";

    historique.push(pokemon.resume());
    sauvegarder();
    afficherHistorique();
  } catch (erreur) {
    etat.textContent = erreur.message;
    etat.className = "erreur";
  }
}

formulaire.addEventListener("submit", (event) => {
  event.preventDefault();

  const valeur = champNom.value.trim();

  if (valeur === "") {
    etat.textContent = "Merci d'entrer un nom.";
    etat.className = "erreur";
    return;
  }

  chercher(valeur);
});

vider.addEventListener("click", () => {
  historique = [];
  localStorage.removeItem(CLE);
  afficherHistorique();
});

charger();
afficherHistorique();

Ce que ce projet mobilise, séance par séance :

Séance Ce qui sert
1–2 La structure HTML
3 Le CSS, la classe .carte
4 Variables, gabarits, conversions
5 querySelector, textContent
6 Les conditions (404, champ vide)
8 Le tableau historique, les boucles
9 Les fonctions, chacune avec un rôle unique
10 Le formulaire, preventDefault, createElement
11 La classe Pokemon
12 JSON, localStorage, try/catch
13 fetch, async/await, reponse.ok

Si vous comprenez ce fichier en entier, vous avez réussi le cours.

8. Fiche mémo

Le squelette complet

async function charger() {
  etat.textContent = "Chargement…";

  try {
    const reponse = await fetch(URL);

    if (!reponse.ok) {
      throw new Error(`Erreur ${reponse.status}`);
    }

    const donnees = await reponse.json();
    console.log(donnees);   // toujours regarder d'abord

    // … utiliser les données …
    etat.textContent = "";
  } catch (erreur) {
    etat.textContent = "Une erreur est survenue.";
    console.log(erreur.message);
  }
}

Les règles

  • await devant fetch ET devant .json()
  • async devant la fonction qui contient un await
  • fetch ne plante pas sur un 404 → tester reponse.ok

Lire un fichier de son propre dossier

const reponse = await fetch("donnees.json");

⚠️ Il faut Live Server : en file://, le navigateur refuse (CORS request not http).

Les trois pannes à tester : fichier absent (404), fichier abîmé (catch), tableau vide.

Lire une donnée imbriquée

donnees.current.temperature_2m
donnees.types[0].type.name

Le symptôme

Promise { <pending> } dans la console → un await a été oublié

Des API gratuites pour jouer

API Adresse
Chien au hasard https://dog.ceo/api/breeds/image/random
Pokémon https://pokeapi.co/api/v2/pokemon/pikachu
Âge d’un prénom https://api.agify.io/?name=camille
Météo https://api.open-meteo.com/v1/forecast?latitude=48.71&longitude=2.24&current=temperature_2m
Communes françaises https://geo.api.gouv.fr/communes?codePostal=91120

9. Avant l’évaluation finale

La séance 14 est l’évaluation finale, sur l’ensemble du cours.

ImportantL’évaluation se fait sans Internet

Les API en ligne ne seront donc pas accessibles. Les données viendront de fichiers JSON fournis dans votre dossier, lus avec fetch — exactement comme à la section 5 de cette séance.

Refaites l’exercice 13.2 et ses trois pannes : c’est la partie la plus rentable de vos révisions.

AstuceComment réviser

Refaites le Pokédex de zéro, sans la solution, en vous aidant seulement des fiches mémo. C’est long, mais c’est la meilleure révision possible : il contient tout le cours.