Blog
Oct 12, 2025 - 10 MIN READ
CommonJS vs ESM: Understanding Both JavaScript Module Systems

CommonJS vs ESM: Understanding Both JavaScript Module Systems

A clear explanation of the two main module systems in JavaScript — CommonJS and ES Modules (ESM) — how they work, their differences, and how to use them in Node.js, TypeScript, and Bun.

dogunfx

dogunfx

JavaScript has evolved a lot — and so have its module systems. If you’ve ever seen both require() and import, you’ve encountered CommonJS (CJS) and ECMAScript Modules (ESM). Both serve the same purpose — organizing and reusing code across files — but they work differently under the hood.

This post explains both systems, when to use each, and how to transition between them safely in Node.js, TypeScript, and Bun.


1) The Purpose of Modules

Modules help developers split large programs into smaller files. Without modules, everything would exist in a single global scope — messy, risky, and hard to maintain.

// Without modules
function greet(name) {
  console.log("Hello " + name);
}

With modules, we can separate functionality:

// greetings.js
export function greet(name) {
  console.log(`Hello, ${name}`);
}

// app.js
import { greet } from "./greetings.js";
greet("Dogunfx");

2) CommonJS (CJS)

CommonJS is the original module system used in Node.js. It relies on require() and module.exports.

Example

// math.js
const add = (a, b) => a + b;
module.exports = { add };
// app.js
const { add } = require("./math");
console.log(add(2, 3)); // 5

Characteristics

Synchronous loading — modules are loaded in order, great for server-side.
Dynamic import — can require conditionally or within functions.
Not browser-native — doesn’t work in browsers without bundlers.
Static analysis harder — tooling like tree-shaking isn’t as effective.

Under the hood

  • Each file is wrapped in a function by Node.js.
  • Variables are scoped locally (not global).
  • Exports are assigned to module.exports.

3) ES Modules (ESM)

ES Modules (standardized in ES6 / 2015) are now the official module format for both browsers and Node.js.

Example

// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from "./math.js";
console.log(add(2, 3)); // 5

Characteristics

Asynchronous loading — ideal for browsers and modern runtimes.
Static structure — enables tree-shaking and better optimization.
Native in browsers & Bun.
Stricter syntax — must use explicit file extensions (.js, .mjs, etc.).
No dynamic require() — static imports only at the top level.

Default Export Example

// logger.js
export default function log(msg) {
  console.log("[LOG]", msg);
}
// app.js
import log from "./logger.js";
log("Running ESM example");

4) File Extensions and Configuration

Node.js uses file extensions or the "type" field in package.json to decide which system to use.

Option 1 — Per-file Extension

FileMeaning
.cjsTreated as CommonJS
.mjsTreated as ES Module

Option 2 — Project-wide Type

package.json

{
  "type": "module"
}

Now all .js files are treated as ESM, unless they have a .cjs extension.


5) Interoperability (Mixing Systems)

Sometimes you’ll need to use both systems, especially when using older NPM packages.

Importing CommonJS in ESM

// app.mjs
import pkg from "lodash";
const { isEmpty } = pkg;

Importing ESM in CommonJS

// app.cjs
(async () => {
  const { add } = await import("./math.mjs");
  console.log(add(2, 2));
})();

6) TypeScript Modules

TypeScript supports both systems depending on your config.

tsconfig.json

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022"
  }
}

math.ts

export function add(a: number, b: number) {
  return a + b;
}

app.ts

import { add } from "./math.js";
console.log(add(2, 3));

CommonJS (Legacy Projects)

{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "ES2020"
  }
}

Then:

const { add } = require("./math");

7) Performance and Tooling

FeatureCommonJSESM
LoadingSynchronousAsynchronous
Native in Browsers❌ No✅ Yes
Static Analysis / Tree Shaking❌ Hard✅ Easy
Top-level await❌ No✅ Yes
Dynamic Import✅ Yes (require())✅ Yes (import())
TypeScript Support✅ Mature✅ Modern default

8) Real Example — Migrating from CJS to ESM

Before (CJS):

const express = require("express");
const app = express();
module.exports = app;

After (ESM):

import express from "express";
const app = express();
export default app;

In package.json:

{ "type": "module" }

9) Bun, Deno, and Modern Runtimes

RuntimeDefault Module SystemNotes
BunESMSupports both, with fast native loading
DenoESMUses explicit URL-based imports
Node.jsCJS (legacy) / ESM (modern)Use "type": "module"
BrowserESM<script type="module"> supported

Example in Browser:

<script type="module">
  import { add } from "./math.js";
  console.log(add(1, 2));
</script>

10) Summary Table

ConceptCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
Load TypeSynchronousAsynchronous
Default SupportNode.js legacyModern JS, browsers, Bun
File Extensions.js / .cjs.js / .mjs
Top-Level Await
Tree Shaking
Dynamic Imports
Best ForLegacy or Node-only appsModern, full-stack, or browser apps

11) References


In short:

  • Use CommonJS only if you must support legacy Node.js projects.
  • Use ESM for everything else — it’s the modern, universal standard for both JavaScript and TypeScript.
    You’ll get cleaner syntax, better tooling, and compatibility with all future runtimes.
Built by DOGUNFX using Nuxt UI • © 2025