JavaScript : modules, fetch et asynchrone
Modules import / export
Section intitulée « Modules import / export »Les modules permettent de découper le code en fichiers réutilisables. Chaque fichier est un module isolé, c’est-à-dire, aucune variable ou fonction n’est partagé si on ne l’exporte pas explicitement. C’est ce qui permet de construire une application à partir de petits morceaux plus ou moins indépendants plutôt que d’un seul fichier géant.
Pourquoi découper en modules
Section intitulée « Pourquoi découper en modules »Imaginons une petite application qui interroge une API de cours universitaires. Sans modules, tout vit dans un fichier. Avec les modules, on sépare les responsabilités :
mon-app/├── index.js ← point d'entrée : orchestre le tout├── api.js ← fonctions qui parlent à l'API├── format.js ← fonctions d'affichage et de mise en forme└── package.json ← "type": "module"Chaque fichier a un rôle clair. api.js regroupe ce qui concerne l’API, format.js
sait présenter des données et index.js les coordonne. Si les détails de communication
avec l’API changent, les modifications peuvent généralement être concentrées dans api.js.
Cette séparation repose sur le même principe qu’on retrouvera côté serveur avec Express : découper par responsabilité.
Exporter
Section intitulée « Exporter »Un export nommé expose une valeur sous son nom. On peut en avoir plusieurs par fichier :
// Export nommé — on peut en avoir plusieurs par fichierexport const SEPARATEUR = " · ";
export function formaterCours(cours) { return `${cours.code}${SEPARATEUR}${cours.name}`;}
export function formaterListe(cours) { return cours.map(formaterCours).join("\n");}On peut aussi regrouper les exports à la fin du fichier, ce qui peut être utile pour voir d’un coup d’œil ce que le module expose :
const SEPARATEUR = " · ";function formaterCours(cours) { return `${cours.code}${SEPARATEUR}${cours.name}`; }function formaterListe(cours) { return cours.map(formaterCours).join("\n"); }
export { SEPARATEUR, formaterCours, formaterListe };Export par défaut
Section intitulée « Export par défaut »Un fichier peut désigner une valeur principale avec export default. Un seul par fichier :
export default function creerClient(baseUrl) { return { async cours() { const reponse = await fetch(`${baseUrl}/courses`); return reponse.json(); } };}Importer
Section intitulée « Importer »L’import doit refléter la façon dont la valeur a été exportée :
// Import nommé : les noms doivent correspondre exactementimport { formaterCours, formaterListe } from "./format.js";
// Import par défaut : on choisit librement le nomimport creerClient from "./api.js";
// Combiner défaut et nommés depuis un même moduleimport creerClient, { SEPARATEUR } from "./module.js";
// Renommer un import nommé avec `as`import { formaterCours as fmt } from "./format.js";Importer un paquet npm
Section intitulée « Importer un paquet npm »Pour un paquet installé via npm, on importe par son nom, sans chemin ni extension :
import express from "express";Node résout le paquet express installé dans le projet, notamment à partir de node_modules. La règle : un chemin (./, ../) pour vos fichiers, un nom nu pour les paquets npm.
Mettre les pièces ensemble
Section intitulée « Mettre les pièces ensemble »Voici comment index.js peut orchestrer les modules de notre arborescence sans connaître leurs détails internes :
import creerUrlCours from "./api.js";import { formaterCours } from "./format.js";
const url = creerUrlCours( "https://planifium-api.onrender.com/api/v1", "IFT3225");
const cours = { code: "IFT3225", name: "Technologies de l'Internet" };
console.log(url);console.log(formaterCours(cours));Chaque module ignore les détails des autres. index.js ne sait pas comment api.js construit une URL ni comment format.js met en forme les données. Il connaît seulement ce que ces modules exposent. C’est le découplage en action.
Plus loin, nous ferons évoluer api.js pour qu’il effectue réellement une requête réseau.
async / await
Section intitulée « async / await »Le problème
Section intitulée « Le problème »Certaines opérations prennent du temps : accès réseau, lecture de fichiers ou requêtes à une base de données. Dans un contexte JavaScript donné, le code s’exécute principalement sur un seul fil d’exécution. Si ce fil attendait passivement chaque opération lente, l’application pourrait devenir inutilisable.
Les API JavaScript exposent donc généralement les opérations d’entrée-sortie sous une forme asynchrone : l’opération est lancée, puis le programme peut poursuivre d’autres tâches pendant l’attente.La question devient : comment exprimer « quand le résultat arrive, fais ceci »? JavaScript a connu trois réponses successives.
Les voir dans l’ordre aide à comprendre pourquoi async/await existe.
Étape 1 — Les callbacks (l’ancienne façon)
Section intitulée « Étape 1 — Les callbacks (l’ancienne façon) »Historiquement, de nombreuses API demandaient de fournir une fonction à exécuter une fois l’opération terminée. Cette fonction est un callback.
// Style callback (ancien) — lire un fichier puis le traiterlireFichier("cours.json", (erreur, contenu) => { if (erreur) { console.error("Échec:", erreur); return; } console.log(contenu);});Cela reste lisible pour une seule opération. Mais dès qu’on enchaîne plusieurs opérations dépendantes, par exemple lire une configuration, appeler une API, analyser la réponse puis sauvegarder le résultat, les callbacks peuvent s’imbriquer :
lireFichier("config.json", (err, config) => { appelerApi(config.url, (err, reponse) => { parserReponse(reponse, (err, data) => { sauvegarder(data, (err, resultat) => { console.log("Enfin terminé!"); }); }); });});Étape 2 — Les Promises
Section intitulée « Étape 2 — Les Promises »Une Promise est un objet représentant le résultat futur d’une opération asynchrone. Elle est d’abord en attente (pending), puis se termine soit avec une valeur (fulfilled), soit avec une erreur (rejected).
Supposons maintenant que les mêmes opérations retournent des Promises. On peut les enchaîner avec .then() :
lireFichier("config.json") .then((config) => fetch(config.url)) .then((reponse) => reponse.json()) .then((data) => sauvegarder(data)) .then((resultat) => console.log("Terminé!")) .catch((err) => console.error("Échec:", err));Le code se lit verticalement plutôt que de dériver vers la droite, et un seul .catch() attrape toutes les erreurs de la chaîne. C’est déjà beaucoup mieux.
Étape 3 — async / await
Section intitulée « Étape 3 — async / await »async/await fournit une syntaxe plus directe au-dessus des Promises. La mécanique reste la même, mais le code ressemble davantage à une suite d’instructions ordinaires. C’est la forme que nous utiliserons principalement dans ce cours.
async function preparer() { const config = await lireFichier("config.json"); const reponse = await fetch(config.url); const data = await reponse.json(); const resultat = await sauvegarder(data); console.log("Terminé!");}Le même enchaînement devient une simple suite de lignes. Chaque await suspend l’exécution de cette fonction jusqu’à ce que la Promise se termine, sans bloquer tout le programme.
Ce que await fait vraiment
Section intitulée « Ce que await fait vraiment »Sans await, on récupère la Promise elle-même (la « boîte »), mais pas son contenu :
const resultat = operationAsynchrone();console.log(resultat); // Promise { <pending> }Avec await, on attend la résolution de la Promise et l’expression produit sa valeur :
async function charger() { const resultat = await operationAsynchrone(); console.log(resultat); return resultat;}Les règles à retenir
Section intitulée « Les règles à retenir »awaitne s’utilise que dans une fonction marquéeasync…- …sauf au niveau supérieur d’un module (
"type": "module"), où le top-level await est permis :
const resultat = await operationAsynchrone();- Une fonction
asyncretourne toujours une Promise, même si elle semble retourner une valeur simple. Son résultat doit donc généralement être utilisé avecawaitou.then().
Séquentiel ou concurrent ?
Section intitulée « Séquentiel ou concurrent ? »Deux await successifs signifient que la deuxième opération commence seulement lorsque la première est terminée :
const cours = await chargerCours();const programmes = await chargerProgrammes();C’est nécessaire lorsque la deuxième opération dépend de la première. Si les deux opérations sont indépendantes, on peut les lancer ensemble avec Promise.all :
const [cours, programmes] = await Promise.all([ chargerCours(), chargerProgrammes(),]);await améliore la lisibilité, mais il ne faut pas rendre séquentielles des opérations indépendantes qui peuvent avancer en même temps.
Gestion d’erreurs avec try / catch
Section intitulée « Gestion d’erreurs avec try / catch »Là où les Promises utilisent .catch(), avec async/await, on peut utiliser le try/catch ordinaire du langage — un autre avantage de lisibilité :
async function chargerCours() { try { const reponse = await fetch("https://planifium-api.onrender.com/api/v1/courses");
if (!reponse.ok) { throw new Error(`HTTP ${reponse.status}`); }
const data = await reponse.json(); return data; } catch (erreur) { console.error("Impossible de charger les cours:", erreur.message); return []; }}Exemple complet
Section intitulée « Exemple complet »async function afficherCours(sigle) { try { const url = `https://planifium-api.onrender.com/api/v1/courses/${sigle}`; const reponse = await fetch(url);
if (!reponse.ok) { throw new Error(`Cours non trouvé (${reponse.status})`); }
const cours = await reponse.json(); console.log(`${cours.code} — ${cours.name}`); } catch (erreur) { console.error(erreur.message); }}
// Appel (top-level await dans un module)await afficherCours("IFT3225");fetch — requêtes HTTP
Section intitulée « fetch — requêtes HTTP »fetch est l’API standard pour effectuer des requêtes HTTP en JavaScript. Elle est disponible nativement dans les navigateurs et dans Node.js (depuis la version 18). On l’utilisera pour interroger des API réelles.
Requête GET
Section intitulée « Requête GET »const reponse = await fetch("https://planifium-api.onrender.com/api/v1/courses");const data = await reponse.json();console.log(data);Deux étapes distinctes : fetch retourne un objet Response (les en-têtes, le statut), puis .json() lit le corps et le parse en objet JavaScript. Le .json() est lui-même asynchrone — d’où le second await.
Vérifier le statut
Section intitulée « Vérifier le statut »C’est le piège le plus courant avec fetch : il ne lance pas d’erreur pour les réponses 4xx ou 5xx — seulement pour les vrais problèmes réseau (serveur injoignable, DNS, coupure). Une réponse 404 est, du point de vue de fetch, une requête réussie. Il faut donc vérifier response.ok soi-même :
const reponse = await fetch(url);
if (!reponse.ok) { throw new Error(`Erreur HTTP : ${reponse.status}`);}
const data = await reponse.json();Requête POST
Section intitulée « Requête POST »Pour envoyer des données, on passe un second argument décrivant la méthode, les en-têtes et le corps :
const reponse = await fetch("https://planifium-api.onrender.com/api/v1/courses", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sigle: "IFT3225", titre: "Technologies de l'internet", session: "E26" })});
const resultat = await reponse.json();Deux points sont faciles à oublier : le corps doit être sérialisé avec JSON.stringify, car on n’envoie pas directement un objet JavaScript, et l’en-tête Content-Type: application/json indique au serveur comment interpréter ce corps.
### Exemple complet
On peut maintenant réunir modules, fetch et async/await. Faisons évoluer api.js pour qu’il charge réellement un cours :
export default function creerClient(baseUrl) { return { async cours(sigle) { const reponse = await fetch(`${baseUrl}/courses/${sigle}`);
if (!reponse.ok) { throw new Error(`Cours non trouvé (${reponse.status})`); }
return reponse.json(); }, };}Puis index.js orchestre le tout :
import creerClient from "./api.js";import { formaterCours } from "./format.js";
const client = creerClient( "https://planifium-api.onrender.com/api/v1");
try { const cours = await client.cours("IFT3225"); console.log(formaterCours(cours));} catch (erreur) { console.error(erreur.message);}On retrouve ici les trois idées de la page : chaque fichier expose une responsabilité claire, fetch communique avec le serveur et await permet d’exprimer cette attente sans transformer le programme en suite de callbacks imbriqués.
Aller plus loin : Axios
Section intitulée « Aller plus loin : Axios »fetch est natif et suffit dans la majorité des cas. Mais beaucoup de projets utilisent Axios, une bibliothèque externe qui simplifie quelques aspects récurrents.
On l’installe puis on l’importe comme tout paquet npm :
npm install axiosimport axios from "axios";
const reponse = await axios.get("https://planifium-api.onrender.com/courses");console.log(reponse.data); // déjà parsé — pas de .json()Trois différences qui expliquent sa popularité :
- Pas d’étape
.json()— Axios parse automatiquement la réponse JSON et la place dansreponse.data. Une étape de moins qu’avecfetch. - Erreur automatique sur 4xx/5xx — contrairement à
fetch, Axios rejette la promesse sur un statut d’erreur. Lecatchse déclenche sans qu’on ait à testerreponse.ok. - Configuration concise — en-têtes, corps et options tiennent dans un objet, et le corps n’a pas besoin d’être sérialisé à la main.
Comparons un POST, côté fetch et côté Axios :
// fetch — natif, verbeuxconst r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sigle: "IFT3225" })});if (!r.ok) throw new Error(`HTTP ${r.status}`);const data = await r.json();// axios — concisconst r = await axios.post(url, { sigle: "IFT3225" });const data = r.data; // parsé ; rejette tout seul sur erreurfetch ne demande aucune dépendance ; Axios demande une installation mais réduit le code répétitif.
Pour ce cours,
fetchest amplement suffisant.