Home Module 13 ES6 Modules

1. Introduction

Before ES6 modules, all JavaScript files shared the same global scope — name collisions were common and code organisation was painful. ES6 introduced a native module system: each file is its own scope, and you explicitly declare what to share (export) and what to use from other files (import).

ES6 modules are the standard in every modern JavaScript project and framework (React, Vue, Node.js). Understanding them is essential.

2. Theory

2.1 Named exports

// math.js — a module that exports multiple things
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.14159265358979;

// Or export all at once at the bottom (preferred for readability)
function add2(a, b)      { return a + b; }
function subtract2(a, b) { return a - b; }
const E = 2.71828;

export { add2, subtract2, E };

2.2 Named imports

// main.js — import what you need from math.js
import { add, subtract, PI } from './math.js';

console.log(add(2, 3));      // 5
console.log(subtract(10, 4)); // 6
console.log(PI);              // 3.14159...

// Rename on import
import { add as addition, PI as pi } from './math.js';
console.log(addition(1, 2)); // 3

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

2.3 Default exports

// user.js — one default export per file
export default function createUser(name, email) {
  return { name, email, createdAt: new Date().toISOString() };
}

// Or as an expression
const helper = {
  format: str => str.trim().toLowerCase(),
  sanitize: str => str.replace(/[<>"']/g, '')
};
export default helper;

2.4 Default imports

// Import the default — any name you choose (no braces)
import createUser from './user.js';

const alice = createUser('Alice', 'alice@example.com');
console.log(alice.name); // 'Alice'

// Import both default and named in one statement
import createUser2, { validateEmail, formatName } from './user.js';

// Named and default can coexist in one file
// user.js:
export const MAX_NAME_LENGTH = 50;
export default function createUser(name) { ... }

2.5 Re-exporting — barrel files

// utils/index.js — re-export from sub-modules
export { add, subtract }  from './math.js';
export { formatDate }     from './date.js';
export { sanitize }       from './string.js';
export { default as http } from './http.js';

// Consumer — one clean import instead of many
import { add, formatDate, sanitize } from './utils/index.js';

2.6 Using modules in the browser

<!-- type="module" enables ES6 modules -->
<script type="module" src="./main.js"></script>

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

<!-- Module scripts: -->
<!-- - Are deferred by default (run after DOM parses) -->
<!-- - Have their own scope (no global leakage) -->
<!-- - Run in strict mode automatically -->
<!-- - Must be served from a server (no file:// for imports) -->

2.7 Dynamic import

// Static import runs at module load time
// Dynamic import runs on demand (code splitting)

// Static — always loaded
import { heavyModule } from './heavy.js'; // loaded immediately

// Dynamic — loaded only when needed
document.querySelector('#feature-btn').addEventListener('click', async () => {
  // Import only when button is clicked — saves initial load time
  const { initFeature } = await import('./features/chart.js');
  initFeature();
});

// Dynamic import returns a Promise — can use .then() too
import('./analytics.js')
  .then(module => module.init())
  .catch(err => console.error('Failed to load analytics', err));

2.8 Module scope and strict mode

// Modules are automatically in strict mode — no 'use strict' needed
// Top-level variables are NOT global
// this at top level is undefined in modules (not window)

// counter.js
let count = 0; // private to this module

export function increment() { count++; }
export function getCount()  { return count; }
// 'count' is not accessible from outside

// main.js
import { increment, getCount } from './counter.js';
increment();
increment();
console.log(getCount()); // 2
// console.log(count); // ReferenceError — count is not exported

2.9 CommonJS vs ES Modules (context)

// CommonJS (Node.js original — still common in older code)
const path = require('path');          // import
module.exports = { add, subtract };    // export

// ES Modules (modern standard — works in browser and Node.js)
import path from 'path';               // import
export { add, subtract };              // export

// Key differences:
// - ES modules are static (imports resolved at parse time)
// - CommonJS is dynamic (require can be in conditionals)
// - ES modules support tree-shaking (bundlers remove unused exports)
// - Node.js supports both — .mjs for ESM, .cjs for CommonJS

3. Real World Example

// api.js — API helper module
const BASE_URL = 'https://api.example.com';

async function request(path, options = {}) {
  const response = await fetch(`${BASE_URL}${path}`, {
    headers: { 'Content-Type': 'application/json', ...options.headers },
    ...options
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

export const api = {
  get:    path         => request(path),
  post:   (path, body) => request(path, { method: 'POST',   body: JSON.stringify(body) }),
  put:    (path, body) => request(path, { method: 'PUT',    body: JSON.stringify(body) }),
  delete: path         => request(path, { method: 'DELETE' })
};

// users.js — domain-specific module
import { api } from './api.js';

export async function getUsers()         { return api.get('/users'); }
export async function getUser(id)        { return api.get(`/users/${id}`); }
export async function createUser(data)   { return api.post('/users', data); }
export async function deleteUser(id)     { return api.delete(`/users/${id}`); }

// main.js — application entry point
import { getUsers, createUser } from './users.js';

const users = await getUsers();
console.log(users);

4. Code Example

<!-- index.html -->
<script type="module">
  // Simulated module system using inline modules
  // In a real project these would be separate .js files

  // Inline definition of what 'math.js' would export
  const mathModule = (() => {
    function add(a, b)      { return a + b; }
    function subtract(a, b) { return a - b; }
    function multiply(a, b) { return a * b; }
    function divide(a, b)   {
      if (b === 0) throw new Error('Division by zero');
      return a / b;
    }
    const PI = Math.PI;
    return { add, subtract, multiply, divide, PI };
  })();

  // Inline definition of what 'validator.js' would export
  const validator = (() => {
    const isEmail = str => /\S+@\S+\.\S+/.test(str);
    const isPhone = str => /^\+?[\d\s\-()]{7,}$/.test(str);
    const inRange = (n, min, max) => n >= min && n <= max;
    return { isEmail, isPhone, inRange };
  })();

  // Use them
  const { add, PI }           = mathModule;
  const { isEmail, inRange }  = validator;

  console.log(add(2, 3));                    // 5
  console.log(PI.toFixed(4));               // 3.1416
  console.log(isEmail('alice@example.com')); // true
  console.log(inRange(75, 0, 100));          // true

  // Real module import syntax (needs a server):
  // import { add, PI } from './math.js';
  // import { isEmail } from './validator.js';
</script>

5. Code Breakdown

IIFE module pattern

Since inline examples can't use real file imports, each "module" is wrapped in an IIFE ((() => { ... })()). This creates a private scope and returns a public interface — exactly mimicking what ES module files do.

Destructured imports

const { add, PI } = mathModule mirrors how named imports work: import { add, PI } from './math.js'. Only the symbols you destructure are accessible in local scope.

Real-world note

Real ES modules require a server (or a bundler like Vite/Webpack) — browsers block imports over file:// due to CORS. When you build with React, Vue, or any modern framework, the module system is handled automatically.

6. Common Mistakes

Mistake 1 — Forgetting type="module" in HTML

<!-- Bug — regular script tag, import statement causes SyntaxError -->
<script src="main.js"></script>

<!-- Fix -->
<script type="module" src="main.js"></script>

Mistake 2 — Importing without file extension

// In browsers — must include .js extension
import { add } from './math';    // fails in browser
import { add } from './math.js'; // works

// Bundlers (Webpack, Vite) resolve without extension — but not bare browsers

Mistake 3 — Mixing default and named import syntax

// utils.js exports:
export default function main() {}
export function helper() {}

// Wrong
import { main } from './utils.js';    // main is default, not named
import helper from './utils.js';       // helper is named, not default

// Correct
import main from './utils.js';         // default
import { helper } from './utils.js';   // named

// Both together
import main, { helper } from './utils.js';

Mistake 4 — Circular imports

// a.js: import { b } from './b.js'; export const a = ...;
// b.js: import { a } from './a.js'; export const b = ...;
// This creates a circular dependency — a may be undefined in b on first load
// Fix: extract shared code to a third file (c.js) that neither imports

7. Best Practices

  1. Prefer named exports over default exports — they're easier to refactor and autocomplete in editors.
  2. One concept per file — a file should do one thing (auth, api, validation).
  3. Create barrel files (index.js) to re-export from a directory — simplifies imports for consumers.
  4. Use dynamic import for heavy features that aren't needed immediately.
  5. Avoid circular imports — they indicate tangled dependencies that should be reorganised.
  6. Keep modules small and focused — large files are hard to test and understand.

8. Practice Exercise

  1. Create three separate JS files (use a local server or bundler): math.js (exports add, subtract, multiply, divide), string.js (exports capitalise, truncate, wordCount), and main.js (imports from both and uses them). Test with type="module".
  2. Create a barrel file utils/index.js that re-exports from both modules. Update main.js to import from utils/index.js only.
  3. Add a button that dynamically imports a chart.js module on click — the module exports a render() function. Log a message to confirm it loaded lazily.

9. Assignment

Structure a "Mini App" using ES6 modules.

  1. Create: data.js (mock data — exported array of objects), api.js (functions to read/create/update/delete from the data array), ui.js (functions to render data to the DOM), events.js (wire up all event listeners), main.js (entry point — imports and initialises everything).
  2. The app: a searchable list where users can add, edit, and delete items.
  3. Serve locally (Live Server extension in VS Code, or Python python -m http.server) to enable ES module imports.

Deliverable: A folder of .html and .js files.

10. Interview Questions

  1. What is an ES6 module?
    A JavaScript file with its own scope. Variables, functions, and classes defined in a module are not globally accessible — they must be explicitly exported to be used by other modules.
  2. What is the difference between named and default exports?
    A file can have multiple named exports, each with a specific name that importers must match (or rename with as). A file can have only one default export, which can be imported with any name chosen by the importer.
  3. What is tree shaking?
    A bundler optimisation that removes unused exports from the final bundle. It works because ES module imports/exports are static — the bundler can determine at build time which exports are actually used.
  4. What is a dynamic import?
    import() called as a function — it returns a Promise that resolves with the module. Unlike static imports (which load at startup), dynamic imports load the module on demand — useful for code splitting and lazy loading.

11. Additional Resources

  • MDN — JavaScript modules
  • javascript.info — Modules — multi-part deep dive
  • MDN — Dynamic import()
  • v8.dev — JavaScript modules