In Node.js, ESM (ECMAScript Modules) and CommonJS (CJS) are two module systems used to organize and manage code. They differ in syntax, usage, and how they handle imports and exports.

1. CommonJS (CJS) Modules

CommonJS was the original module system in Node.js and is widely used in many Node.js applications. It’s synchronous, designed to work on the server-side in environments like Node.js.

Key Features:

  • Importing Modules: You use require() to import other modules.
  • Exporting Modules: You use module.exports or exports to export modules.
  • Synchronous Loading: Modules are loaded synchronously, which makes it suitable for server-side environments where blocking code isn’t a major concern.
  • File Extension: CommonJS modules usually have the .js extension.

Use Cases:

  • Most widely used in Node.js.
  • Supported in all versions of Node.js.
  • Many existing Node.js modules and packages use CommonJS.

2. ESM (ECMAScript Modules)

ECMAScript Modules (ESM) is the standardized module system introduced in JavaScript (ES6, or ES2015). It’s the official module system for JavaScript, and Node.js has gradually adopted it to stay aligned with the evolving JavaScript standard.

Key Features:

  • Importing Modules: You use the import keyword to import other modules.
  • Exporting Modules: You use the export keyword to export modules.
  • Asynchronous Loading: ESM modules are loaded asynchronously, making them better suited for environments like the browser, where non-blocking operations are important.
  • File Extension: ESM modules can have .mjs extension or can use .js with a "type": "module" field in package.json.

Example of ESM Syntax:

// Exporting in ESM
export function greet() {
  return 'Hello, world!';
}
 
// Importing in ESM
import { greet } from './greet.mjs';
console.log(greet()); // Outputs: Hello, world!

Use Cases:

  • The preferred module system for new JavaScript/Node.js code.
  • Asynchronous module loading makes it more suitable for modern web applications.
  • Aligns with the JavaScript standard (used in browsers).

3. Differences Between ESM and CommonJS

FeatureESMCommonJS (CJS)
Import syntaximportrequire
Export syntaxexport, export defaultmodule.exports, exports
LoadingAsynchronousSynchronous
File extension.mjs or .js (with "type": "module").js
Default exportsSupports export defaultNo native default export
Support in Node.jsNode 12+ (Stable in Node 14+)Supported in all versions

4. How to Use ESM in Node.js

By default, Node.js treats files with a .js extension as CommonJS modules. However, to use ESM syntax (import/export), you can:

  1. Use the .mjs Extension: Node.js will treat .mjs files as ESM modules.
  2. Set "type": "module" in package.json: You can also set "type": "module" in your package.json to tell Node.js that all .js files should be treated as ESM modules.

Example:

// package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module"
}

5. Interoperability

Node.js provides limited interoperability between ESM and CommonJS modules:

  • CJS to ESM: You can import CommonJS modules into ESM using import.
  • ESM to CJS: You need to use import() (dynamic import) to load ESM modules into CommonJS.

6. All examples of CJS vs MJS export and import

  • In-short, CJS have a module.exports and in MJS there’s only export.
  • In CJS exports is a reference to module.exports. But always avoid to mess things.
  • In CJS, module.exports = {} or function no need for {} in require. But if module.exports.foo is used, then foo is added as a property, and can only be required with {}.
// =======================================================================
// ==========================  COMMONJS (CJS)  ============================
// ==========================  module.exports  ============================
// =======================================================================
 
// 1. Single export (function)
module.exports = function add(a, b) {
  return a + b;
};
 
// 2. Single export (object)
module.exports = {
  sum(a, b) { return a + b; },
  mul(a, b) { return a * b; }
};
 
// 3. Export multiple items individually
module.exports.add = (a, b) => a + b;
module.exports.sub = (a, b) => a - b;
 
// 4. Using `exports`
exports.mul = (a, b) => a * b;
exports.div = (a, b) => a / b;
 
// 5. Export a class
class Person {
  constructor(name) { this.name = name; }
}
module.exports.Person = Person;
 
// 6. Runtime conditional exports
if (process.env.NODE_ENV === 'prod') {
  module.exports.config = require('./prod.config');
} else {
  module.exports.config = require('./dev.config');
}
 
// 7. CJS export merging (valid)
module.exports = {};
module.exports.a = 10;
module.exports.b = 20;
 
// 8. ❌ Wrong pattern — reassigning `exports`
/*
exports = { x: 10 }; // breaks!
*/
 
// 9. Exporting a function with properties (valid in CJS)
function fn() {}
fn.version = 1.0;
module.exports = fn;
 
// 10. Exporting a constructor + static functions
function Service() {}
Service.start = () => {};
module.exports = Service;
 
// 11. Exporting JSON is also valid
module.exports.settings = require('./settings.json');
 
// =======================================================================
// =========================  ECMASCRIPT MODULES  =========================
// ==========================        MJS / ESM       =====================
// =======================================================================
 
// ----------------------
// Named exports
// ----------------------
 
// 12. Named function export
export function greet(name) {
  return `Hello ${name}`;
}
 
// 13. Named variables export
export const A = 10;
export const B = 20;
 
// 14. Named class export
export class User {
  constructor(n) { this.name = n; }
}
 
// 15. Inline declarations
export const PI = 3.14;
export function area(r) { return PI * r * r; }
 
// 16. Export list at bottom
function foo() {}
function bar() {}
export { foo, bar };
 
// 17. Renaming exports
function login() {}
function logout() {}
export { login as userLogin, logout as userLogout };
 
// ----------------------
// Default exports
// ----------------------
 
// 18. Default export — function
export default function logger(msg) {
  console.log("LOG:", msg);
}
 
// 19. Default export — arrow function
export default (msg) => {
  console.log("LOG:", msg);
};
 
// 20. Default export — class
export default class Animal {
  constructor(type) { this.type = type; }
}
 
// 21. Default export — object literal
export default {
  x: 1,
  y: 2,
  print() { console.log("hello"); }
};
 
// 22. Default export — already declared value
const apiVersion = "v1";
export default apiVersion;
 
// 23. Default export — renaming a value
function compute() {}
export default compute;
 
// ----------------------
// Re-exports
// ----------------------
 
// 24. Re-export all
export * from './math.js';
 
// 25. Re-export specific
export { add, mul } from './operations.js';
 
// 26. Re-export with rename
export { add as plus } from './calc.js';
 
// 27. Re-export default (rare but valid)
export { default as MyUtil } from './util.js';
 
// 28. Export + re-export together
export { A as Alpha };
export * from './helpers.js';
 
// -------------------------------------------------------
// Dynamic imports (valid in ESM)
// -------------------------------------------------------
 
// 29. Dynamic import (top-level await)
const config = await import('./config.mjs');
 
// 30. Dynamic import inside a function
export async function loadLib() {
  const mod = await import('./lib.mjs');
  return mod;
}
 
// -------------------------------------------------------
// Hybrid CJS+ESM patterns (when using `"type": "module"`)
// -------------------------------------------------------
 
// 31. ESM export of CJS module using createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
export const legacy = require('./old-cjs.js');
 
// =======================================================================
// ************************** END OF MASTER FILE **************************
// =======================================================================