Aller au contenu

JavaScript : modules, fetch et asynchrone

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.

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é.

Un export nommé expose une valeur sous son nom. On peut en avoir plusieurs par fichier :

format.js
// Export nommé — on peut en avoir plusieurs par fichier
export 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 :

format.js
const SEPARATEUR = " · ";
function formaterCours(cours) { return `${cours.code}${SEPARATEUR}${cours.name}`; }
function formaterListe(cours) { return cours.map(formaterCours).join("\n"); }
export { SEPARATEUR, formaterCours, formaterListe };

Un fichier peut désigner une valeur principale avec export default. Un seul par fichier :

api.js
export default function creerClient(baseUrl) {
return {
async cours() {
const reponse = await fetch(`${baseUrl}/courses`);
return reponse.json();
}
};
}

L’import doit refléter la façon dont la valeur a été exportée :

index.js
// Import nommé : les noms doivent correspondre exactement
import { formaterCours, formaterListe } from "./format.js";
// Import par défaut : on choisit librement le nom
import creerClient from "./api.js";
// Combiner défaut et nommés depuis un même module
import creerClient, { SEPARATEUR } from "./module.js";
// Renommer un import nommé avec `as`
import { formaterCours as fmt } from "./format.js";

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.

Voici comment index.js peut orchestrer les modules de notre arborescence sans connaître leurs détails internes :

index.js
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.


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.

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 traiter
lireFichier("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é!");
});
});
});
});

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.

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.

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;
}
  • await ne s’utilise que dans une fonction marquée async…
  • …sauf au niveau supérieur d’un module ("type": "module"), où le top-level await est permis :
const resultat = await operationAsynchrone();
  • Une fonction async retourne toujours une Promise, même si elle semble retourner une valeur simple. Son résultat doit donc généralement être utilisé avec await ou .then().

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.

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 [];
}
}
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 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.

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.

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();

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 :

api.js
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 :

index.js
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.


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 :

Fenêtre de terminal
npm install axios
import 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 dans reponse.data. Une étape de moins qu’avec fetch.
  • Erreur automatique sur 4xx/5xx — contrairement à fetch, Axios rejette la promesse sur un statut d’erreur. Le catch se déclenche sans qu’on ait à tester reponse.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, verbeux
const 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 — concis
const r = await axios.post(url, { sigle: "IFT3225" });
const data = r.data; // parsé ; rejette tout seul sur erreur
fetch ne demande aucune dépendance ; Axios demande une installation mais réduit le code répétitif.

Pour ce cours, fetch est amplement suffisant.