ES Modules

Import/export syntax, dynamic imports, module patterns, and bundling

ES Modules (ESM)

ES Modules are the official, standardized module system for JavaScript. They provide a clean syntax for organizing code into reusable pieces with explicit imports and exports. ESM is supported in all modern browsers and Node.js.

Key Features

  • Static analysis — Imports/exports are determined at compile time
  • Strict mode — Modules are always in strict mode
  • Deferred execution — Module scripts are deferred by default
  • Single instance — Modules are singletons (cached after first load)

Named Exports

// math.js — Named exports
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// Can also export at the end
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;

export { subtract, divide };

// Rename on export
export { subtract as sub, divide as div };
// main.js — Named imports
import { add, multiply, PI } from "./math.js";

console.log(add(2, 3));      // 5
console.log(multiply(4, 5)); // 20
console.log(PI);             // 3.14159

// Rename on import
import { add as sum, multiply as mult } from "./math.js";

// Import all as namespace
import * as math from "./math.js";
console.log(math.add(1, 2));
console.log(math.PI);

Default Exports

// logger.js — Default export
export default function log(message) {
  console.log(`[LOG] ${message}`);
}

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

// Or export at the end
function log(message) {
  console.log(message);
}
export default log;
// main.js — Default import (any name works)
import log from "./logger.js";
import myLogger from "./logger.js"; // Same thing, different name

log("Hello!");

// Mix default and named
import log, { formatMessage, LogLevel } from "./logger.js";

Re-exporting

// utils/index.js — Barrel file (re-exports)
export { add, subtract } from "./math.js";
export { formatDate } from "./date.js";
export { default as Logger } from "./logger.js";

// Re-export everything
export * from "./math.js";

// Re-export with rename
export { add as sum } from "./math.js";
// main.js — Clean imports from barrel
import { add, formatDate, Logger } from "./utils/index.js";
// or
import { add, formatDate, Logger } from "./utils"; // With bundler

Dynamic Imports

// Static import (top of file, always loads)
import { heavyFunction } from "./heavy-module.js";

// Dynamic import (loads on demand, returns Promise)
async function loadModule() {
  const module = await import("./heavy-module.js");
  module.heavyFunction();
}

// Conditional loading
if (needsFeature) {
  const { feature } = await import("./feature.js");
  feature();
}

// Load based on user action
button.addEventListener("click", async () => {
  const { showModal } = await import("./modal.js");
  showModal();
});

// With error handling
try {
  const module = await import(`./locales/${language}.js`);
  applyTranslations(module.translations);
} catch (error) {
  console.error("Failed to load locale:", error);
}

Using Modules in HTML

<!-- Module script (deferred by default) -->
<script type="module" src="main.js"></script>

<!-- Inline module -->
<script type="module">
  import { greet } from "./utils.js";
  greet("World");
</script>

<!-- Fallback for older browsers -->
<script nomodule src="legacy-bundle.js"></script>

<!-- Preload modules for performance -->
<link rel="modulepreload" href="./utils.js">

CommonJS vs ES Modules

Feature CommonJS (CJS) ES Modules (ESM)
Syntax require() / module.exports import / export
Loading Synchronous Asynchronous
Analysis Dynamic (runtime) Static (compile time)
Tree shaking Difficult Built-in support
Browser Needs bundler Native support

Module Patterns

// Singleton pattern
let instance = null;

export function getInstance() {
  if (!instance) {
    instance = createInstance();
  }
  return instance;
}

// Factory with private state
const privateData = new WeakMap();

export class User {
  constructor(name) {
    privateData.set(this, { name });
  }
  
  getName() {
    return privateData.get(this).name;
  }
}

// Configuration module
export const config = Object.freeze({
  API_URL: "https://api.example.com",
  TIMEOUT: 5000,
  VERSION: "1.0.0"
});

// Side-effect only import
// analytics.js
console.log("Analytics loaded");
window.analytics = { track: () => {} };

// main.js
import "./analytics.js"; // Just runs the module

Node.js ESM

// package.json - Enable ESM for entire project
{
  "type": "module"
}

// Or use .mjs extension for ESM files
// utils.mjs

// Import JSON (requires assertion)
import data from "./data.json" with { type: "json" };

// Import from node_modules
import express from "express";
import { useState } from "react";

// Node built-ins
import fs from "node:fs";
import path from "node:path";

// __dirname equivalent in ESM
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

  

// import.meta
console.log(import.meta.url); // file:///path/to/module.js

💡 Key Takeaways

  • • Use named exports for multiple values, default for main export
  • • Dynamic import() for code splitting and lazy loading
  • • Barrel files (index.js) simplify imports
  • • ESM enables tree shaking for smaller bundles
  • • Modules are singletons and always use strict mode
  • • Use "type": "module" in package.json for Node.js ESM