Corriger les ruptures de dépendances ESM vs CommonJS dans les applications Node
Corrigez les ruptures ESM vs CommonJS en repérant rapidement les incompatibilités de module et en choisissant la bonne solution dans package.json, le build ou la dépendance.

À quoi ressemble une rupture ESM vs CommonJS
Node.js propose deux façons de charger des fichiers JavaScript. CommonJS est l'ancien style qui utilise require() et module.exports. ESM (ECMAScript Modules) est le style plus récent qui utilise import et export.
La plupart des applications ne sont pas « purement » l'un ou l'autre. Votre code peut être CommonJS, une dépendance peut être uniquement ESM, et un outil de dev peut réécrire les imports pendant que vous êtes en local. C'est ce mélange qui crée les problèmes.
Quand on parle de « mismatch de format de module », on veut dire qu'un fichier est chargé comme si c'était CommonJS alors qu'il est en fait ESM (ou l'inverse). Par exemple : votre code fait require('some-lib'), mais some-lib est uniquement ESM, donc Node refuse de le charger avec require().
C'est pour ça que cela casse souvent juste après l'ajout d'une dépendance ou après le déploiement. Beaucoup de configurations de dev cachent le mismatch :
- TypeScript + ts-node/tsx peuvent exécuter des imports de style ESM en dev même si le build produit du CommonJS.
- Les bundlers peuvent faire fonctionner les choses localement en bundleant les dépendances, alors que la production utilise la résolution Node non bundleée.
- Un build serverless ou Docker peut utiliser une version de Node différente ou un fichier d'entrée différent de votre exécution locale.
Un scénario typique : un prototype fonctionne bien avec npm run dev, puis plante en production juste après l'ajout d'un petit paquet utilitaire. En local, le serveur de dev transpile tout à la volée. En production, vous exécutez le dist/index.js compilé, Node le traite comme CommonJS, et le premier require() d'une dépendance ESM-only lance une exception.
Cela affecte beaucoup d'équipes utilisant TypeScript, des outils de type Next/Nuxt, et des starters générés par l'IA. Le code généré tend aussi à mélanger les signaux (par exemple "type": "module" dans package.json, mais une sortie CommonJS du build), ce qui créé des erreurs « ça marche sur ma machine ». Le problème central est simple : le runtime et la sortie du build ne parlent pas la même langue de modules.
Messages d'erreur courants et ce qu'ils indiquent généralement
Quand une app Node mélange ESM et CommonJS, le message d'erreur est souvent l'indice le plus rapide. La formulation indique généralement ce que Node pensait que le fichier était (ESM ou CJS) et ce qu'il a essayé de charger.
ERR_REQUIRE_ESM
Cela arrive lorsqu'un code CommonJS appelle require() sur un paquet qui est uniquement ESM.
On le voit souvent après la mise à jour d'une dépendance, car de nombreuses bibliothèques sont passées à l'ESM dans des versions majeures. Déclencheurs courants : votre fichier est traité comme CommonJS (pas de "type": "module", ou le fichier a une extension .cjs), vous requirez un chemin profond qui contourne les points d'entrée du paquet, ou un outil (test runner, chargeur de config) exécute votre code en CommonJS même si votre app est par ailleurs ESM.
Ce que l'erreur dit vraiment : arrêtez d'utiliser require() pour cet import, ou choisissez une version de dépendance qui supporte encore CommonJS.
« Cannot use import statement outside a module »
C'est l'image miroir. Node traite le fichier courant comme CommonJS, mais il contient de la syntaxe ESM comme import.
Ca vient souvent d'un "type": "module" manquant dans package.json, d'utiliser .js où Node attend .mjs, ou d'un step de build qui émet de l'ESM tandis que vous le démarrez en CommonJS.
« Named export ... not found » (ou surprises liées à l'export par défaut)
Ces erreurs proviennent généralement d'une hypothèse que les formes d'exports ESM et CommonJS sont identiques. Un module CommonJS exporte souvent un objet unique, tandis qu'ESM s'attend à des exports nommés.
Un test rapide : si import { thing } from "pkg" échoue, mais import pkg from "pkg" fonctionne, vous êtes probablement face à un problème d'interop CommonJS.
« exports is not defined » et autres surprises à l'exécution
Cela survient souvent quand du code qui attend des globals CommonJS (exports, require, module) tourne dans un contexte ESM qui ne les fournit pas.
Un scénario fréquent est « ça marchait en dev ». Un serveur de dev transpile tout, mais la production exécute les fichiers buildés directement. La sortie du build contient encore exports.foo = ..., et Node charge ça comme ESM, donc ça plante.
Vérifications rapides avant de modifier quoi que ce soit
Quand vous avez des erreurs de format de module, la tentation est de basculer "type": "module" ou de commencer à réécrire les imports. Ne le faites pas tout de suite. Quelques vérifications rapides vous diront généralement si vous avez ajouté une dépendance ESM-only, si vous démarrez le mauvais point d'entrée, ou si un outil exécute votre code dans un mode inattendu.
Commencez par confirmer le runtime exact. Le même code peut se comporter différemment sous node, ts-node, un test runner ou un bundler. Vérifiez aussi la version de Node dans l'environnement qui échoue (local, CI, production). Les défauts et les cas limites ont changé entre les versions de Node, et beaucoup d'hébergeurs sont en retard par rapport à votre machine locale.
Avant de toucher au code, confirmez :
- La version de Node et la commande de démarrage dans chaque environnement (par exemple
node server.jsvs un runner TypeScript). - Le premier fichier qui plante et son extension :
.cjs,.mjs,.js, ou.ts. - Le
package.jsonle plus proche de ce fichier et s'il définit"type": "module". - Le nom de la dépendance et le chemin de fichier mentionné dans la première ligne de la stack trace qui compte.
- Si ça arrive seulement en dev ou seulement en production, et ce qui diffère (sortie du build, méthode d'installation, variables d'environnement).
Deux motifs rapides reviennent souvent. Si la stack trace pointe dans node_modules/<pkg>/... et dit qu'on ne peut pas require() ce fichier, vous avez probablement importé un paquet ESM-only dans du code CommonJS. Si elle pointe vers votre sortie build (comme dist/index.js), votre build et votre runtime ne sont pas d'accord sur le format de module.
Un petit exemple : un prototype tourne en local via ts-node (qui peut gérer ESM différemment), mais la production lance node dist/server.js. Ce seul changement peut révéler le mismatch que vous devez corriger.
Étape par étape : diagnostiquer le mismatch de format de module
Le chemin le plus rapide est d'arrêter de deviner et d'identifier où Node pense que la frontière est entre ESM et CommonJS.
1) Commencez par la première frame qui est « votre code »
Ouvrez l'erreur et descendez la stack trace. Ignorez d'abord la longue liste de frames dans node_modules. Trouvez la première frame qui pointe vers un fichier qui vous appartient (chemin du repo), notez le nom du fichier et son extension, la ligne qui a déclenché le chargement (un import, require, ou un import() dynamique), et le package.json le plus proche qui contrôle ce fichier.
Cette frame est généralement là où la mauvaise décision de format de module devient visible.
2) Confirmez comment Node interprète ce fichier (ESM ou CJS)
Node décide ESM vs CJS principalement selon les extensions de fichier et package.json :
.mjss'exécute en ESM..cjss'exécute en CommonJS..jsdépend detypedanspackage.json(si"type": "module", c'est ESM ; sinon c'est CommonJS).
Un piège courant : vous pensez être en CommonJS parce que vous avez écrit require(), mais votre package est en type: module, donc le fichier .js est en réalité ESM.
3) Inspectez les points d'entrée de la dépendance
Regardez la dépendance qui est chargée au point d'échec. Dans son node_modules/<pkg>/package.json, vérifiez ce que Node sélectionne :
main(souvent CommonJS)module(souvent ESM, mais Node ne l'utilise pas toujours directement)exports(peut mapper des fichiers différents pourimportvsrequire)
Si exports existe, il décide souvent de tout. Un paquet peut fournir de l'ESM pour import mais ne pas offrir de chemin CommonJS pour require, ce qui mène à ERR_REQUIRE_ESM.
4) Reproduisez avec le plus petit snippet possible
Créez un petit fichier à côté de votre app (ou dans un dossier scratch) et testez uniquement l'import problématique.
// test-load.js
const pkg = require("the-problem-package");
console.log(pkg);
Puis essayez la version ESM aussi :
// test-load.mjs
import pkg from "the-problem-package";
console.log(pkg);
Si l'un fonctionne et l'autre échoue, vous avez confirmé que c'est un mismatch de format (et non votre logique métier).
5) Décidez quoi changer : l'app, le build ou la dépendance
Utilisez ce que vous avez appris pour choisir la correction la moins risquée :
- Changez le mode de modules de votre app (extensions ou
type) si vous contrôlez la majeure partie du code. - Changez la sortie de votre build/transpile si vous compilez TypeScript ou bundlez.
- Changez la dépendance (pinner une version, remplacer le paquet, ou utiliser un autre point d'entrée) si le paquet ne supporte plus votre format.
Correctifs ciblés dans package.json (type, main, exports)
Beaucoup de crashes liés au format de module peuvent être corrigés sans réécrire tout le code. L'objectif est de clarifier si votre package (ou la dépendance) est ESM, CommonJS, ou les deux.
Commencez par "type". Définir "type": "module" inverse le comportement par défaut pour chaque fichier .js du package en ESM. C'est bien si vous êtes complètement engagé sur ESM, mais cela peut aussi déclencher une cascade d'échecs require(). Si vous avez encore des fichiers CommonJS, envisagez de laisser "type" non défini et d'opter pour des extensions fichier par fichier.
Quand vous avez besoin d'un comportement différent par fichier, préférez les extensions aux basculements globaux :
- Utilisez
.cjspour les fichiers qui doivent être CommonJS (require,module.exports). - Utilisez
.mjspour les fichiers qui doivent être ESM (import,export). - Utilisez
.jsseulement si letypedu package correspond à ce que vous voulez.
Ensuite, vérifiez vos points d'entrée. "main" est l'entrée classique de Node et est généralement CommonJS. Certains bundlers regardent aussi "module" comme entrée ESM. Si vous avez besoin des deux builds, pointez-les vers des fichiers différents (par exemple dist/index.cjs vs dist/index.js).
"exports" est le plus puissant et aussi le plus susceptible de vous surprendre. Une fois présent, il peut bloquer des imports profonds comme some-lib/dist/internal.js même si ce fichier existe. Les anciens outils et test runners peuvent aussi échouer s'ils comptent sur des chemins profonds. Utilisez "exports" pour exposer seulement ce que vous voulez, mais soyez explicite sur les cibles import et require quand vous supportez les deux.
Si vous changez les points d'entrée, évitez de casser les consommateurs en procédant par étapes : gardez "main" stable pendant que vous introduisez "exports", exportez à la fois des cibles "import" et "require" quand vous supportez les deux, et remplacez les chemins profonds par un export public documenté.
Correctifs via les paramètres de build et de transpilation
Beaucoup d'échecs ESM/CommonJS ne viennent pas vraiment de la dépendance. Ils proviennent d'une sortie de build qui ne correspond pas à la façon dont Node exécute votre app.
Paramètres TypeScript qui déterminent ce que Node va charger
TypeScript peut compiler du code qui a l'air correct dans l'éditeur, mais les fichiers émis peuvent ne pas correspondre à votre runtime. Si vous exécutez du JavaScript compilé, vérifiez d'abord ces options :
compilerOptions.module:CommonJSproduitrequire(...);NodeNextouESNextproduitimport.compilerOptions.moduleResolution:NodeNextcomprend les règles ESM (comme les extensions de fichiers etexports).compilerOptions.esModuleInteropetallowSyntheticDefaultImports: ils peuvent faire compiler des imports même si l'interop d'exécution est toujours incorrecte.outDir: assurez-vous que tout le code exécuté provient d'un seul dossier (généralementdist).
Règle simple : compilez vers le même format de module que ce que votre processus Node attend. Si votre app est ESM, émettez de l'ESM. Si elle est CommonJS, émettez du CommonJS.
Quand le bundler « corrige » en dev, puis Node casse ensuite
Les bundlers et serveurs de dev réécrivent souvent ou bundleent les dépendances, donc l'app semble fonctionner en développement. Ensuite la production exécute Node directement sur vos fichiers compilés, et vous rencontrez soudainement des erreurs ESM/CJS.
Pour réduire les surprises, exécutez la commande de démarrage de production localement contre la sortie compilée, pas contre le serveur de dev.
Évitez le mélange src vs dist
Les ruptures reviennent souvent quand votre runtime importe certains fichiers depuis src et d'autres depuis dist. Cela mélange les systèmes de modules et les extensions.
Gardez cela propre :
- Assurez-vous que la production exécute uniquement
dist(ou uniquementsrcsi vous exécutez vraiment TypeScript directement). - Supprimez les anciens artefacts de build avant de reconstruire (des fichiers obsolètes peuvent encore être importés).
- Utilisez des chemins d'import cohérents qui pointent vers les fichiers buildés.
Correctifs en ajustant, pinant, ou changeant les dépendances
Parfois la correction la plus rapide se trouve dans le choix de dépendance, pas dans le code de l'app. Traitez la dépendance comme la variable : choisissez une version compatible, utilisez un point d'entrée supporté, ou remplacez-la.
Remplacer par une alternative compatible CJS (si possible)
Si votre app est CommonJS (utilise require) et qu'une dépendance est passée en ESM-only, la remplacer est souvent plus propre que de forcer un step de build juste pour ce paquet. C'est particulièrement vrai pour de petits utilitaires.
En choisissant une alternative, gardez-le simple : assurez-vous qu'elle supporte le système de modules que vous utilisez aujourd'hui, qu'elle correspond à votre version de Node, et qu'elle n'exige pas d'import profond.
Pinner une version compatible (avec prudence)
Pinner peut être le moyen le plus rapide d'arrêter l'hémorragie, surtout quand une release a changé le format de module. Considérez-le comme une mesure temporaire. Vous pouvez livrer ensuite, puis planifier la vraie correction (migrer votre app vers ESM, ou remplacer la dépendance). Surveillez aussi les correctifs de sécurité que vous pourriez manquer sur des versions plus anciennes.
Utiliser le point d'entrée documenté, pas un import profond
Beaucoup de pannes surviennent parce que le code importait un chemin interne qui fonctionnait avant, comme some-lib/dist/index.js. Après une mise à jour, le paquet ajoute une map exports et bloque les chemins profonds. La correction est généralement d'importer depuis le point d'entrée public (ou un sous-chemin exporté documenté).
Si la dépendance est ESM-only mais votre app est CJS
Vous avez trois choix réalistes : basculer votre app en ESM, remplacer la dépendance, ou l'isoler.
L'isolation est souvent un bon compromis : chargez le paquet ESM dans un petit module wrapper (en utilisant import() dynamique), et gardez le reste du code CommonJS pendant que vous planifiez une migration plus complète.
Pièges fréquents qui font revenir la rupture
Beaucoup de corrections ESM/CommonJS échouent la deuxième fois parce que l'app n'est pas réellement cohérente. Elle fonctionne avec une commande (souvent en dev), puis casse dans les tests, CI, ou la production parce qu'un point d'entrée ou une chaîne d'outils différente est utilisée.
Mélanger require() et import sur le même chemin d'exécution
Le piège n'est pas d'utiliser les deux styles quelque part dans le repo. Le piège, c'est que le même chemin d'exécution peut s'exécuter sous les deux règles de module.
Exemple : vous avez patché une route pour utiliser import() dynamique pour une dépendance ESM-only, mais un script CLI ou un test atteint encore l'ancien chemin require().
Si vous devez mélanger les formats temporairement, gardez la frontière évidente : un module wrapper qui fait l'import dynamique, et tout le reste appelle ce wrapper.
Déployer le source TypeScript au lieu du JS compilé
Cela arrive quand vous déployez un dossier contenant encore des .ts (ou une sortie ESM) alors que le runtime attend CommonJS (ou l'inverse). Localement ça semble correct parce que ts-node, un serveur de dev, ou un bundler compile pour vous.
Vérifiez ce qui est réellement déployé. Si votre serveur démarre avec node dist/index.js, assurez-vous que dist existe et contient le format que vous pensez. Confirmez aussi que vos points d'entrée (main, exports) pointent vers des fichiers buildés, pas vers le source.
Outils de dev qui patchent le chargement des modules
Les test runners, serveurs de dev et transpilers peuvent masquer des problèmes en transformant les imports à la volée. La production exécute Node.js nu et rencontre le mismatch brut.
Si le dev utilise un runner personnalisé mais que la production utilise node directement, ne considérez pas « ça marche en dev » comme validé tant que vous n'avez pas exécuté la commande de production localement.
Ajouter "type": "module" pour réparer un fichier et casser tout le reste
Définir "type": "module" change la signification de chaque fichier .js dans le package. Cela peut casser instantanément des require(), des fichiers de config que les outils attendent en CommonJS, et des dépendances plus anciennes qui supposent des points d'entrée CommonJS.
Si vous avez seulement besoin d'ESM dans une zone, pensez à utiliser .mjs pour les fichiers ESM et .cjs pour CommonJS, ou isolez le changement dans un sous-paquet au lieu de basculer tout le projet.
Pièges des paquets « double build » (comportement différent ESM vs CJS)
Certaines librairies distribuent à la fois des builds ESM et CJS. Node peut choisir une entrée différente selon que vous import ou require, et selon la condition exports du paquet. Le problème est que les deux versions peuvent « fonctionner » mais se comporter légèrement différemment (forme de l'export par défaut, effets de bord).
Quand la rupture revient, pinner la version de la dépendance et verrouiller le point d'entrée souhaité (en utilisant le style d'import documenté) aide. Si la librairie reste imprévisible, la remplacer par une dépendance plus simple est souvent la solution la plus rapide à long terme.
Exemple : un prototype qui marche en dev mais casse en production
Une histoire courante : tout semble bien sur votre laptop, puis les logs de déploiement explosent. En local vous avez lancé node server.js, cliqué un peu, et l'API a répondu. En production, le process démarre, reçoit la première requête, et plante.
Voici un setup réaliste. Le prototype a un fichier serveur CommonJS (server.js) qui utilise require() partout. Une dépendance, ajoutée pour la commodité, est ESM-only.
Le crash ressemble souvent à :
Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.
Instead change the require of ... to a dynamic import()
La cause racine est simple : un fichier CommonJS tente de charger un paquet ESM-only avec require(). Node refuse parce que les règles de chargement ESM et CommonJS sont différentes.
Deux corrections fonctionnent bien, selon le degré de changement que vous voulez faire.
Option 1 : basculer un fichier (ou la frontière) en ESM
Si le fichier serveur est le seul endroit qui importe la dépendance ESM-only, déplacez cette frontière vers l'ESM.
Vous pouvez renommer server.js en server.mjs et remplacer require() par import, ou garder server.js et charger la dépendance ESM avec un import dynamique :
const esmLib = await import('esm-only-lib');
Cela maintient la majorité du code inchangé tout en permettant de charger correctement le paquet ESM-only.
Option 2 : remplacer la dépendance par une alternative compatible CommonJS
Si convertir un fichier clé en ESM entraîne trop de changements (tests, configs, autres imports), remplacer la dépendance peut être plus rapide. Choisissez une dépendance qui supporte CommonJS (ou qui propose des builds doubles) et adaptez l'utilisation.
Pour confirmer la correction, ne relancez pas seulement le serveur dans le même environnement. Faites un rebuild propre et un cold start : supprimez la sortie de build et les caches, réinstallez les dépendances à partir de zéro, démarrez le serveur et testez l'endpoint qui plantait.
Étapes suivantes pour une correction propre et prête pour la production
Allez plus vite en prenant une décision et en la rendant cohérente : changez le format de l'app si la majeure partie du code et des outils penche d'un côté, changez la dépendance si un paquet est l'exception, ou changez le build si le source est correct mais la sortie est erronée.
Si vous demandez de l'aide, fournissez un snapshot propre :
- Le texte d'erreur exact et la stack complète
- Votre
package.json(en particuliertype,main,exports, et les dépendances) - Votre version de Node (locale, CI, production)
- Le fichier et la ligne qui déclenchent l'erreur
- La commande exacte de démarrage (avec les étapes de build)
Si cela vient d'un prototype généré par IA qui a été patché plusieurs fois, les mismatches de modules apparaissent souvent avec d'autres problèmes de production (auth broken, secrets exposés, structure difficile à maintenir). FixMyMess (fixmymess.ai) se concentre sur la stabilisation de ces bases héritées : diagnostiquer ce qui casse, appliquer les plus petits changements sûrs, et vérifier le résultat humainement. Si vous voulez un point de départ à faible risque, leur audit de code gratuit peut rapidement indiquer quelle frontière de module est erronée et quel chemin de correction est le plus sûr.
Questions Fréquentes
Quelle est la différence la plus simple entre ESM et CommonJS dans Node ?
CommonJS utilise require() et module.exports, tandis qu'ESM utilise import et export. Le problème pratique est que Node charge un fichier comme un seul format à l'exécution, et si le code ou une dépendance attend l'autre format, l'application plante.
Pourquoi ça marche en dev mais casse après le déploiement ?
Généralement, votre outil de développement masque le mélange. Un serveur de dev, un runner TypeScript ou un bundler peut réécrire les imports ou bundler les dépendances, alors que la production exécute souvent node directement sur dist, ce qui révèle le vrai décalage de format.
Que signifie généralement `Error [ERR_REQUIRE_ESM]` ?
Il signifie presque toujours qu'un fichier CommonJS appelle require() sur un paquet qui ne fournit que de l'ESM. Les corrections rapides sont : remplacer cet import par un import() dynamique depuis un wrapper, pinner/échanger la dépendance pour une version CJS-compatible, ou migrer cette partie de l'app vers ESM.
Comment corriger « Cannot use import statement outside a module » ?
Node traite le fichier comme CommonJS alors que le fichier contient de la syntaxe ESM. Vérifiez si "type": "module" manque dans le package.json, si vous utilisez .js alors que vous vouliez .mjs, ou si votre build émet de l'ESM alors que la production le démarre comme CommonJS.
Pourquoi j'obtiens « Named export … not found » ou un comportement par défaut étrange ?
C'est généralement un problème d'interop : vous importez des exports nommés depuis un module au format CommonJS. Essayez d'importer le module en entier comme default puis lisez ses propriétés, ou ajustez votre build/runtime pour charger la dépendance dans le format attendu.
Quelle est la façon la plus rapide de trouver la source réelle du décalage ?
Commencez par la première frame de stack trace qui pointe vers votre dépôt (pas node_modules). Notez l'extension du fichier, la ligne qui fait l'import/require, et le package.json le plus proche : c'est ce qui détermine si .js est ESM ou CJS.
Comment savoir si une dépendance est uniquement ESM ?
Ouvrez node_modules/<pkg>/package.json et vérifiez exports, main et les conditions d'import/require. S'il y a un exports et pas de chemin require, alors require() échouera même si des versions plus anciennes fonctionnaient.
Dois-je simplement ajouter ou retirer `"type": "module"` dans package.json ?
Évitez de basculer "type": "module" comme premier réflexe : cela change le sens de chaque fichier .js. Préférez être explicite : utilisez .mjs pour les fichiers ESM et .cjs pour CommonJS, ou isolez le changement dans un sous-paquet.
Quelles options TypeScript/build provoquent le plus souvent des problèmes ESM/CJS ?
Assurez-vous que la sortie du build correspond à la façon dont vous démarrez l'app. Si la production exécute node dist/server.js, vérifiez que TypeScript émet le même format de module que Node interprétera pour ce fichier, et évitez de mélanger des imports entre src et dist à l'exécution.
Que dois-je collecter avant de demander de l'aide, et FixMyMess peut-il corriger ça rapidement ?
Rassemblez : le texte d'erreur exact et la stack complète, votre package.json (notamment type, main, exports et les dépendances), la version de Node (locale et production), le fichier et la ligne qui déclenchent l'erreur, et la commande de démarrage (avec les étapes de build). FixMyMess (fixmymess.ai) peut réaliser un audit gratuit et appliquer la correction minimale en général en 48–72 heures.