JavaScript Modules

From script tags to ES Modules — organizing code across files for maintainable applications

Last updated on

Modules let you split code into separate files and share functionality between them. Modern JavaScript uses ES Modules (ESM).

The Evolution

Script tags (global scope) → IIFE pattern → CommonJS (Node) → ES Modules (standard)

ES Modules (The Standard)

Named Exports

// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export const PI = 3.14159;
// app.js
import { add, subtract, PI } from "./math.js";

add(2, 3);    // 5
subtract(5, 3); // 2

Default Export

One per file — the "main" thing:

// Logger.js
export default class Logger {
  log(msg) { console.log(msg); }
}

// app.js
import Logger from "./Logger.js"; // name can be anything
const logger = new Logger();

Renaming Imports

import { add as sum } from "./math.js";
sum(1, 2); // 3

Re-exports (Barrel Files)

// utils/index.js
export { add, subtract } from "./math.js";
export { formatDate } from "./date.js";
export { default as Logger } from "./Logger.js";

// app.js — import everything from one place
import { add, formatDate, Logger } from "./utils/index.js";

Dynamic Import (Code Splitting)

// Load module only when needed
const button = document.querySelector("#heavy-feature");

button.addEventListener("click", async () => {
  const { heavyFunction } = await import("./heavy-module.js");
  heavyFunction();
});

CommonJS (Node.js)

// math.js
function add(a, b) { return a + b; }
module.exports = { add };

// app.js
const { add } = require("./math");
add(2, 3); // 5

ESM vs CommonJS

FeatureES ModulesCommonJS
Syntaximport/exportrequire/module.exports
LoadingStatic (can be analyzed)Dynamic (runtime)
AsyncTop-level await ✅No
Tree shakingYesNo
BrowserYes (native)No (needs bundler)
Node.js.mjs or "type": "module"Default

Using ESM in Node.js

// package.json
{
  "type": "module"
}

Module Scope

Variables in modules are scoped to the file — NOT global:

// config.js
const SECRET = "abc123"; // not accessible from other files
export const PUBLIC_KEY = "xyz"; // accessible via import

Circular Dependencies

// a.js
import { b } from "./b.js";
export const a = "A";

// b.js
import { a } from "./a.js";
export const b = "B";
// Works in ESM (evaluates partially), but avoid when possible

Best practice: Restructure to avoid circular imports.

On this page