CommonJS vs ES Modules in Node.js: Which Should Beginners Use?

Posted November 17, 2025 by Karol Polakowski

Node.js supports two module systems today: the older CommonJS (CJS) and the newer ECMAScript Modules (ESM). Both work in modern Node, but they behave differently in syntax, loading model, tooling, and interoperability. This guide explains the differences, shows concise examples, and gives a pragmatic recommendation for beginners.


Quick overview

CommonJS (CJS)

  • Synchronous, runtime require-based system (require and module.exports).
  • Historically the default for Node.js and the npm ecosystem.
  • Works everywhere in Node, including older versions and many packages.

Example (CommonJS):

// lib.cjs
function hello(name) {
  return `Hello, ${name}!`;
}
module.exports = { hello };
// index.cjs
const { hello } = require('./lib.cjs');
console.log(hello('World'));

ES Modules (ESM)

  • Standardized JavaScript modules (import / export) that work in browsers and Node.
  • Supports static analysis, tree-shaking, and top-level await.
  • In Node you enable ESM via `”type”: “module”` in package.json or using `.mjs` extensions.

Example (ESM):

// lib.mjs
export function hello(name) {
  return `Hello, ${name}!`;
}
export default hello;
// index.mjs
import hello from './lib.mjs';
import { hello as helloNamed } from './lib.mjs';
console.log(hello('World'), helloNamed('World'));

Key differences that matter to beginners

Syntax and semantics

  • CJS: `const x = require(‘x’)`, `module.exports = {}`, synchronous.
  • ESM: `import x from ‘x’`, `export default`, `export const a = …`, static imports by default.

Why it matters: ESM’s static imports allow better IDE support and tree-shaking; CJS is straightforward and familiar in many tutorials.

Enabling ESM in Node

  • Option A: set `”type”: “module”` in package.json — all `.js` files are treated as ESM.
  • Option B: keep `”type”: “commonjs”` (or omit) and use `.mjs` for ESM files.

Example package.json to opt into ESM:

{
  "type": "module"
}

File extensions and package.json

  • `.js` — interpreted based on package.json `type`.
  • `.mjs` — always ESM.
  • `.cjs` — always CommonJS.

This lets you mix formats in a repo if needed (useful during migration).

Loading model and startup

  • CJS loads modules synchronously via require() at runtime.
  • ESM loads statically where possible; dynamic import() returns a promise and supports top-level await.

Top-level await example (ESM only):

// fetch-data.mjs
const data = await fetch('https://api.example.com/data').then(r => r.json());
console.log(data);

dirname and filename

  • In CJS you have ` dirname` and ` filename` built-in.
  • In ESM they are not present; compute them from `import.meta.url`:
// esm-dir.mjs
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Interop between CJS and ESM

  • Importing a CJS package from ESM can yield the CommonJS exports as the `default` export when using `import` (behavior can vary).
  • From CJS you cannot use static `import` — you can use dynamic `import()` (returns a promise) or `createRequire`.

Example: require a CJS module from ESM using createRequire:

// esm-interop.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjsPkg = require('some-cjs-package');

Or dynamically import from CJS:

// dynamic.mjs
const { default: pkg } = await import('some-cjs-package');

Interop is workable but can be confusing; default/named export semantics may not match expectations.

Tooling and ecosystem

  • Many npm packages are modernizing to ESM or publishing dual packages (CJS + ESM). However, some older packages remain CJS-only.
  • Tooling like bundlers and test runners now have good ESM support, but config may be necessary (especially for Node-based tooling like Jest; though Jest and others support ESM nowadays).

Migration tips (small checklist)

  • Pick a single module strategy per package: either CJS or ESM for the majority of files.
  • If you want ESM but need CJS-only dependencies, use `createRequire()` or dynamic `import()`.
  • For gradual migration: rename entry files to `.mjs` or set `”type”: “module”` and convert modules incrementally.
  • Update tooling configurations (linters, test runners, build scripts) to understand ESM.
  • Test scripts that rely on ` dirname`/` filename` or synchronous requires — these need changes in ESM.

Which should beginners use?

  • If you’re following older tutorials, working with many CJS-only packages, or need maximum compatibility immediately: start with CommonJS. It’s simple and works out of the box in most Node versions.
  • If you are starting a new project, targeting future compatibility with browsers, or want to use top-level await and static analysis benefits: prefer ES Modules (ESM). Use Node 18+ (or latest LTS) for best ESM support and global fetch, and set `”type”: “module”` in package.json or use `.mjs` files.

Practical recommendation:

  • Beginner learning Node quickly and running examples from the web => CommonJS is fine.
  • Beginner building a new modern project, using front-end tooling, or planning to share code between browser and server => ESM is the better choice.

Short migration recipe (CJS -> ESM)

  1. Upgrade Node to a modern LTS (Node 18 or newer recommended).
  2. Add `”type”: “module”` to package.json (or rename entry to `.mjs`).
  3. Replace `module.exports =` and `require()` with `export` and `import`.
  4. Replace ` dirname/ filename` uses with `fileURLToPath(import.meta.url)` if needed.
  5. For any CJS-only dependency you can `import` dynamically or use `createRequire`.

Example conversion:

// before (CommonJS)
const { readFile } = require('fs').promises;
module.exports = async function load(path) {
  return readFile(path, 'utf8');
};
// after (ESM)
import { readFile } from 'fs/promises';
export default async function load(path) {
  return readFile(path, 'utf8');
}

Final thoughts

Both module systems will coexist for a while. ESM is the standard forward path: it aligns Node with browser JS and enables modern features. For beginners, choose based on short-term needs:

  • Need compatibility and follow many tutorials? Use CommonJS to move quickly.
  • Starting fresh and want modern behavior? Use ES Modules.

Either way, understanding the differences will save you time when you encounter interop edge cases or when you pick third-party packages.