Sitemap
Press enter or click to view image in full size
Diagram showing JavaScript module systems: AMD, CommonJS, UMD, and ES Modules with connected structure on a dark background

JavaScript Modules Explained: AMD, CommonJS, UMD & ES Modules

--

Learn why JavaScript needed modules—and how AMD, CommonJS, UMD, and ES Modules shaped modern JS development across browsers and Node.js.

There was a time when JavaScript files were just… scripts. Drop a <script> tag on a page, maybe two or three more, and hope nothing clashed. As web apps grew, so did the pain of managing code across multiple files—globals everywhere, name collisions, and zero code isolation.

This pain set the stage for something crucial: JavaScript modules.

The Early Days: From Script Soup to Structure

Before modules, developers resorted to patterns like IIFEs (Immediately Invoked Function Expressions) and namespacing to prevent global scope pollution:

// Poor man's module system
var MyApp = (function() {
var privateVar = 'secret';
return {
sayHello: function() {
console.log('Hello');
}
};
})();

It worked — but barely. We needed something native, scalable, and maintainable. That’s when the community started experimenting.

AMD (Asynchronous Module Definition)

AMD was born in the browser world, especially for use with script loaders like RequireJS.

jsdefine(['dep1', 'dep2'], function(dep1, dep2) {
return function() {
// your module here
};
});

Why it mattered:

  • Asynchronous loading was a win for performance.
  • Dependencies were declared up front.

But AMD came with boilerplate and verbosity. Try debugging an AMD-heavy codebase, and you’ll see why people moved on.

Unknown gem: AMD was actually inspired by CommonJS, even though AMD came first in the browser space.

CommonJS: The Node.js Revolution

While AMD targeted browsers, CommonJS (CJS) became the de facto standard in Node.js.

/ CommonJS syntax
const fs = require('fs');
module.exports = function readFile(path) {
return fs.readFileSync(path, 'utf-8');
};

It’s synchronous, making it great for the server (but a deal-breaker for browsers).

Get Srinithin’s stories in your inbox

Join Medium for free to get updates from this writer.

Cool detail: Node’s require() isn’t just an import—it’s a full function with caching, dynamic loading, and scope wrapping. That’s why require can be used conditionally and inside functions.

UMD: The Compatibility Hero

Then came UMD (Universal Module Definition) — a jack-of-all-trades format that worked everywhere.

(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.MyModule = factory(); // Global
}
})(this, function() {
return { hello: 'world' };
});

It’s not pretty, but it meant a single build could work in Node, RequireJS, or as a browser global. UMD was a bridge until something better came along.

ES Modules (ESM): Native, Declarative, Modern

With ES6, JavaScript finally got native modules:

// ES Module syntax
import { readFile } from './utils.js';
export function run() {
readFile();
}

The browser supports ESM natively now — with <script type="module">—and so does Node (with .mjs or "type": "module" in package.json).

But there are gotchas:

  • Top-level await is only allowed in modules.
  • Module specifiers must be relative or bare imports (no require.resolve() tricks).
  • Imports are static: You can’t conditionally import() without using dynamic import().

Bundlers vs. Bare Metal: If you’re using a bundler like Vite, Webpack, or Rollup:

  • ESM is often converted to CJS (or vice versa) depending on the target.
  • Code splitting, tree-shaking, and HMR (Hot Module Replacement) depend on modules being statically analyzable.

Without a bundler, native ESM is your best bet — but be ready to deal with CORS issues, absolute paths, and browser caching.

Wait — How Does Vite Handle All This?

If you’ve peeked into your dev console and seen things like __vite__createHotContext or __vite__cjsImport0_react_jsxDevRuntime, you're witnessing how Vite emulates a module environment.

  • __vite__createHotContext: Vite’s internal hook to enable HMR per module.
  • __vite__cjsImport0_*: Vite sometimes converts ESM to CJS or vice versa, inserting these helpers for compatibility.

Pro tip: Vite uses ESM in dev but can output different formats for production. You can even target legacy browsers with UMD if needed.

So… Which Module System Should You Use?

  • Browser only? Go with ESM.
  • Node-only? ESM is future-proof, but CJS is still widely used.
  • Library for others to consume? Build with ESM, then bundle to ESM + CJS + maybe UMD using tools like Rollup or tsup.

Final Thoughts

UMD was a bridge until something better came along.

JavaScript didn’t just “get” modules. It grew into them, adapting to the web’s evolution. Each format — AMD, CJS, UMD, ESM — was a response to a specific era’s needs.

Today, we’re lucky. We can write clean import/export code and let tools do the heavy lifting.

But understanding the past? That’s what separates a solid dev from a great one.

--

--

Srinithin
Srinithin

Full Stack Developer @Fountainhills Technologies