Skip to main content

ESM vs CJS

There are currently two main module formats in the JavaScript ecosystem: ECMAScript Modules (ESM) and CommonJS (CJS). CJS was first, but ESM is now the JavaScript standard and is the future. The main difference between the two is how they import files.

tl;dr

  • CJS uses require and exports/module.exports
  • CJS is the past
  • ESM uses import and export
  • ESM is the future

CJS

JavaScript used to not have any module system, so CJS was created. CJS is the default module format in Node.js but has never been supported in browsers, without expensive building processes. Thus, there has been a split between code that natively supports Node.js vs natively supporting browsers. (As explained in the CJS section, this is no longer the case.)

How to Run JavaScript in CJS Mode

  • run node without "type": "module" in an ancestor package.json
    • or without any ancestor package.json
    • or with "type": "commonjs"
  • transpile TS to JS with "module": "CommonJS" (this produces CJS outputs)
  • use the .cjs or .cts extensions (to automatically trigger CJS mode)

CJS syntax

  • import other modules with require:
    // import export by name
    const {myVar} = require('./my-module.js');
    // import the default export
    const myVar = require('./my-module.js');
  • create exports by assigning them to the global exports object:
    exports.myVar = 'my var';
  • create a default export by assigning it to module.exports:
    module.exports = 'my var';
  • dynamic (async) import can be used:
    const {myVar} = await import('./my-module.js');
  • top-level await are not supported
    // this will not work
    await myAsyncFunction();
  • in Node.js, the path to the current script file is obtained with globals __dirname and __filename

ESM

The standard module system incorporated into the JavaScript language itself is ESM. ESM is already supported in browsers and Node.js support has been improving with every release. Using ESM allows you to write code that can run, natively, in both Node.js and browsers. ESM syntax is also (imo) much cleaner than CJS syntax.

How to Run JavaScript in ESM Mode

  • include type="module" in <script> tags in HTML:
    <script type="module" src="./my-modules.js"></script>
  • add "type"="module" in your package.json file
  • transpile TS to JS with any "module" compiler option that starts with "ES" (ES6, ES2022, etc) (this will produce ESM outputs)
  • use the .mjs or .mts extensions (to automatically trigger ESM mode)

ESM syntax

  • import other modules with import:
    // import export by name
    import {myVar} from './my-module.js';
    // import the default export
    import myVar from './my-module.js';
    // import all named exports
    import * as myVar from './my-module.js';
  • use the export keyword for exports:
    export const myVar = 'my var';
  • create a default export using export default:
    export default 'my var';
  • dynamic (async) import can be used:
    const {myVar} = await import('./my-module.js');
  • top-level await is supported
    // this will work
    await myAsyncFunction();
  • in Node.js, the path to the current script file is obtained with import.meta.dirname and import.meta.filename

ESM-only features

  • ESM can statically import CJS files
    • CJS cannot statically import ESM (import {} from '';)
    • CJS can dynamically import ESM (import(''))
  • ESM supports top-level await
    • this might not seem like much but has been a game-changer for my open source packages
  • native browser support