Deux systèmes de modules pour un seul langage
À l'origine, JavaScript n'avait aucun système de modules. Node a comblé ce manque en 2009 avec CommonJS (require, module.exports), et pendant des années le code Node a ressemblé à ça. Puis en 2015, le langage s'est doté de son propre système standard — les ES Modules (import, export) — désormais pris en charge aussi bien par les navigateurs que par Node.
Les deux cohabitent encore aujourd'hui. Voici le même petit module écrit dans chacun des deux styles :
Même fonction, deux emballages différents. Le reste de cette page explique quand chaque emballage compte vraiment, et lequel dégainer selon la situation.
Les différences de syntaxe entre require et import
Au quotidien, les différences entre CommonJS et ES Modules tiennent sur une carte postale :
// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");
module.exports = something;
module.exports.name = value;
exports.name = value;
// Modules ES
import fs from "fs";
import { readFile } from "fs/promises";
export default something;
export const name = value;
export { name };
require est un simple appel de fonction. import, c'est une instruction — elle ne peut apparaître qu'au niveau racine d'un module, et le chemin doit obligatoirement être une chaîne littérale. Cette contrainte n'est pas là par hasard : c'est justement ce qui permet aux ES Modules de faire des choses impossibles en CommonJS.
La vraie différence entre require et import : statique vs dynamique
En CommonJS, require() est évalué au moment où la ligne s'exécute. Tu peux le glisser dans un if, calculer le chemin à la volée, charger un module sous condition :
Les ES Modules sont statiques. Le moteur analyse toutes les instructions import avant d'exécuter la moindre ligne de code : il construit un graphe de dépendances et résout tout en amont. C'est pour ça que le chemin doit être une chaîne littérale et que import ne peut apparaître qu'au niveau racine du fichier.
L'avantage est énorme : les outils peuvent voir l'ensemble du graphe de modules sans rien exécuter. C'est ce qui permet aux bundlers de faire du tree-shaking (éliminer les exports inutilisés), aux éditeurs de te proposer une autocomplétion fiable, et aux navigateurs de charger les modules en parallèle.
Quand tu as vraiment besoin d'un chargement dynamique en ESM, utilise import() — une expression qui ressemble à une fonction et qui renvoie une Promise :
Comment Node détermine quel système utilise un fichier
Un même projet Node peut contenir les deux types de fichiers. Pour savoir quel système s'applique à chaque fichier, Node se base sur deux critères :
- L'extension du fichier :
.mjscorrespond toujours aux ES Modules,.cjstoujours à CommonJS. - Le champ
"type"dupackage.jsonle plus proche :"module"indique que les fichiers.jssont traités comme des ES Modules, alors que"commonjs"(la valeur par défaut si le champ est absent) les traite comme du CommonJS.
// package.json
{
"name": "my-app",
"type": "module"
}
Avec "type": "module", un simple hello.js dans le même package utilise import/export. Glissez un hello.cjs à côté, et ce fichier-là utilise require. C'est comme ça qu'un projet peut migrer petit à petit, ou qu'une librairie peut livrer les deux saveurs en parallèle.
Piège pour débutants : dans un fichier ESM, require et module.exports n'existent tout simplement pas. Si vous les tapez par réflexe, vous allez droit au ReferenceError.
Interop : faire cohabiter les deux
Il arrive très souvent qu'un fichier ESM doive consommer un package CommonJS, ou l'inverse. Et les règles ne sont pas symétriques.
Un ESM qui importe du CommonJS, ça marche directement. L'objet module.exports du module CJS devient l'export par défaut :
// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));
Les imports nommés depuis un module CommonJS fonctionnent parfois — Node tente de détecter statiquement les exports nommés — mais pour plus de fiabilité, récupérez l'export par défaut puis déstructurez :
import pkg from "./utils.cjs";
const { parse, stringify } = pkg;
Importer un module ESM depuis du CommonJS, c'est là que ça se corse. Impossible de faire require() sur un module ES — vous aurez droit à l'erreur ERR_REQUIRE_ESM. La seule porte de sortie, c'est l'import() dynamique, qui fonctionne aussi en CJS et renvoie une Promise :
Node récent (22+) a ajouté un require() synchrone pour l'ESM sous certaines conditions, mais l'import() dynamique reste la réponse portable.
Autres différences de comportement bonnes à connaître
Au-delà de la syntaxe, les deux systèmes divergent sur quelques détails qui finissent parfois par poser problème :
Quelques autres différences :
thisau niveau racine. En CJS,thiscorrespond àmodule.exports. En ESM, il vautundefined. Les modules ESM sont toujours en mode strict.__dirnameet__filename. CJS vous les fournit gratuitement. En ESM, il faut les reconstruire à partir deimport.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
- Extensions de fichier dans les imports. ESM impose l'extension (
"./utils.js", et non"./utils") pour les chemins relatifs. CJS, lui, est plus tolérant. - Liaisons vivantes vs instantanés. En ESM, les imports sont des références vivantes vers les variables du module qui exporte. En CJS, tu récupères une copie de ce qui a été assigné à
module.exportsau moment du chargement. La plupart du temps ça ne change rien, mais ça devient important en cas de dépendances circulaires.
Lequel choisir ?
Pour un nouveau projet : ES Modules, sans hésiter. Mets "type": "module" dans ton package.json et n'y reviens plus. ESM est le standard du langage, fonctionne de la même façon côté navigateur et côté Node, gère le top-level await, et tous les outils modernes sont pensés autour.
Reste sur CommonJS si :
- Tu maintiens une base de code CJS existante et la migration n'en vaut pas encore la peine.
- Tu publies une bibliothèque qui doit rester compatible avec de très vieilles versions de Node ou avec des consommateurs qui ne peuvent pas utiliser ESM.
- Une dépendance critique ne propose que du CJS et son interop est bancale. (Rare aujourd'hui, mais ça arrive encore.)
Même dans ces cas-là, tu vas lire du code ESM en permanence : à peu près tout ce qui est publié sur npm ces dernières années va dans ce sens. Être à l'aise avec les deux n'est pas un luxe ; maîtriser les idiomes de celui que tu écris réellement non plus.
Petite check-list mentale
Quand tu ouvres un nouveau fichier, pose-toi ces questions :
- Ce fichier utilise-t-il
import/exportourequire/module.exports? Ne mélange pas les deux. - Que dit le
package.jsonle plus proche à propos de"type"? - Si tu importes un package, regarde son
package.json: livre-t-il de l'ESM, du CJS, ou les deux ? - Si tu tombes sur
ERR_REQUIRE_ESM, tu es en CJS en train d'essayer de charger de l'ESM. Passe à unimport()dynamique, ou bascule l'appelant en ESM.
Quatre-vingt-dix pour cent des galères de modules dans Node se résument à l'une de ces quatre questions.
La suite : les bases de npm
Les modules servent à découper ton code entre plusieurs fichiers. L'étape suivante, c'est récupérer du code écrit par d'autres — et c'est exactement le rôle de npm. On va voir comment installer des paquets, comprendre les plages de versions semver, et les parties du workflow npm que tu utilises vraiment au quotidien.
Questions fréquentes
Quelle est la différence entre require et import en JavaScript ?
require vient de CommonJS : chargement synchrone, exécuté à l'endroit où tu l'appelles, et il renvoie ce que le module a affecté à module.exports. import, c'est la syntaxe ES Modules : statique, hissée en haut du fichier et analysée avant que le code ne tourne. Les deux divergent aussi sur la valeur de this, sur la résolution des dépendances circulaires et sur la prise en charge du await au niveau racine.
CommonJS ou ES Modules pour un nouveau projet Node ?
Pars sur ES Modules. Tu ajoutes "type": "module" dans ton package.json et tu écris en import/export. ESM est le standard officiel, il fonctionne aussi bien côté navigateur que sous Node, et il gère le top-level await. CommonJS reste présent dans les vieux packages et certains outils — tu vas donc en lire, même si tu n'en écris plus.
Peut-on mélanger require et import dans un même projet ?
Oui, mais avec des règles. Un fichier .mjs ou un package avec "type": "module" est en ESM ; .cjs ou "type": "commonjs" reste en CJS. Depuis l'ESM, tu peux import un module CommonJS (son module.exports apparaît comme export par défaut). L'inverse est bloqué : un fichier CommonJS ne peut pas faire require() d'un module ESM, il faut passer par import() dynamique, qui renvoie une Promise.