Menu
Français

CommonJS vs ES Modules : require ou import en JS

Node embarque deux systèmes de modules. On voit pourquoi les deux coexistent et comment choisir entre require et import dans vos projets.

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 :

index.js
Output
Click Run to see the output here.
index.js
Output
Click Run to see the output here.

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 :

index.js
Output
Click Run to see the output here.

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 :

index.js
Output
Click Run to see the output here.

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 : .mjs correspond toujours aux ES Modules, .cjs toujours à CommonJS.
  • Le champ "type" du package.json le plus proche : "module" indique que les fichiers .js sont 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 :

index.js
Output
Click Run to see the output here.
// 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 :

index.js
Output
Click Run to see the output here.

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 :

index.js
Output
Click Run to see the output here.

Quelques autres différences :

  • this au niveau racine. En CJS, this correspond à module.exports. En ESM, il vaut undefined. Les modules ESM sont toujours en mode strict.
  • __dirname et __filename. CJS vous les fournit gratuitement. En ESM, il faut les reconstruire à partir de import.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.exports au 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/export ou require/module.exports ? Ne mélange pas les deux.
  • Que dit le package.json le 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 à un import() 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.

Apprendre à coder avec Coddy

COMMENCER