Modules in NodeJS : ESM vs CommonJS
Tags: javascript, node, tutorial
Published: 2023-05-04 07:59:01

## NodeJS Module Types :

NodeJS has three module types :

- Core Modules : These are built-in modules that are part of Node.js distribution and are availabe globally without needing to install any additional distribution. Example : fs, http, etc.
- Local Modules : Custom modules written by developers and saved as individual files.
- Third-Party Modules : Written by external developers, installed and managed by Node Package Manager (NPM).

## NodeJS Module Systems :

Node provides us with 2 module system to work with modules :

- CommonJS : It is the original way to work with modules using the `require()` syntax to import and `module.exports` to export modules.
- ESM (ECMAScript modules): Newer module system introduced in es6. It uses `import` to import modules and `export` to export modules.

Module Importing Priority : core modules > third party modules (node_modules) > local modules

## CommonJS :

### Importing :

In CommonJS syntax, we import using the `require()` function. When the require function is invoked, Node goes through the following steps :

- Resolving
- Loading
- Wrapping : Code inside the module is wrapped in a special wrapper which gives the encapsulation and provides with some special properties :
  ```js
  (function (exports, require, module, __filename, __dirname) {
    // Module code lives in here
  });
  ```
- Evaluating : Node executes the code within the wrapeer and exports function and variables mentioned in module.exports
- Caching : Modules are cached after loading for the first time. So, any later loading of the module simply returns the cached value without executing it again. Caching will be discussed in details in a later section.

By wrapping the code in this wrapper, Node.js achieves a few things :
- It keeps top-level variables (defined with `var`, `const`, or `let`) scoped to the module rather than the global object.
- It helps to provide some global-looking variables that are actually specific to the module, such as: `__filename` and `__dirname`

### Exporting :

In CommonJS, we can export variables or functions with the `export` property of the `module` object provided by the wrapper.

- For **default export**, we can set our `module.export` directly to whatever we want to export. Also, In the importing file, we don't need to match the name of the exported variable/function, as we can have only one default export per module.

  ```js
  // my-module.js

  function hello(name) {
    console.log(`Hello, ${name}!`);
  }

  module.exports = hello;
  ```

  ```js
  // app.js

  const whateverYouNameIt = require("./my-module");

  hello("world");
  ```

  Logging the module object in `my-module.js` gives us:

  ```js
  Module {
    id: '.',
    path: '/home/user/Lessons/nodejs',
    exports: [Function: hello],
    filename: '/home/user/Lessons/nodejs/module.js',
    loaded: false,
    children: [],
    paths: [
      ...
    ]
  }
  ```

- For **named export**, we can bind all our exportable variables/functions to the `module.exports` object. Then we can destructure to use specific ones.

  ```js
  // my-module.js

  function hello(name) {
    console.log(`Hello, ${name}!`);
  }

  module.exports = {
    hello,
  };
  ```

  or,

  ```js
  // my-module.js

  module.exports.hello = function (name) {
    console.log(name);
  };
  ```

  ```js
  // app.js

  const { hello } = require("./my-module");

  hello("world");
  ```

  Logging the module object in `my-module.js` gives us:

  ```js
  Module {
    id: '.',
    path: '/home/user/Lessons/nodejs',
    exports: { hello: [Function: hello] },
    filename: '/home/user/Lessons/nodejs/module.js',
    loaded: false,
    children: [],
    paths: [
      ...
    ]
  }
  ```

### exports vs module.exports :

From the wrapper, we get a `exports` object, this is just a reference to the `module.exports` object. So, whatever we add to the `export` object, gets added to the `module.exports` as well. But if we reassign `export` with some variable/function in hope of default exporting, the link is broken, and `module.exports` will return an empty object.

## ECMAScript Modules :

### Importing :

To use import syntax, we need to set `"type" : "module"` in `package.json`.

Different than CommonJS module, in ESM module,
`import {myFunction} from "./circle.js"`
and,
`import {myFunction} from "./circle"`
isn't the same.

because, in ESM we can import other type of modules beside JS files like JSON or CSS files, and specifying the extension is necessary to disambiguate between them.

When import is invoked, Node goes through the following steps :

- Resolving : The resolving algorithm is different than that of CommonJS.
- Loading
- Wrapping : There is no wrapper for ES modules. they are native to the underlying JS engine. Specifically, ECMAScript modules have their own module scope, which means that the variables and functions defined in an ECMAScript module are not global.
- Evaluating: Node executes the code within the module.
- Caching: Modules are cached after loading for the first time. So, any later loading of the module simply returns the cached value without executing it again.

### Exporting :

In ESM, we have `export` for named exports and `export default` for default exports.

```js
// my-module.js

export function hello(name) {
  console.log(`Hello, ${name}!`);
}

export default function logger(message) {
  console.log(message);
}
```

```js
// app.js

import whateverYouNameIt, { hello } from "./my-module.js";

whateverYouNameIt("custom message"); // calls the logger function, we can name it anything because of it being a default export

hello("world");
```

We can have multiple exports from a module, but only one default export.

## Module caching :

### Caching :

When loading a module, NodeJS executes the code once and caches the result. On subsequent imports from same or other modules, it returns the cached results.

**Caching example in CommonJS** :

Let's do a little fun experiment to see this in action.

In `one.js` :

```js
// We generate the current timestamp and add 5 to it
// so, each time we execute this following code, we will have different results

console.log("one.js execution started");

let timestamp = Date.now();
let number = 5;

let sum = timestamp + number;

module.exports = {
  sum,
};
```

In `two.js` :

```js
// This file imports the time dependant sum
// but executes after a 5 seconds delay
// We export a dummy variable from this file to reference in three.js

console.log("two.js execution started");

console.log("Loop started");
const startTime = Date.now();
while (Date.now() < startTime + 5000) {}
console.log("Loop finished after 5 seconds");

const { sum } = require("./one");

console.log(`Sum within two.js is ${sum}`);

let dummy = 8;

module.exports = {
  dummy,
};
```

In `three.js` :

```js
// This file imports both one.js and two.js

console.log("three.js execution started");

const { sum } = require("./one");

console.log(`Sum within three.js is ${sum}`);

// two.js is imported after getting the sum from one.js

const { dummy } = require("./two");
```

**Intuitively,** when we run `three.js` with `node three.js`, we expect it:

1. Log "three.js execution started"
2. Import the `one.js` module and execute its code
3. Log "Sum within three.js is (current timestamp + 5)" with the current time of execution.
4. Importing `two.js` module starts the 5 seconds loop and then requires the `one.js` file to get the sum. So, technically, timestamp is in seconds, so for five seconds delay, we can expect some difference
5. The log within `two.js` should generate a sum greater than the one logged in `three.js` due to difference in execution time.

So, log output should be something like this :

```
three.js execution started
one.js execution started
Sum within three.js is 1683184022476
two.js execution started
Loop started
Loop finished after 5 seconds
Sum within two.js is 1683184022481
```

**But, what we really get is this :**

```
three.js execution started
one.js execution started
Sum within three.js is 1683184022476
two.js execution started
Loop started
Loop finished after 5 seconds
Sum within two.js is 1683184022476
```

Even with the five seconds loop, we get the same value for sum which is dependant on time !  
So, by now we should be clear that NodeJS when importing a module for the first time, executes the code once and caches the result for future imports.

**Caching example in ESM** :

```js
// my-module.js

let count = 0;

console.log("Module loaded");

export function increment() {
  count++;
}

export function getCount() {
  return count;
}
```

```js
// app.js

import { increment, getCount } from "./my-module.js";

console.log("Import 1");
increment();
console.log(getCount()); // 1

console.log("Import 2");
increment();
console.log(getCount()); // 2
```

```js
//some-other-file.js

// Import the module again from a different file
import { getCount } from "./my-module.js";
console.log(getCount()); // 2 (the same value as the previous call to getCount())
```

References used :

- [Default vs Named Exports](https://betterprogramming.pub/understanding-the-difference-between-named-and-default-exports-in-react-2d253ca9fc22)
- [exports vs module.exports](https://www.scaler.com/topics/nodejs/module-exports-vs-exports/)

More details on the underlying workings can be found at NodeJS specification :

- [Node CommonJS Modules](https://nodejs.org/api/modules.html)
- [Node ECMAScript Modules](https://nodejs.org/api/esm.html)