🎉 I'm releasing 12 products in 12 months! If you love product, checkout my new blog workingoutloud.dev

Back to home

Explaining CJS vs AMD vs UMD vs ESM

Introduction

Something that I think we can all agree on though is our mutual hurt for the module system. Since ES6 (or even before...), the pain has mainly been felt by library maintainers, with the majority of pain being abstracted behind bundlers and transpilers.

However, as the ecosystem has continued to grow (along with ways to support these module systems), it is no longer just library maintainers that feel the pain. It is now also developers who are trying to understand the differences between each module system, how to use them and wtf is happening when you see that dreaded ERR_REQUIRE_ESM message and attempt padding it over with a "type": "module" file extension in package.json to only find out it does not work and you do not truly understand why.

If you are yet to truly understand their differences, then do not despair: this post is for you (and me when I inevitably forget how it works again).

We will explore the module types, follow that up with understanding how package.json and file extensions affect Node.js and then finish on how bundlers have abstracted away the pain for us mere non-library maintainers for so long.

What Are CJS, AMD, UMD, and ESM?

CommonJS (CJS), Asynchronous Module Definition (AMD), Universal Module Definition (UMD) and ES Modules (ESM) are all module systems in JavaScript, allowing developers to organize and structure their code better.

Let's break down each, then demonstrate how to use them in Node.js and with (and without) bundlers like Webpack and Rollup.

CJS (CommonJS)

  • Introduced for use in Node.js.
  • Synchronous require and module.exports for importing/exporting.
  • Mainly for server-side.

For a CJS example:

const moduleA = require("moduleA"); module.exports = someFunction;

AMD (Asynchronous Module Definition)

  • For the browser, designed to load modules asynchronously.
  • Uses define and require.

For an AMD example:

define(["moduleA", "moduleB"], function (moduleA, moduleB) { return someFunction; });

UMD (Universal Module Definition)

  • Tries to unify CJS and AMD, making modules work in both client and server.
  • Checks the environment and then decides which system to use.

For a UMD example:

(function (root, factory) { if (typeof define === "function" && define.amd) { define(["moduleA"], factory); } else if (typeof exports === "object") { module.exports = factory(require("moduleA")); } })(this, function (moduleA) { return someFunction; });

ESM (ECMAScript Modules)

  • Native JavaScript module system introduced in ES6.
  • Uses import and export keywords.
  • Static module structure, meaning you can statically analyze the imports and exports without running the code.

For an ESM example:

import moduleA from "./moduleA"; export const someFunction = () => {};

Important Differences Between Each

  • Synchronous vs. Asynchronous: CJS is synchronous while AMD is asynchronous. UMD can handle both.
  • Environment: CJS is mainly for Node.js, AMD is for browsers, UMD is universal, and ESM is the new standard for both browser and server.
  • Syntax: Different keywords and approaches (e.g., require vs. import).
  • Static vs. Dynamic: ESM is static (better for tree shaking and optimization), while others like CJS are dynamic.

With the growing adoption of ES6 and beyond, ESM is becoming the most commonly used module system, especially with build tools and bundlers like Webpack and Rollup which can handle various module systems and transpile them to a chosen format.

Node.js: Using CJS or ESM

As you may have grokked from above, CJS comes from Node.js, and both UMD and ESM since have been approaches to unify the browser and server.

With the introduction of ESM in ES6, Node.js has been working on supporting it natively.

There are few ways to tell Node.js which module system to use. In Node.js, the module system (CommonJS vs. ESM) can be determined by either the file extension or by specifying the module type in the package.json file.

1. By File Extension

  • .mjs: By using the .mjs extension, you tell Node.js to treat the file as an ES module, regardless of other settings.
  • .cjs: This extension forces Node.js to treat the file as a CommonJS module.

2. Specifying "type" within package.json

  • "type": "module": By adding this line to your package.json, you inform Node.js to treat .js files in your project as ESM.

For example:

{ "type": "module" }

  • If you don't specify the "type": "module" entry (or if it's set to "commonjs"), then .js files are treated as CommonJS modules by default.

3. Dynamic Import

  • Regardless of the module system in place, you can always use dynamic imports in Node.js to load ESM:

This can be done as follows:

import("path/to/module.mjs").then((module) => { console.log(module); });

Important note: If you're using "type": "module" in your package.json, but still need to include a CommonJS file, you can use the .cjs extension for that specific file.

Using Bundlers Like Webpack and Rollup

When you're using bundlers like Webpack or Rollup, you generally don't need to specify the module type via file extension or package.json for the following reasons:

  1. Bundlers Handle Module Resolution: Tools like Webpack and Rollup have their own mechanisms to resolve and bundle modules. They can handle a mix of module types (CJS, ESM, AMD, etc.) and transpile them into a format of your choice.
  2. Configuration Files: These bundlers come with configuration files (webpack.config.js for Webpack and rollup.config.js for Rollup) where you define how modules are resolved, loaded, and bundled. You have extensive control over the bundling process, including specifying loaders, plugins, and output formats.
  3. Transpilers: Often, bundlers are used in tandem with transpilers like Babel. With Babel, you can use the latest ECMAScript features and transpile them down to a version compatible with your target environments. This also allows you to write in ESM and then compile to CJS or another format if needed.

Let's create a simple example where we have an ES module and want to bundle it using Webpack and Rollup to output it in a different format (e.g., CommonJS).

Directory Structure:

src/ |- index.js

In index.js:

export const greet = () => { console.log("Hello from the module!"); };

In our example above, the code is written in ESM. We want to bundle it using Webpack + Babel and Rollup + Babel and output it in CommonJS format to demonstrate how bundlers can handle module resolution and transpilation.

Example: Webpack

For a working Webpack example, we first need to install the necessary dependencies for Webpack and Babel and create a configuration file.

# Install Necessary Dependencies npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-env # Create a `webpack.config.js` in the root touch webpack.config.js

Inside of webpack.config.js:

const path = require("path"); module.exports = { entry: "./src/index.js", // This tells Webpack to output the bundle in CommonJS format into the `dist` folder as `bundle.js` output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), libraryTarget: "commonjs2", }, module: { rules: [ { // This targets all `.js` files in the project test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env"], }, }, }, ], }, };

Finally, you can run the bundler:

npx webpack

You'll get a bundle.js in the dist folder, which is in CommonJS format.

Example: Rollup

Like the previous Webpack example, we first need to install the necessary dependencies for Rollup and Babel and create a configuration file.

# Install Necessary Dependencies npm install --save-dev rollup @rollup/plugin-node-resolve @rollup/plugin-babel @babel/core @babel/preset-env # Create a `rollup.config.js` in the root touch rollup.config.js

Inside of rollup.config.js, we can put the following:

import babel from "@rollup/plugin-babel"; import resolve from "@rollup/plugin-node-resolve"; export default { input: "src/index.js", output: { file: "dist/bundle.js", format: "cjs", }, plugins: [ resolve(), babel({ babelHelpers: "bundled", presets: ["@babel/preset-env"] }), ], };

Finally, you can run the bundler:

npx rollup -c

Again, you'll get a bundle.js in the dist folder, which is in CommonJS format.

In both examples, we're using Babel to transpile our code. The configurations above will take the ES module from src/index.js, transpile it using Babel to make it compatible with older JavaScript versions, and bundle it in CommonJS format in the dist folder.

This example specifically demonstrated valid ESM code in a .js file and outputting it to valid CJS in another .js file, but you can use the same approach to bundle other module types (e.g., AMD, UMD, etc.) and output them in a format of your choice.

A Worthy Mention About Bundlers

However, there are a few things to keep in mind:

  • If you're developing a library or a package that you want to publish to npm, and you want it to be usable both in Node.js and in the browser, you might still consider specifying module types or providing both CJS and ESM builds.
  • Even if you're using a bundler, your Node.js specific code (like server-side code or scripts) that isn't being bundled might still require you to specify module types based on how you intend to use modules.

While using a bundler, the need to specify module type via file extension or package.json becomes less critical for the bundled output. However, depending on your project's structure and target environments, there might still be scenarios where you'd specify module types.

This may be why, if you've been using more recent versions of Node, you've seen .cjs or .mjs file extensions (particularly for configuration files), you may have been confused about how they fit into the process when there is still a bundler targets other files for transpiling (such as .ts, .tsx, or more confusingly, other .js files).

Conclusion

In this post, we covered the different module systems in JavaScript, how to use them in Node.js, and how bundlers like Webpack and Rollup can handle them.

It's not a fire blanket to douse the fire (the ecosystem is much bigger than this), but it will cover you up at night and help you sleep a little easier.

As Node.js continues to evolve, it's always a good idea to consult the official documentation to understand the current behavior and best practices regarding modules.

Finally, while not covered, understanding this will also help you as alternative JavaScript runtimes such as Deno and Bun implement the solutions around the module system and continue their march forward in this space.

References and Further Reading

Photo credit: pawel_czerwinski

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

Explaining CJS vs AMD vs UMD vs ESM

Introduction

Share this post