IFT3225

Modules import / export

Les modules permettent de découper le code en fichiers réutilisables. Chaque fichier est un module isolé — rien n’est partagé tant qu’on ne l’exporte pas explicitement. C’est ce qui permet de construire une application à partir de petits morceaux indépendants plutôt que d’un seul fichier géant.

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 sait parler au réseau, format.js sait présenter des données, index.js les coordonne. Si l’API change, seul api.js bouge. Cette séparation est le même principe qu’on retrouvera côté serveur avec Express — découper par responsabilité.

Exporter

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 — 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 };

Export par défaut

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

Importer

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";

Importer un paquet npm

Pour un paquet installé via npm, on importe par son nom — sans chemin ni extension :

import express from "express";

npm résout automatiquement le chemin vers node_modules/express. La règle : un chemin (./, ../) pour vos fichiers, un nom nu pour les paquets npm.

Mettre les pièces ensemble

Voici comment index.js orchestre les trois modules de notre arborescence :

// index.js
import creerClient from "./api.js";
import { formaterListe } from "./format.js";

const client = creerClient("https://planifium-api.onrender.com/api/v1");
const cours = await client.cours();

console.log(formaterListe(cours));

Chaque module ignore les détails des autres : index.js ne sait pas comment api.js parle au réseau, ni comment format.js met en forme. Il connaît seulement leurs interfaces. C’est le découplage en action.


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

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

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

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 faciles à oublier : le corps doit être sérialisé avec JSON.stringify (on n’envoie pas un objet JavaScript brut, mais du texte), et l’en-tête Content-Type: application/json prévient le serveur de la nature de ce texte.


async / await

Le problème

Les opérations réseau, lecture de fichiers, et requêtes à une base de données prennent du temps. JavaScript est mono-thread : s’il bloquait en attendant chaque réponse, l’application entière serait figée. Tout I/O en JavaScript est donc non-bloquant : on lance l’opération, et on fournit du code à exécuter quand le résultat arrive.

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)

Au début, on passait une fonction à exécuter une fois l’opération terminée — 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);
});

Ça marche pour une opération. Mais dès qu’on enchaîne des opérations dépendantes — lire un fichier, puis appeler une API avec son contenu, puis sauvegarder le résultat — les callbacks s’imbriquent les uns dans les autres :

lireFichier("config.json", (err, config) => {
  fetch(config.url, (err, reponse) => {
    parserReponse(reponse, (err, data) => {
      sauvegarder(data, (err, resultat) => {
        console.log("Enfin terminé!");
        // ... et ça continue à droite, indéfiniment
      });
    });
  });
});

Étape 2 — Les Promises

Une Promise est un objet qui représente une valeur qui n’existe pas encore mais qui arrivera éventuellement. Au lieu d’imbriquer, on chaîne 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

async/await est du sucre syntaxique par-dessus les Promises : la même mécanique, mais écrite comme du code séquentiel ordinaire. C’est la façon moderne, et celle qu’on utilise 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 qui produisait une pyramide de callbacks devient une simple suite de lignes. Chaque await attend que la Promise se résolve avant de passer à la ligne suivante — sans bloquer le reste du programme.

Ce que await fait vraiment

Sans await, on récupère la Promise elle-même — la « boîte », pas son contenu :

const data = fetch("https://planifium-api.onrender.com/api/v1/courses");
console.log(data);  // Promise { <pending> }  ← pas le résultat !

Avec await, on attend que la boîte s’ouvre et on prend son contenu :

async function chargerCours() {
  const reponse = await fetch("https://planifium-api.onrender.com/api/v1/courses");
  const data = await reponse.json();
  console.log(data);  // les données sont là
  return data;
}

Les règles à retenir

// top-level await dans un module
const reponse = await fetch("https://planifium-api.onrender.com/api/v1/courses");
const data = await reponse.json();

Gestion d’erreurs avec try / catch

Là où les Promises utilisent .catch(), async/await utilise 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

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

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

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.