Understanding CJS and ESM Imports and Exports

As JavaScript has evolved, its module system has also improved. Two primary module formats dominate the ecosystem: CommonJS (CJS) and ECMAScript Modules (ESM). Understanding these two formats is crucial for developers, especially when working with modern Node.js or front-end frameworks. In this post, we'll dive deep into the differences between CJS and ESM, how to use them, and the implications of each.

Introduction to Modules in JavaScript

Modules are self-contained blocks of code that encapsulate functionality, making it reusable and maintainable. They allow developers to split their codebase into smaller, manageable parts. This modularity has become a cornerstone of modern JavaScript development.

Before modules, JavaScript developers had to rely on patterns like Immediately Invoked Function Expressions (IIFE) to create isolated scopes. However, the need for a standardized module system became evident as applications grew. This led to the development of CommonJS and later, ECMAScript Modules.

What is CommonJS (CJS)?

CommonJS is a module system introduced in 2009, primarily designed for server-side JavaScript with Node.js. It became the de facto standard for Node.js modules and is still widely used today.

Key Features of CJS:

  • Synchronous Loading: Modules are loaded synchronously, meaning the script execution waits until the module is fully loaded. This behavior is well-suited for server environments but less ideal for the browser.

  • Single Export Object: Each module returns a single object that contains all the exported variables and functions.

  • Dynamic Exports: The module's exports can be modified at runtime.

Example CJS Syntax:

// math.js
const add = (a, b) => a + b
const subtract = (a, b) => a - b

module.exports = {
  add,
  subtract
}

// main.js
const math = require('./math.js')
console.log(math.add(2, 3)) // Output: 5

What are ECMAScript Modules (ESM)?

ECMAScript Modules (ESM) is the official standard for modules in JavaScript, introduced in ES6 (ECMAScript 2015). Unlike CJS, ESM was designed to work both in browsers and server environments, making it a more versatile solution.

Key Features of ESM:

  • Asynchronous Loading: ESM modules are loaded asynchronously, better suited for browser environments where non-blocking behavior is essential.

  • Explicit Exports: Each export is explicitly defined, allowing for more granular control over a module's exposure.

  • Immutable Exports: Once a module is imported, its exports cannot be modified by the importing module.

  • Static Analysis: Since ESM uses static import and export statements, tools can perform better optimizations and analyses.

Example ESM Syntax:

// math.mjs
export const add = (a, b) => a + b
export const subtract = (a, b) => a - b

// main.mjs
import { add, subtract } from './math.mjs'
console.log(add(2, 3)) // Output: 5

Differences Between CJS and ESM

FeatureCommonJS (CJS)ECMAScript Modules (ESM)
LoadingSynchronousAsynchronous
Export Stylemodule.exports (single object)export (multiple named exports)
Import Stylerequire()import
SupportNode.js (default), not browser-nativeBrowser-native (modern), Node.js (with flag or native from v12)
Mutable ExportsYesNo
Dynamic ImportsLimitedSupported via import()
Default ExportsSupported (module.exports = ...)Supported (export default ...)

Using CJS

In CommonJS, modules are defined using module.exports and imported using require().

Exporting Modules:

// logger.js
const log = (message) => console.log(message)

module.exports = log

Importing Modules:

// app.js
const log = require('./logger.js')
log('Hello, CJS!')

In this example, logger.js exports a single function, which is then imported in app.js using require().

Using ESM

In ECMAScript Modules, modules are defined using export statements and imported using import.

Named Exports:

// utils.mjs
export const formatDate = (date) => {
    // formatting logic
}

export const parseDate = (dateString) => {
    // parsing logic
}

Default Exports:

// config.mjs
const config = {
    apiEndpoint: 'https://api.example.com',
    timeout: 5000
}

export default config

Importing Modules:

// app.mjs
import { formatDate, parseDate } from './utils.mjs'
import config from './config.mjs'

console.log(config.apiEndpoint)

Here, formatDate and parseDate are named exports, while config is a default export. The import statement allows you to import both named and default exports seamlessly.

Interoperability Between CJS and ESM

One of the challenges in modern JavaScript development is ensuring compatibility between CJS and ESM modules, especially when working with legacy code or third-party libraries.

ESM can import CJS modules using a default import:

// app.mjs
import logger from './logger.cjs'
logger('This is a CJS module in ESM')

CJS can import ESM modules, but it requires using dynamic import():

// app.js
const config = import('./config.mjs')
  .then(module => {
    console.log(module.default.apiEndpoint)
  })
  .catch(err => console.error(err))

Best Practices for Choosing Between CJS and ESM

When to Use CJS:

  • Legacy Projects: If you're maintaining or extending an existing Node.js project that heavily uses CJS, it might be simpler to continue using it.

  • Node.js-specific Tools: If you're writing Node.js tools that don't need to be run in the browser, CJS might be more straightforward due to its synchronous nature.

When to Use ESM:

  • New Projects: If you're starting a new project, especially one that needs to be compatible with both Node.js and browsers, ESM is the way to go.

  • Modern Tooling: Modern build tools and frameworks (like Webpack, Babel, etc.) are optimized for ESM, providing better tree-shaking and static analysis.

  • Interoperability: ESM is the standard for modern JavaScript, making it easier to integrate with other modern libraries and tools.

As the JavaScript ecosystem continues to evolve, ESM is becoming the dominant standard, but CJS remains an important part of the landscape, especially in the Node.js community. Whether you're maintaining a legacy system or building the next big thing, knowing when and how to use these module systems is essential. Happy coding!

10
Subscribe to my newsletter

Read articles from Nirmal Sankalana directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nirmal Sankalana
Nirmal Sankalana