Menu

CommonJS vs ES Modules: require vs import in Node.js

JavaScript bringt zwei Modulsysteme mit. Warum es beide gibt und wann du in Node-Projekten zu require oder import greifen solltest.

Zwei Modulsysteme, eine Sprache

Ursprünglich hatte JavaScript überhaupt kein Modulsystem. Node hat die Lücke 2009 mit CommonJS geschlossen (require, module.exports), und jahrelang sah Node-Code genau so aus. 2015 bekam die Sprache dann ein eigenes, standardisiertes Modulsystem spendiert — ES Modules (import, export) —, das inzwischen sowohl Browser als auch Node unterstützen.

In freier Wildbahn begegnen dir deshalb beide Varianten. Hier ist dasselbe Minimodul einmal in jedem Stil:

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

Gleiche Funktion, nur anders verpackt. Auf den restlichen Abschnitten dieser Seite geht es darum, wann welche Verpackung zählt – und zu welcher du im Alltag greifen solltest.

Die Unterschiede in der Syntax

Die Unterschiede, die dir täglich begegnen, passen locker auf eine Postkarte:

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require ist ein ganz normaler Funktionsaufruf. import dagegen ist ein Statement – es darf nur auf der obersten Ebene eines Moduls stehen, und der Pfad muss ein String-Literal sein. Diese Einschränkung ist kein Zufall: Genau sie macht möglich, was ESM kann und CommonJS nicht.

Der eigentliche Unterschied: statisch vs. dynamisch

Bei CommonJS wird require() in dem Moment ausgewertet, in dem die Zeile ausgeführt wird. Du kannst den Aufruf also in ein if packen, den Pfad zur Laufzeit zusammenbauen oder ein Modul nur unter bestimmten Bedingungen laden:

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

ES Modules sind statisch. Bevor irgendein Code läuft, parst die Engine erst mal alle import-Anweisungen, baut daraus einen Abhängigkeitsgraphen und löst alles im Voraus auf. Genau deshalb muss der Pfad ein literaler String sein, und genau deshalb darf import nur auf oberster Ebene stehen.

Der Vorteil: Tools sehen den kompletten Modulgraph, ohne auch nur eine Zeile Code auszuführen. So machen Bundler Tree-Shaking (unbenutzte Exports fliegen raus), Editoren liefern dir zuverlässige Autovervollständigung, und der Browser kann Module parallel laden.

Wenn du in ESM wirklich mal dynamisch laden musst, nimmst du dynamisches import() — ein funktionsähnlicher Ausdruck, der ein Promise zurückgibt:

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

Wie Node entscheidet, welches Modulsystem eine Datei nutzt

Ein einzelnes Node-Projekt kann problemlos beide Modulsysteme gleichzeitig enthalten. Welches System Node für eine bestimmte Datei anwendet, macht es an zwei Dingen fest:

  • An der Dateiendung: .mjs ist immer ESM, .cjs ist immer CommonJS.
  • Am Feld "type" in der nächstgelegenen package.json: Bei "module" werden .js-Dateien als ESM behandelt, bei "commonjs" (der Standard, wenn das Feld fehlt) als CJS.
// package.json
{
    "name": "my-app",
    "type": "module"
}

Mit "type": "module" verwendet eine normale hello.js im selben Package import/export. Legst du daneben eine hello.cjs ab, arbeitet genau diese eine Datei mit require. So lässt sich ein Projekt Schritt für Schritt migrieren – oder eine Library kann beide Varianten parallel ausliefern.

Typische Anfängerfalle: In einer ESM-Datei existieren require und module.exports schlicht nicht. Wer sie aus Gewohnheit trotzdem benutzt, fängt sich einen ReferenceError ein.

Interop: CommonJS und ES Modules mischen

Früher oder später musst du aus einer ESM-Datei ein CommonJS-Package einbinden – oder andersherum. Die Regeln dafür sind nicht symmetrisch.

ESM importiert CommonJS funktioniert direkt. Das module.exports-Objekt aus CJS landet als Default-Export:

index.js
Output
Click Run to see the output here.
// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

Named Imports aus CommonJS klappen manchmal – Node versucht, benannte Exports statisch zu erkennen –, aber verlässlich wird's erst, wenn du den Default holst und destrukturierst:

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJS, das ESM einbindet — das ist die schmerzhafte Richtung. Ein ES-Modul per require() zu laden geht nicht, das wirft sofort ERR_REQUIRE_ESM. Der rettende Ausweg ist das dynamische import(), das auch in CJS funktioniert und ein Promise zurückgibt:

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

Moderne Node-Versionen (22+) haben unter bestimmten Bedingungen ein synchrones require() für ESM eingeführt – portabel ist aber nach wie vor das dynamische import().

Weitere Verhaltensunterschiede, die du kennen solltest

Abseits der Syntax unterscheiden sich die beiden Systeme noch in ein paar Details, die einen gelegentlich auf dem falschen Fuß erwischen:

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

Noch ein paar Punkte:

  • this auf oberster Ebene. In CJS zeigt this auf module.exports. In ESM ist es undefined – ESM läuft grundsätzlich im Strict Mode.
  • __dirname und __filename. In CJS bekommst du beide geschenkt. In ESM musst du sie dir aus import.meta.url selbst ableiten:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • Dateiendungen bei Imports. ESM verlangt bei relativen Pfaden die Endung ("./utils.js", nicht "./utils"). CJS ist da nachsichtiger.
  • Live Bindings statt Snapshots. ESM-Imports sind lebende Referenzen auf die Variablen des exportierenden Moduls. CJS gibt dir dagegen eine Kopie dessen, was zum Ladezeitpunkt in module.exports stand. Den meisten Code interessiert das nicht – bei zirkulären Abhängigkeiten wird es aber relevant.

Welches System solltest du nehmen?

Für ein neues Projekt: ES Modules. Setz "type": "module" in der package.json und gut ist. ESM ist der Sprachstandard, funktioniert in Browser und Node gleich, unterstützt Top-Level-await und das gesamte Tooling ist darauf ausgelegt.

Bei CommonJS bleibst du, wenn:

  • du eine bestehende CJS-Codebasis pflegst und die Migration den Aufwand (noch) nicht wert ist.
  • du eine Library veröffentlichst, die sehr alte Node-Versionen oder Konsumenten ohne ESM-Support bedienen muss.
  • eine zentrale Dependency nur CJS ausliefert und der Interop-Teil unsauber ist. (Heute selten, kommt aber vor.)

Selbst dann wirst du ständig ESM-Code lesen – alles, was in den letzten Jahren auf npm veröffentlicht wurde, geht in diese Richtung. An beiden Welten vorbeizukommen ist keine Option; fließend in den Idiomen der Welt zu sein, in der du gerade schreibst, schon.

Eine kurze mentale Checkliste

Wenn du eine neue Datei öffnest, frag dich:

  • Verwendet diese Datei import/export oder require/module.exports? Nicht mischen.
  • Was sagt die nächstgelegene package.json zum Feld "type"?
  • Wenn du ein Paket importierst: Wirf einen Blick in dessen package.json – liefert es ESM, CJS oder beides?
  • Kommt ein ERR_REQUIRE_ESM, bist du in CJS und versuchst ESM zu laden. Wechsel auf dynamisches import() oder zieh den Aufrufer nach ESM um.

Neunzig Prozent aller Modul-Verwirrungen in Node fallen in eine dieser vier Kategorien.

Als Nächstes: npm-Grundlagen

Module sind der Weg, um deinen eigenen Code auf mehrere Dateien aufzuteilen. Im nächsten Schritt holst du dir Code von anderen Leuten ins Projekt – genau dafür ist npm da. Wir schauen uns an, wie du Pakete installierst, was es mit semver-Ranges auf sich hat und welche Teile des npm-Workflows du im Alltag wirklich brauchst.

Häufig gestellte Fragen

Was ist der Unterschied zwischen require und import in JavaScript?

require ist die CommonJS-Variante: synchron, wird genau an der Stelle ausgeführt, an der sie steht, und gibt zurück, was das Modul an module.exports gehängt hat. import gehört zu den ES Modules — statisch, wird an den Anfang der Datei gehoben und schon vor der Ausführung analysiert. Dazu kommen Unterschiede bei this, beim Auflösen zyklischer Abhängigkeiten und beim Top-Level-await, das nur ESM erlaubt.

CommonJS oder ES Modules für ein neues Node-Projekt?

Nimm ES Modules. Trag einfach "type": "module" in die package.json ein und arbeite mit import/export. ESM ist der offizielle Standard, läuft im Browser wie in Node und unterstützt Top-Level-await. CommonJS wirst du trotzdem lesen müssen — in älteren Paketen und Tooling ist es nach wie vor überall.

Kann ich require und import im selben Projekt mischen?

Ja, aber mit klaren Regeln. Eine .mjs-Datei oder ein Paket mit "type": "module" läuft als ESM, .cjs oder "type": "commonjs" als CJS. Aus ESM heraus kannst du ein CommonJS-Modul ganz normal importen — module.exports landet dann als Default-Export. Umgekehrt geht das nicht direkt: CommonJS kann ESM nicht per require() laden, du brauchst das dynamische import(), und das liefert ein Promise zurück.

Lerne mit Coddy zu programmieren

LOS GEHT'S