← thecodex.expert · The Codex Family of Knowledge
The Codex / JavaScript / Modules (ESM)

Modules (ESM)

Organising JavaScript into reusable files — import, export, named vs default, and ESM vs CommonJS.

What it is, in plain English: A module is a JavaScript file that explicitly exports what it wants to share and imports what it needs from other files. Modules solve the problem of large programs becoming one tangled mess — they let you split code into focused, reusable pieces. ES Modules (ESM) is the standard: use 'export' to share, 'import' to use. Node.js also supports CommonJS (require/module.exports) — understanding both is essential for working in the JS ecosystem.
Where this bites you

In Node's ES modules, an import path is a URL, not a search. import { add } from "./math" doesn't try ./math.js, ./math/index.js or anything else: Node looks for a file named exactly math, finds nothing, and stops before running a single line. It catches people because bundlers such as webpack and Vite, and many TypeScript setups, do fill in the extension, so the same import works in one project and fails in the next.

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

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

// app.js
import { add } from "./math";      // no extension
console.log(add(2, 3));

// $ node app.js
// Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../math' imported from .../app.js
// Did you mean to import "./math.js"?

Splitting code into reusable files

As programs grow, putting everything in one file becomes unmanageable. Modules let you split code into focused files — one file for maths utilities, one for user-related functions, one for API calls — and connect them with import and export.

Exporting from a module

// math.js — a module that exports useful maths functions

// Named exports — you can have many:
export function add(a, b)      { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159265;

// Or export at the bottom (equivalent):
function divide(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}
export { divide };
// user.js — a module with a default export

// Default export — only ONE per module:
export default class User {
  constructor(name, email) {
    this.name  = name;
    this.email = email;
  }
  greet() { return `Hello, I'm ${this.name}`; }
}

// You can mix default and named exports:
export const ROLES = ["admin", "user", "viewer"];

Importing from a module

// app.js — using the modules above

// Named imports — curly braces, exact names:
import { add, multiply, PI } from "./math.js";

console.log(add(2, 3));           // 5
console.log(multiply(PI, 2));     // 6.28...

// Default import — no curly braces, any name you choose:
import User from "./user.js";

const alice = new User("Alice", "alice@example.com");
console.log(alice.greet());       // "Hello, I'm Alice"

// Import both default and named:
import User, { ROLES } from "./user.js";
console.log(ROLES);               // ["admin", "user", "viewer"]

// Import everything as a namespace object:
import * as Math from "./math.js";
console.log(Math.add(2, 3));      // 5
console.log(Math.PI);             // 3.14159265

// Rename on import (avoid naming conflicts):
import { add as mathAdd } from "./math.js";
import { add as vectorAdd } from "./vectors.js";

Using modules in the browser

<!-- Use type="module" on your script tag -->
<script type="module" src="app.js"></script>

<!-- Inline module -->
<script type="module">
  import { add } from "./math.js";
  console.log(add(1, 2));
</script>

Important: modules are always in strict mode automatically. They have their own scope — variables don't leak to the global window. They're deferred by default (like defer attribute).

Using modules in Node.js

// Option 1: use .mjs extension
// math.mjs — same export syntax as above

// Option 2: add "type": "module" to package.json
// {
//   "type": "module"
// }
// Then .js files use ESM syntax

// With "type": "module" in package.json:
// math.js uses: export function add...
// app.js uses:  import { add } from "./math.js"

That's why the Node.js example above writes "./math.js" in full. For relative imports, Node's ES module loader uses the path exactly as written, so the file name has to include its extension (and a folder import has to name its file, as in "./utils/index.js"). Recent versions of Node print a Did you mean hint, as shown above, but they still refuse to guess:

// app.js
import { add } from "./math.js";   // full file name, extension included
console.log(add(2, 3));

// $ node app.js
// 5
Try It Yourself

Official Sources

The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.