Retour au cours

backend / nodejs

Modules CommonJS et ESM

Leçon 21 exercice

Explication

Ce que vous allez apprendre

  • Distinguer CommonJS (require/module.exports) et ESM (import/export)
  • Comprendre pourquoi ESM permet le top-level await et pas CommonJS
  • Configurer le type de module d'un projet via "type": "module" dans package.json
  • Importer un module ESM depuis du CommonJS avec un import() dynamique
  • Reconstruire __dirname/__filename en ESM avec import.meta.url

Dans quel contexte ?

Une équipe migre un vieux projet Node écrit entièrement en CommonJS (require) vers un starter moderne qui n'exporte que des modules ESM. Le build échoue avec une erreur cryptique dès qu'un fichier CommonJS tente de require() un de ces nouveaux modules. Cette leçon explique pourquoi cette interopérabilité n'est pas automatique dans les deux sens, et comment migrer sans tout casser d'un coup.

Une histoire d'héritage à connaître d'abord

Quand Node.js est né, JavaScript n'avait pas encore de système de modules officiel. CommonJS (require/module.exports) a été inventé à ce moment-là pour combler ce vide.

Des années plus tard, le langage lui-même a adopté un standard officiel : l'ESM (import/export). Résultat aujourd'hui : un développeur Node croise encore les deux systèmes, et doit comprendre leurs différences pour éviter des erreurs déroutantes.

Ce n'est pas qu'une question de syntaxe différente — voyons pourquoi. CommonJS charge les modules de façon SYNCHRONE : le require bloque littéralement jusqu'à ce que le module soit entièrement chargé.

ESM, à l'inverse, est conçu pour être asynchrone dès le départ. C'est ce qui permet le "top-level await" — utiliser await directement en dehors d'une fonction async — quelque chose d'impossible en CommonJS classique.

CaractéristiqueCommonJSESM
Mots-clésrequire, module.exportsimport, export
ChargementSynchroneAsynchrone
Top-level awaitNonOui
Extension / config.cjs (ou défaut).mjs ou "type": "module"

Prérequis

Cette leçon suppose que tu es à l'aise avec l'event loop vu à la leçon précédente : le chargement synchrone de CommonJS et asynchrone d'ESM s'appuie directement sur cette distinction.

Comment Node sait-il alors quel système utiliser pour un fichier donné ? Il regarde l'extension (.cjs = CommonJS, .mjs = ESM) ou, plus couramment, le champ "type" du package.json : "type": "module" fait que tous les .js du projet sont interprétés comme de l'ESM.

Une fois cette base posée, un piège fréquent guette dès qu'on mélange les deux systèmes. Importer un module CommonJS depuis de l'ESM fonctionne généralement sans problème.

Mais l'inverse est plus délicat. Utiliser require() pour charger un module ESM pur NE fonctionne PAS de façon synchrone — il faut alors un import() dynamique, qui renvoie une Promise, une source fréquente de confusion en migrant un vieux projet.

Piège fréquent

const mod = require("./pure-esm-module.mjs") échoue avec une ERR_REQUIRE_ESM si ce module est écrit en ESM pur. La seule solution correcte est un import() dynamique dans une fonction async, jamais un require() classique.

Un dernier détail surprend souvent les développeurs habitués à CommonJS. __dirname et __filename, disponibles nativement en CommonJS, n'existent PAS en ESM : il faut les reconstruire manuellement via import.meta.url.

Et la suite ? Cette leçon complète directement la précédente : comprendre comment le code s'exécute (l'event loop) va de pair avec comprendre comment il est organisé et chargé en mémoire.

Commandes & code

Modules CommonJS et ESM

js
// math.cjs — module CommonJS (require/module.exports)
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { add, multiply };
// ou : module.exports.add = add; export nommé individuel
js
// app.cjs — consommation CommonJS
const { add, multiply } = require("./math.cjs");
const path = require("path"); // modules natifs Node aussi en CommonJS

console.log(add(2, 3));
console.log(path.join(__dirname, "data"));
js
// math.mjs — module ES (ESM), le standard moderne
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

export default function subtract(a, b) {
  return a - b;
}
js
// app.mjs — consommation ESM
import subtract, { add, multiply } from "./math.mjs";
import { readFile } from "node:fs/promises"; // préfixe "node:" recommandé pour les modules natifs

console.log(add(2, 3), subtract(5, 2));

const content = await readFile("./data.txt", "utf-8"); // top-level await disponible en ESM
console.log(content);
json
// package.json — déclarer le type de module par défaut du projet
{
  "name": "mon-projet",
  "type": "module",
  "main": "./src/index.js",
  "exports": {
    ".": "./src/index.js",
    "./utils": "./src/utils.js"
  }
}
js
// Interopérabilité : importer un module CommonJS depuis de l'ESM (souvent OK)
import express from "express"; // fonctionne même si express est en CommonJS

// L'inverse (require un module ESM pur) NE fonctionne PAS directement en CommonJS synchrone :
// il faut un import() dynamique, qui retourne une Promise
async function loadEsmModule() {
  const mod = await import("./pure-esm-module.mjs");
  return mod.default;
}
CommonJSESM
Importrequire()import
Exportmodule.exportsexport / export default
ChargementSynchroneAsynchrone (top-level await possible)
__dirnameDisponibleNon disponible (utiliser import.meta.url)
js
// Équivalent de __dirname en ESM
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Résumé

  • "type": "module" dans package.json fait de .js de l'ESM par défaut (sinon CommonJS).
  • ESM supporte le top-level await, contrairement à CommonJS.
  • __dirname/__filename n'existent pas nativement en ESM : reconstruits via import.meta.url.
  • Préférer le préfixe node: pour les imports de modules natifs (node:fs, node:path).

Exercices pratiques

1 disponible
1

Mission : réparer une migration CommonJS vers ESM cassée

Objectif : Diagnostiquer une erreur ERR_REQUIRE_ESM et migrer un module de configuration CommonJS vers un module ESM fonctionnel, sans __dirname natif.

Contexte

Une équipe migre progressivement son projet vers l'ESM. Un fichier config.mjs pur ESM a été créé, mais un vieux fichier server.cjs encore en CommonJS tente de le charger avec const config = require("./config.mjs") et le build échoue immédiatement au démarrage avec ERR_REQUIRE_ESM. Le fichier config.mjs a aussi besoin de connaître le chemin absolu de son propre dossier pour résoudre un fichier data.json voisin.

Résoudre l’exercice →