JavaScript Module Systems: require, import, .mjs & More

JavaScript Module Systems: require, import, .mjs & More

JavaScript once had no built-in way to split code across files — a limitation that spawned an entire ecosystem of competing module formats. Today, developers encounter require(), import, .mjs, .cjs, AMD, and UMD, often all in the same project. This guide demystifies every module system, explains when to use each, and maps out the clear path forward.

Why JavaScript Needed Module Systems

In the early days of the web, JavaScript was a scripting language meant for simple page interactions. As applications grew, developers stuffed everything into global variables — leading to naming collisions and unmaintainable “spaghetti” code [1]. The community responded by inventing module patterns outside the language itself: first Immediately Invoked Function Expressions (IIFEs) to create private scopes, then formal module specifications like AMD and CommonJS. Only in 2015 did JavaScript finally gain a native module system via the ES6 specification [6].

js module timeline

CommonJS (CJS) — The Node.js Workhorse

CommonJS was introduced in 2009 as the module system for server-side JavaScript, and it remains the default module format in Node.js to this day [3].

Syntax

// Exporting
const greet = (name) => `Hello, ${name}!`;
module.exports = { greet };

// Importing
const { greet } = require('./greet');
console.log(greet('World'));

Key Characteristics

  • Synchronous loadingrequire() blocks execution until the file is fully loaded, which is fine on a server but problematic in browsers [2].
  • Dynamic by nature — you can call require() inside if blocks, loops, or functions at runtime [1].
  • module.exports / exports — the exported value is a plain JavaScript object, assigned at runtime.
  • No tree-shaking — because exports are determined at runtime, bundlers cannot statically determine which parts are unused [4].

CommonJS is still the right choice when maintaining existing Node.js codebases or working with packages that haven’t shipped an ESM build [2].

AMD — Asynchronous Module Definition

AMD emerged around 2011 specifically to solve browser performance: unlike CommonJS, it loads dependencies asynchronously so the page doesn’t freeze [5].

// AMD define + require via RequireJS
define(['dependency'], function(dep) {
  return { hello: () => dep.greet() };
});

require(['myModule'], function(mod) {
  mod.hello();
});

AMD was popularised by the RequireJS loader. However, with ES Modules now providing native async loading in every browser, AMD is considered largely obsolete for new projects [6].

UMD — Universal Module Definition

In 2011, UMD arrived as a compatibility shim to bridge the gap between CommonJS (Node.js), AMD (browsers), and plain global scripts [5].

(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define([], factory);          // AMD
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory();   // CommonJS
  } else {
    root.myLib = factory();       // Global variable
  }
}(this, function() {
  return { version: '1.0' };
}));

Major libraries like Lodash, Underscore.js, Backbone.js, and Moment.js adopted UMD to be universally consumable [6]. Today, UMD is practically obsolete because ES Modules are natively supported everywhere — but you’ll still encounter it in older packages on npm.

ES Modules (ESM) — The Modern Standard

Introduced in ES2015 (ES6) and now supported natively in all modern browsers and Node.js ≥ 12, ESM is the official, standardised module system for JavaScript [4].

Syntax

// Named exports
export const add = (a, b) => a + b;
export const PI = 3.14159;

// Default export
export default class Calculator { /* ... */ }

// Importing
import Calculator, { add, PI } from './math.js';

Key Characteristics

  • Static analysisimport and export statements must be at the top level, enabling tools like Webpack and Rollup to perform tree-shaking (removing dead code) [3].
  • Asynchronous loading — the browser or Node.js fetches and parses modules without blocking [1].
  • Live bindings — imported values are live references, not copies, so changes in the exporting module are reflected in the importer [8].
  • Top-level await — ESM files can use await outside async functions, a feature CJS does not support [2].
  • Explicit file extensions — relative imports must include the extension (e.g., ./utils.js) [4].

Dynamic import() — Lazy Loading on Demand

The import() operator (dynamic import) is a function-like expression that loads an ES module asynchronously and returns a Promise [7]. It works in both ESM and CJS contexts.

// Load a module only when the user clicks a button
button.addEventListener('click', async () => {
  const { Chart } = await import('./chart.js');
  new Chart(data).render();
});

Common Use Cases

  • Code splitting — ship only the JavaScript a user currently needs
  • Conditional loading — load a polyfill only in older browsers
  • Runtime path construction — build module paths from variables
  • CJS ↔ ESM bridge — CommonJS code can import an ESM package using await import() [8]

Dynamic imports are fully supported in all modern browsers and Node.js [7].

File Extensions: .js, .mjs, and .cjs

The extension you choose tells Node.js (and your bundler) which module system to use [4][9].

ExtensionModule SystemWhen to Use
.jsDetermined by package.json "type" fieldDefault; follows project-wide setting
.mjsAlways ES ModuleForce a single file to be ESM, regardless of package.json
.cjsAlways CommonJSForce a single file to be CJS inside an ESM-first project

The package.json "type" Field

// All .js files treated as ES Modules
{ "type": "module" }

// All .js files treated as CommonJS (default)
{ "type": "commonjs" }

Setting "type": "module" in package.json makes every .js file in that package an ES module [4]. You can override per-file using .mjs (force ESM) or .cjs (force CJS) extensions regardless of the "type" setting [9].

CJS vs ESM — Full Comparison

FeatureCommonJS (CJS)ES Modules (ESM)
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
Where it runsNode.js (natively)Browser + Node.js
Tree-shaking❌ Not possible✅ Supported
Dynamic importsrequire() anywhereimport() expression
Top-level await
Live bindings❌ (copy at load time)
File extension.cjs or .js.mjs or .js
StatusLegacy (still widely used)Modern standard

Interoperability: Mixing CJS and ESM

Node.js lets both systems coexist with important rules [8]:

  • ESM can import CommonJS packages — Node.js wraps module.exports as the default export.
  • CJS cannot require() an ESM file — this throws an ERR_REQUIRE_ESM error.
  • CJS can load ESM using dynamic await import() as a workaround [8].

Which Module System Should You Use in 2026?

  • New projects → Use ES Modules ("type": "module" in package.json). ESM is the standard, enables tree-shaking, and works in both browsers and Node.js [1][2].
  • Existing Node.js codebasesCommonJS is still practical and well-supported; migrate incrementally.
  • Library authors → Ship dual packages (both CJS and ESM builds) via the "exports" field in package.json for maximum compatibility [3].
  • Legacy browser support → Use a bundler (Vite, Webpack, Rollup) that converts ESM to the target format; AMD/UMD are no longer necessary [6].

The JavaScript module landscape has converged: ESM is the clear winner for new code, but understanding CJS (and even AMD/UMD) is essential for navigating the vast npm ecosystem built on those older foundations.

Sources

  1. CommonJS vs ES Modules in JavaScript — Syncfusion Blogs
  2. CommonJS vs. ES Modules — Better Stack Community
  3. CommonJS vs. ES Modules in Node.js — LogRocket Blog
  4. ECMAScript Modules — Node.js Official Documentation
  5. What the heck are CJS, AMD, UMD, and ESM in Javascript? — DEV Community
  6. Navigating the module maze: History of JavaScript module systems — Codilime
  7. import() — MDN Web Docs
  8. A Deep Dive Into CommonJS and ES Modules in Node.js — AppSignal Blog
  9. What are .mjs, .cjs, .mts, and .cts extensions? — Total TypeScript
  10. Understanding MJS and CJS — RGB Studios