Javascript

Node.js Module Systems Demystified: CommonJS and ES Modules

In this guide, we’ll focus on understanding how CommonJS and ES Modules work in Node.js, and how the "type" field in package.json helps define which module system to use.

Node js-module-system-demystified-common-js-and-es-modules.webp

Node.js has long relied on the CommonJS module system, which has been the backbone of how modules are structured and consumed. However, with the rise of ECMAScript Modules (ESM) as the standard for JavaScript modules, Node.js has introduced support for ES Modules alongside CommonJS. This evolution has brought more flexibility but also some nuances when it comes to module systems in Node.js.

CommonJS: The Traditional Node.js Module System

In Node.js, CommonJS has been the default module system since its inception. It uses synchronous require() statements to import modules and module.exports to expose functionalities. Here’s a quick breakdown of the basic elements of CommonJS:

Importing with require()

The require() function is used to load modules in CommonJS. When you require a module, Node.js synchronously loads and executes it.

// math.js (module definition) const add = (a, b) => a + b; const subtract = (a, b) => a - b; module.exports = { add, subtract };
// app.js (module usage) const math = require('./math'); console.log(math.add(3, 5)); // Output: 8

Exporting with module.exports

In CommonJS, you use module.exports to define what a module exposes. This can be an object, function, or any other value.

// logger.js module.exports = function(message) { console.log(message); };
// app.js const logger = require('./logger'); logger('Hello, Node.js'); // Output: Hello, Node.js

CommonJS operates synchronously, meaning the module is loaded and executed at runtime when it is required. This works well for server-side applications where blocking operations are acceptable, especially during the startup phase.

ES Modules (ESM): The New Standard

ES Modules (ESM) were introduced as part of the ECMAScript standard, and they use import and export keywords for module handling. Unlike CommonJS, ES Modules are loaded asynchronously and statically analyzed, which means they are more suitable for both client-side and server-side applications.

Importing with import

ESM uses the import keyword to load modules, which allows for more flexible and modern syntax.

// math.mjs (module definition) export const add = (a, b) => a + b; export const subtract = (a, b) => a - b;
// app.mjs (module usage) import { add, subtract } from './math.mjs'; console.log(add(3, 5)); // Output: 8

Exporting with export

ESM provides two types of exports:

  • Named Exports: You export specific functions or variables.

    export const multiply = (a, b) => a * b;
  • Default Exports: You can export a single value or function as the default export.

    export default function greeting() { return 'Hello, World!'; }

Dynamic Imports

ESM also allows for dynamic imports, which enable you to load modules asynchronously during runtime using import().

// app.mjs import('./math.mjs').then(module => { console.log(module.add(3, 5)); // Output: 8 });

type Field in package.json

With Node.js supporting both CommonJS and ES Modules, managing which module system to use in a project can get tricky. That’s where the "type" field in package.json comes in. This field allows you to specify whether your project should treat .js files as CommonJS or ES Modules by default.

Setting type to CommonJS

By default, Node.js treats all .js files as CommonJS unless specified otherwise. If your project is primarily using CommonJS, you don’t need to explicitly define the "type" field, but if you want to make it clear, you can set it like this:

{ "name": "my-app", "version": "1.0.0", "type": "commonjs" }

With "type": "commonjs", any .js file will be treated as a CommonJS module:

// app.js (treated as CommonJS) const math = require('./math'); console.log(math.add(3, 5)); // Works as CommonJS

Setting type to Module

To make your project use ES Modules by default, you can set the "type" field to "module" in package.json. This tells Node.js to treat .js files as ES Modules.

{ "name": "my-app", "version": "1.0.0", "type": "module" }

Now, any .js file will be treated as an ES Module:

// app.js (treated as ES Module) import { add } from './math.js'; console.log(add(3, 5)); // Works as ES Module

If you want to mix both module systems, you can use the .mjs extension for ES Modules and .cjs for CommonJS, even within the same project. Node.js will respect these extensions regardless of the "type" field.

Mixed Mode Example

// math.mjs (ES Module) export const add = (a, b) => a + b;
// logger.cjs (CommonJS Module) module.exports = function(message) { console.log(message); };

Key Differences Between CommonJS and ES Modules

  1. Synchronous vs. Asynchronous:
    CommonJS modules are loaded synchronously, meaning the module is fully executed when require() is called. ES Modules, on the other hand, are loaded asynchronously, allowing them to be statically analyzed.

  2. Dynamic Imports:
    While ES Modules support dynamic imports (import()), CommonJS does not have an equivalent built-in feature.

  3. Exports Syntax:
    CommonJS uses module.exports to export functionality, whereas ES Modules use export or export default.

  4. Scope of Variables:
    In CommonJS, every file/module has its own scope. In ES Modules, imports are live bindings, meaning if the imported value changes in the original module, it will be updated in the importing module.

Migration Strategies

If you’re considering moving from CommonJS to ES Modules, here are a few tips:

  1. Gradual Migration:
    You don’t need to migrate all files at once. You can mix .cjs and .mjs extensions within the same project to transition gradually.

  2. Switching to "type": "module":
    If your project is small, consider switching to "type": "module" in your package.json and updating your imports/exports syntax.

  3. Tooling and Compatibility:
    Most modern tooling, such as Webpack and Babel, now supports both CommonJS and ES Modules. Ensure your build process is compatible with ESM if you decide to migrate.

Conclusion

The introduction of ES Modules into Node.js provides a more modern and standard approach to module management. While CommonJS remains widely used and perfectly viable, ES Modules offer additional flexibility with features like static imports and dynamic import() statements. The "type" field in package.json simplifies working with both systems, giving developers the choice of which module system to use by default.

Whether you stick to CommonJS or embrace ES Modules, understanding both systems allows you to build efficient, scalable Node.js applications and make informed decisions as the ecosystem evolves.