# Node.js Modules: ESM vs CommonJS Explained

Source: https://devdocs10.com/articles/javascript/js-node-modules  
By Murat Bo · Published 2026-07-20 · DevDocs10 (https://devdocs10.com)

Node.js supports two module systems: `CommonJS` and `ESM` (ECMAScript Modules). This article explains how they differ, how they affect syntax and loading behavior, and how to choose the right one for your project.

> **Quick answer:** CommonJS uses `require()` and `module.exports`, while ESM uses `import` and `export`. In modern Node.js, ESM is the standard JavaScript module syntax, but CommonJS is still widely used and supported.

> **Difficulty:** Beginner to Intermediate
>
> **You'll understand this better if you know:** basic JavaScript syntax, how files can export values, and the difference between named and default values.

## 1. What Are ESM and CommonJS?

Modules let you split code into files and reuse functionality without putting everything in one script. In Node.js, the two main module systems are CommonJS and ESM.

- `CommonJS` is the older Node.js module system.
- `ESM` is the official JavaScript module standard.
- Both let you export code from one file and use it in another.
- They are not identical: they use different syntax and follow different loading rules.

For example, a utility file can export a function, and another file can import it. The syntax you use depends on whether the file is treated as CommonJS or ESM.

## 2. Why This Matters

Choosing the wrong module system can cause confusing runtime errors, failed imports, or package compatibility problems. Many Node.js projects still use CommonJS, but modern libraries increasingly publish ESM or dual packages.

Understanding both systems helps you:

- read older Node.js code confidently,
- use newer packages that ship as ESM-only,
- avoid import and export errors,
- configure your project correctly with `package.json`.

## 3. Basic Syntax

### CommonJS export and import

CommonJS uses `module.exports` to export values and `require()` to load them.

```javascript
function sum(a, b) {
  return a + b;
}

module.exports = sum;
```

Another file loads it with `require()`:

```javascript
const sum = require("./sum");

console.log(sum(2, 3));
```

### ESM export and import

ESM uses `export` and `import`.

```bash
export function sum(a, b) {
  return a + b;
}
```

And in another file:

```javascript
import { sum } from "./sum.js";

console.log(sum(2, 3));
```

The main idea is simple: both systems share the same goal, but they use different file loading rules and different syntax.

## 4. Step-by-Step Examples

### Example 1: Exporting a single function in CommonJS

This pattern is common in older Node.js code and many existing packages.

```javascript
function formatName(first, last) {
  return `${first} ${last}`;
}

module.exports = formatName;
```

This file exports one function as the module value itself, so the importing file gets that function directly.

### Example 2: Importing a CommonJS module

Use `require()` to load the exported function.

```javascript
const formatName = require("./format-name");

const displayName = formatName("Ada", "Lovelace");

console.log(displayName);
```

Here, Node.js resolves the file, evaluates it, and returns the value assigned to `module.exports`.

### Example 3: Exporting named values in ESM

ESM supports named exports, which makes it easy to share multiple values from one file.

```bash
export const pi = 3.14159;

export function circleArea(radius) {
  return pi * radius * radius;
}
```

Both `pi` and `circleArea` are available to import by name.

### Example 4: Importing named exports in ESM

Use matching names inside curly braces.

```javascript
import { circleArea, pi } from "./math.js";

console.log(pi);
console.log(circleArea(2));
```

This shows the most common ESM style: explicit named imports that match the exported names.

### Example 5: Default export in ESM

ESM can also export a single default value.

```bash
export default function greet(name) {
  return `Hello, ${name}!`;
}
```

And import it without curly braces:

```javascript
import greet from "./greet.js";

console.log(greet("Maya"));
```

This is useful when a file has one primary export.

## 5. Practical Use Cases

- Use `CommonJS` when maintaining older Node.js applications that already rely on `require()`.
- Use `ESM` for new Node.js projects when you want to align with the JavaScript standard.
- Use CommonJS when you depend on packages that are still CommonJS-only.
- Use ESM when you want native `import` syntax and better alignment with browser JavaScript.
- Use dual-package support when you publish a library that must work in both ecosystems.

In practice, many teams use CommonJS for existing backend services and ESM for new codebases or shared libraries.

## 6. Common Mistakes

### Mistake 1: Using `import` in a CommonJS file without enabling ESM

Beginners often write ESM syntax in a file that Node.js still treats as CommonJS.

**Problem:** Node.js will not parse `import` in a CommonJS file, so you may see an error like “Cannot use import statement outside a module”.

```javascript
import { readFile } from "node:fs/promises";

console.log("Loaded");
```

**Fix:** Either switch the project to ESM or use `require()` in CommonJS.

```javascript
const { readFile } = require("node:fs/promises");

console.log("Loaded");
```

The corrected version works because CommonJS files must use CommonJS loading rules unless the package is configured for ESM.

### Mistake 2: Using `require()` in an ESM file

When a file is treated as ESM, `require()` is not available by default.

**Problem:** In ESM, Node.js can report “require is not defined in ES module scope”.

```javascript
const path = require("node:path");

console.log(path.basename("/tmp/file.txt"));
```

**Fix:** Use `import` in ESM files.

```javascript
import path from "node:path";

console.log(path.basename("/tmp/file.txt"));
```

This works because ESM expects standard module imports instead of CommonJS loading functions.

### Mistake 3: Forgetting the file extension in ESM relative imports

Node.js ESM is stricter than CommonJS about relative paths.

**Problem:** In many ESM setups, omitting the extension in a relative import causes module resolution failure.

```javascript
import { sum } from "./math";
```

**Fix:** Include the full file name, including `.js` for local files.

```javascript
import { sum } from "./math.js";
```

This works because ESM follows explicit file resolution rules for local relative imports.

### Mistake 4: Mixing default and named exports incorrectly

CommonJS and ESM both support single-value and multi-value exports, but the syntax is not interchangeable.

**Problem:** Importing a default export as a named import, or vice versa, leads to undefined values or syntax errors.

```bash
export default function slugify(text) {
  return text.toLowerCase().replace(/\s+/g, "-");
}
```

```javascript
import { slugify } from "./slugify.js";
```

**Fix:** Import default exports without curly braces.

```javascript
import slugify from "./slugify.js";
```

The fixed version works because default exports must be imported as the default binding.

## 7. Best Practices

### Practice 1: Be consistent within a package

Mixing module systems in the same package increases confusion and makes imports harder to reason about. Pick one system for the package and use it consistently unless you have a specific interoperability need.

```javascript
// Preferred: use one style consistently in the package
export function parseId(value) {
  return Number(value);
}
```

### Practice 2: Use explicit file extensions in ESM

Explicit extensions make imports clearer and avoid resolution surprises in Node.js.

```javascript
import { parseId } from "./parse-id.js";
```

This helps prevent “module not found” problems caused by ambiguous relative paths.

### Practice 3: Choose named exports for shared utilities

Named exports make it easier to refactor and reduce accidental renaming across files.

```bash
export function formatCurrency(amount) {
  return `$${amount.toFixed(2)}`;
}
```

With named exports, imports stay clear about what the module provides.

### Practice 4: Use CommonJS only when compatibility requires it

If you are starting a new backend project, ESM is usually the better default. CommonJS is still valuable for older code and packages that depend on it, but it is no longer the best choice simply because it is familiar.

```javascript
// Legacy compatibility case
const legacyLib = require("legacy-lib");
```

This keeps your code aligned with the ecosystem you are actually targeting.

## 8. Limitations and Edge Cases

- `CommonJS` cannot use top-level `import` and `export` syntax without switching the file or package to ESM.
- `ESM` is stricter about file extensions in relative imports.
- Some packages are ESM-only, which means `require()` will fail with `ERR_REQUIRE_ESM`.
- Some older packages are CommonJS-only, so direct named imports may not work the way you expect.
- Interop between the systems is possible, but the returned shapes can be surprising, especially with default exports.
- Built-in Node.js modules may behave slightly differently depending on whether you import them with `import` or load them with `require()`.

One common “not working” scenario is importing a local file in ESM without the extension. Another is using a package that changed to ESM-only and getting `ERR_REQUIRE_ESM` when you try to load it with `require()`.

## 9. Practical Mini Project

Here is a small ESM-based command-line utility that splits a sentence into words and counts them. It shows a realistic Node.js workflow with reusable modules.

```javascript
// file: text-utils.js
export function countWords(text) {
  return text
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .length;
}

// file: app.js
import { countWords } from "./text-utils.js";

const input = "Node.js modules are useful";
const total = countWords(input);

console.log(`Word count: ${total}`);
```

Run the app file with Node.js in an ESM-configured project, and it prints the number of words in the sample string. This demonstrates how one module can export a function and another module can reuse it cleanly.

## 10. Key Points

- `CommonJS` uses `require()` and `module.exports`.
- `ESM` uses `import` and `export`.
- ESM is the JavaScript standard module format, but CommonJS remains important in Node.js.
- File type and `package.json` configuration determine how Node.js interprets a file.
- Interop is possible, but mixing the systems requires careful syntax and import shape awareness.

## 11. Practice Exercise

- Create two files: one that exports a greeting function and one that uses it.
- Do it once with CommonJS and once with ESM.
- Make sure the output prints a message like `Hello, Sam!`.

**Expected output:**

`Hello, Sam!`

**Hint:** In CommonJS, export with `module.exports`. In ESM, export with `export` and import with `import`.

**Solution:**

```javascript
// CommonJS version: greet.cjs
function greet(name) {
  return `Hello, ${name}!`;
}

module.exports = greet;

// CommonJS version: app.cjs
const greetCommonJs = require("./greet.cjs");

console.log(greetCommonJs("Sam"));

// ESM version: greet.js
export function greet(name) {
  return `Hello, ${name}!`;
}

// ESM version: app.js
import { greet } from "./greet.js";

console.log(greet("Sam"));
```

This exercise helps you see the same idea expressed in both systems, which makes the syntax differences easier to remember.

## 12. Final Summary

Node.js supports both CommonJS and ESM, and each one solves the same core problem: splitting code across files and reusing it safely. CommonJS is the traditional Node.js system and still powers a large amount of existing code. ESM is the modern JavaScript standard and is increasingly the best choice for new projects.

The most important differences are syntax and loading behavior. CommonJS uses `require()` and `module.exports`; ESM uses `import` and `export`. Once you understand how Node.js decides whether a file is CommonJS or ESM, most module errors become easier to diagnose.

If you are starting a new project, learn ESM first and keep CommonJS knowledge for maintenance and interoperability. Next, try converting a small Node.js file pair from CommonJS to ESM so you can practice the syntax and see the loading differences in action.
