Lab 03: Modules & ESM

Time: 30 minutes | Level: Practitioner | Docker: docker run -it --rm node:20-alpine sh

Overview

Understand ES module system: named/default exports, imports, dynamic import(), namespace imports, re-exporting, and differences between ESM and CommonJS.


Step 1: Named Exports & Imports

// math.mjs — named exports
export const PI = 3.14159265358979;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export class Vector {
  constructor(x, y) { this.x = x; this.y = y; }
  magnitude() { return Math.sqrt(this.x ** 2 + this.y ** 2); }
  add(other) { return new Vector(this.x + other.x, this.y + other.y); }
}

// main.mjs — named imports
import { PI, add, multiply, Vector } from './math.mjs';
console.log(PI);                          // 3.14159...
console.log(add(2, 3));                   // 5
const v = new Vector(3, 4);
console.log(v.magnitude());               // 5

💡 Named exports are bound to the module's live bindings — if the value changes in the exporting module, importers see the change.


Step 2: Default Exports

💡 A module can have only ONE default export but unlimited named exports.


Step 3: Namespace Imports & Re-exports


Step 4: Dynamic import()

💡 Dynamic import() is perfect for code splitting and lazy loading in browsers.


Step 5: Module Resolution


Step 6: Circular Dependencies


Step 7: CommonJS vs ESM Differences

💡 Use "type": "module" in package.json to make all .js files use ESM.


Step 8: Capstone — Module-Based Utility Library

Run with ESM:

📸 Verified Output:


Summary

Feature
ESM
CommonJS

Syntax

import/export

require()/module.exports

Loading

Async, static analysis

Sync, dynamic

File extension

.mjs or .js with "type":"module"

.cjs or .js (default)

__dirname

dirname(fileURLToPath(import.meta.url))

Built-in

Dynamic

await import(specifier)

require(variable)

Tree shaking

✓ Yes (bundlers can optimize)

✗ No

Circular deps

Live bindings

Snapshot at require time

Last updated