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
fetchetawait - 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.
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) |
async 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.
awaitveut dire : « mets cette ligne en pause jusqu’à ce que la réponse arrive, puis continue ».asyncdoit être écrit devant la fonction qui contient unawait. 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();
}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 bizarreSi 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. 🎉
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.
- Ouvrez l’URL dans le navigateur et regardez la structure du JSON
- 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 dedansDans 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.3Et 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 :
- le nom
- l’adresse de l’image
- le type (
"electric")
a) donnees.name
b) donnees.sprites.front_default
c) donnees.types[0].type.namePour 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é |
fetch 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);
}
}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.
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.
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" +
"¤t=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();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;
}asyncmanquant devantfunction charger(). Sans lui, impossible d’utiliserawait.awaitmanquant devantfetch(…)→reponsecontient une promesse, pas une réponse.awaitmanquant devantreponse.json()→ même problème.donnees.imagen’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
- Un formulaire pour saisir un nom de Pokémon
- À 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
- Un message
Chargement…pendant l’attente - Un message clair si le Pokémon n’existe pas (l’API répond 404)
- Un historique des recherches réussies, affiché en dessous
- L’historique sauvegardé dans
localStorage(séance 12), restauré au rechargement - Une classe
Pokemonavec une méthoderesume()
- 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
awaitdevantfetchET devant.json()asyncdevant la fonction qui contient unawaitfetchne plante pas sur un 404 → testerreponse.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.nameLe 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¤t=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.
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.
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.