Day 24: Modules

 

Day 24: Modules


1. Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what ES6 modules are and why they matter
  • Export values using export and export default
  • Import values using import with named and default imports
  • Rename imports and exports using as
  • Re-export values from other modules
  • Understand the difference between static and dynamic imports
  • Organize code into separate module files with clear responsibilities
  • Use modules effectively in both browser and Node.js environments

2. Theory

What are Modules?

Modules are reusable pieces of code that can be exported from one file and imported into another. They help you:

  • Organize code into logical, separate files
  • Encapsulate implementation details (keep things private)
  • Avoid naming conflicts — each module has its own scope
  • Manage dependencies — explicitly declare what you need

ES6 Module Syntax

JavaScript modules use two main keywords: export and import.

Exporting from a Module

// ---- Named Exports ----
// Export individual features
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Circle { }

// Export at the end
const E = 2.71828;
function subtract(a, b) { return a - b; }
export { E, subtract };

// Export with alias
function multiply(a, b) { return a * b; }
export { multiply as mul };

// ---- Default Export ----
// Each module can have ONE default export
export default function divide(a, b) {
    if (b === 0) throw new Error("Division by zero");
    return a / b;
}

// Default export with class
export default class Calculator { }

// Default export with expression
export default { name: "MyModule", version: "1.0" };

Importing into a Module

// ---- Named Imports ----
import { PI, add, Circle } from "./math.js";

// Import with alias
import { add as sum } from "./math.js";

// Import all named exports as a namespace object
import * as MathUtils from "./math.js";
console.log(MathUtils.PI);         // 3.14159
console.log(MathUtils.add(2, 3));  // 5

// ---- Default Import ----
import divide from "./math.js";
console.log(divide(10, 2));  // 5

// Default + named imports together
import divide, { PI, add } from "./math.js";

Key Rules of ES6 Modules

RuleExplanation
Strict mode by defaultModules automatically run in strict mode ("use strict")
Static structureImports/exports are resolved at parse time, not runtime
Live bindingsExported values are live bindings (changes in the exporting module are reflected in importing modules)
Single default exportOnly one export default per module
Top-level onlyimport and export must be at the top level, not inside blocks or functions
// Static vs Dynamic
// Static (correct):
import { add } from "./math.js";

// Dynamic (also possible — see below):
if (condition) {
    const module = await import("./math.js"); // Dynamic import
}

Module Scope and Encapsulation

Variables declared at the top level of a module are scoped to that module — they are not visible outside unless exported.

// ---- math-utils.js ----
// Private — not accessible from outside
const internalCounter = 0;
function privateHelper() {
    console.log("This is private");
}

// Public — exported
export function publicFunction() {
    privateHelper(); // Can use internal helpers
    return internalCounter;
}

export const VERSION = "1.0.0";

Renaming Imports and Exports

// ---- Exporting with alias ----
function calculateTotal(price, tax) { return price + price * tax; }
export { calculateTotal as total };

// ---- Importing with alias ----
import { total as calcTotal } from "./utils.js";
console.log(calcTotal(100, 0.08)); // 108

Re-exporting

You can import and then immediately export — useful for creating "barrel" files that aggregate multiple modules.

// ---- barrel.js ----
// Import and re-export
export { default as Button } from "./Button.js";
export { default as Input } from "./Input.js";
export { default as Checkbox } from "./Checkbox.js";

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

// Re-export with filtering (specific named exports)
export { add, subtract } from "./math.js";

Dynamic Imports

Static imports are evaluated at parse time. Dynamic imports use import() as a function, returning a Promise — useful for:

  • Code splitting / lazy loading
  • Loading modules conditionally
  • Loading modules based on user interaction
// Dynamic import
button.addEventListener("click", async () => {
    const module = await import("./heavy-component.js");
    module.render();
});

// Conditionally load based on environment
let config;
if (process.env.NODE_ENV === "production") {
    config = await import("./config.prod.js");
} else {
    config = await import("./config.dev.js");
}

Browser vs Node.js

EnvironmentFile ExtensionModule TypeNotes
Browser.js, .mjsES6 modulesUse <script type="module" src="app.js">
Node.js (>= 12).mjsES6 modulesSet "type": "module" in package.json
Node.js (legacy).jsCommonJSUses require() and module.exports
<!-- Browser: Load as module -->
<script type="module" src="app.js"></script>

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

Note: type="module" automatically enables strict mode, defers script execution, and prevents top-level variables from leaking to the global scope.


3. Code Examples

Example 1: Basic Module Structure

// ---- math.js ----
// Named exports
export const PI = 3.14159;
export const E = 2.71828;

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

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

// Private (not exported)
function privateHelper() {
    console.log("This is not accessible outside");
}

// Default export
export default function calculate(operation, a, b) {
    switch (operation) {
        case "add": return add(a, b);
        case "subtract": return subtract(a, b);
        default: throw new Error(`Unknown operation: ${operation}`);
    }
}

// ---- app.js ----
// Import named exports
import { PI, add, subtract } from "./math.js";

// Import default export
import calculate from "./math.js";

// Import both
import calculate, { PI, add as sum } from "./math.js";

// Import all as namespace
import * as MathUtils from "./math.js";

console.log(add(5, 3));          // 8
console.log(PI);                  // 3.14159
console.log(calculate("add", 5, 3)); // 8
console.log(MathUtils.E);         // 2.71828

Example 2: Module Organization Pattern

// ---- utils/validators.js ----
export function isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export function isRequired(value) {
    return value !== null && value !== undefined && value.trim() !== "";
}

export function isInRange(value, min, max) {
    return value >= min && value <= max;
}

// Private helper
function sanitizeString(str) {
    return str.trim().toLowerCase();
}

// ---- utils/formatters.js ----
export function formatCurrency(amount, currency = "USD") {
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency
    }).format(amount);
}

export function formatDate(date) {
    return new Intl.DateTimeFormat("en-US", {
        year: "numeric",
        month: "long",
        day: "numeric"
    }).format(date);
}

// ---- utils/helpers.js ----
export function capitalize(str) {
    if (!str) return "";
    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

export function truncate(str, maxLength) {
    if (str.length <= maxLength) return str;
    return str.slice(0, maxLength) + "...";
}

export function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

// ---- utils/index.js (Barrel File) ----
// Re-export everything from all utility modules
export * from "./validators.js";
export * from "./formatters.js";
export * from "./helpers.js";

// ---- app.js ----
// Import everything through the barrel
import { isValidEmail, formatCurrency, capitalize, randomInt } from "./utils/index.js";

console.log(isValidEmail("alice@example.com")); // true
console.log(formatCurrency(99.99));             // $99.99
console.log(capitalize("hello world"));         // Hello world
console.log(randomInt(1, 10));                  // e.g., 7

Example 3: Default Export Patterns

// ---- calculator.js ----
// Default export as a class
export default class Calculator {
    constructor() {
        this.history = [];
    }

    add(a, b) { this.#log("add", a, b); return a + b; }
    subtract(a, b) { this.#log("subtract", a, b); return a - b; }
    multiply(a, b) { this.#log("multiply", a, b); return a * b; }
    divide(a, b) {
        if (b === 0) throw new Error("Division by zero");
        this.#log("divide", a, b);
        return a / b;
    }

    #log(operation, a, b) {
        this.history.push({ operation, a, b, result: eval(`${a} ${operation === "add" ? "+" : operation === "subtract" ? "-" : operation === "multiply" ? "*" : "/"} ${b}`), timestamp: new Date() });
    }

    getHistory() { return [...this.history]; }
}

// ---- config.js ----
// Default export as an object
export default {
    appName: "MyApp",
    version: "1.0.0",
    apiUrl: "https://api.example.com",
    debug: true,
    features: {
        darkMode: true,
        notifications: false
    }
};

// ---- main.js ----
import Calculator from "./calculator.js";
import config from "./config.js";

const calc = new Calculator();
console.log(calc.add(10, 5));        // 15
console.log(calc.divide(10, 2));     // 5

console.log(config.appName);         // "MyApp"
console.log(config.features.darkMode); // true

// Default export can be imported with any name
import MyCalc from "./calculator.js";
const c2 = new MyCalc();

Example 4: Dynamic Imports

// ---- heavy-component.js ----
export function render() {
    console.log("Rendering heavy component...");
    // Simulate heavy computation
    const element = document.createElement("div");
    element.textContent = "Heavy Component Loaded!";
    document.body.appendChild(element);
}

export const metadata = { size: "5MB", type: "chart" };

// ---- app.js ----
// Static imports for critical code
import { isValidEmail } from "./utils/validators.js";

// Dynamic import for code splitting
async function loadChartModule() {
    try {
        const chartModule = await import("./heavy-component.js");
        chartModule.render();
        console.log(`Loaded: ${chartModule.metadata.type} (${chartModule.metadata.size})`);
    } catch (error) {
        console.error("Failed to load chart module:", error.message);
    }
}

// Load on user interaction
document.getElementById("load-chart-btn").addEventListener("click", loadChartModule);

// Dynamic import with condition
async function loadTheme(themeName) {
    try {
        const theme = await import(`./themes/${themeName}.js`);
        theme.apply();
    } catch (error) {
        console.error(`Theme "${themeName}" not found.`);
    }
}

// Preload for performance (optional)
const chartPromise = import("./heavy-component.js");

// Later, await the already-loading promise
button.addEventListener("click", async () => {
    const chart = await chartPromise;
    chart.render();
});

Example 5: Module Pattern with Private State

// ---- counter.js ----
// Using module-level variables for private state
let count = 0;
const history = [];
const listeners = new Set();

function notifyListeners() {
    listeners.forEach(fn => fn(count));
}

export function increment(amount = 1) {
    count += amount;
    history.push({ action: "increment", amount, timestamp: new Date() });
    notifyListeners();
    return count;
}

export function decrement(amount = 1) {
    count -= amount;
    history.push({ action: "decrement", amount, timestamp: new Date() });
    notifyListeners();
    return count;
}

export function reset() {
    const prev = count;
    count = 0;
    history.push({ action: "reset", previousValue: prev, timestamp: new Date() });
    notifyListeners();
    return count;
}

export function getCount() {
    return count;
}

export function getHistory() {
    return [...history]; // Return a copy
}

export function subscribe(callback) {
    listeners.add(callback);
    // Return unsubscribe function
    return () => listeners.delete(callback);
}

export default { increment, decrement, reset, getCount, getHistory, subscribe };

// ---- app.js ----
import counter, { increment, getCount, subscribe } from "./counter.js";

// Subscribe to changes
const unsubscribe = subscribe((newCount) => {
    console.log(`Count changed to: ${newCount}`);
});

increment(5);  // Count changed to: 5
increment(3);  // Count changed to: 8
console.log(getCount()); // 8

unsubscribe(); // Stop listening

4. Exercises

Beginner

  1. Create a file greeting.js that exports a function sayHello(name) returning "Hello, [name]!". Import and use it in another file.
  2. Create a module constants.js that exports PI and E as named exports. Import them in another file and log their values.
  3. Create a default export in a file utils.js that exports an object with a capitalize function. Import it.

Intermediate

  1. Create a math.js module that exports add, subtract, multiply, divide as named exports and a default export calculate(op, a, b) that uses a switch to call the right function.
  2. Create a barrel file shapes/index.js that re-exports Circle, Rectangle, Triangle from separate files. Import all three in app.js.
  3. Create a module store.js that uses module-level private state (like the counter example) to manage a simple todo list with addTodo(text), removeTodo(id), getTodos(), and getStats().

Advanced

  1. Build a plugin system using dynamic imports: create a plugins/ folder with multiple plugin modules, each exporting name, execute(data), and version. Create a PluginManager module that dynamically loads all plugins from a list and runs them.
  2. Create an async data module that fetches data from an API and exports it as a live binding that other modules can subscribe to. Use module-level state and exported update/subscribe functions.

5. Mini Project: Modular Application Shell

Build a small modular application that demonstrates module organization, barrel files, dynamic imports, and private state.

// ---- project/app.js — Main Entry Point ----
import { initializeUI } from "./ui/index.js";
import { loadConfig } from "./config/index.js";
import { initializeAuth } from "./services/auth.js";

async function main() {
    console.log("🚀 Application starting...");

    // Load configuration
    const config = await loadConfig();
    console.log(`📋 Config loaded: ${config.appName} v${config.version}`);

    // Initialize authentication
    const auth = initializeAuth(config.api);
    console.log(`🔐 Auth initialized: ${auth.isLoggedIn() ? "Logged in" : "Guest"}`);

    // Initialize UI
    initializeUI(config);
    console.log("🖥️ UI initialized");
}

main().catch(console.error);

// ---- project/config/index.js — Barrel + Config Loading ----
import { loadEnvironmentConfig } from "./environment.js";
import { mergeConfigs } from "./merge.js";

const DEFAULT_CONFIG = {
    appName: "Modular App",
    version: "1.0.0",
    debug: false,
    api: {
        baseUrl: "https://api.example.com",
        timeout: 5000,
        retries: 3
    },
    ui: {
        theme: "light",
        fontSize: 14
    }
};

let currentConfig = null;

export async function loadConfig(overrides = {}) {
    if (currentConfig) return currentConfig;

    const envConfig = await loadEnvironmentConfig();
    currentConfig = mergeConfigs(DEFAULT_CONFIG, envConfig, overrides);
    return currentConfig;
}

export function getConfig() {
    if (!currentConfig) throw new Error("Config not loaded. Call loadConfig() first.");
    return currentConfig;
}

export function updateConfig(path, value) {
    if (!currentConfig) throw new Error("Config not loaded.");
    const keys = path.split(".");
    let target = currentConfig;
    for (let i = 0; i < keys.length - 1; i++) {
        target = target[keys[i]];
    }
    target[keys[keys.length - 1]] = value;
    return currentConfig;
}

// ---- project/config/environment.js ----
export async function loadEnvironmentConfig() {
    // Simulate fetching environment config
    return {
        debug: window.location.hostname === "localhost",
        api: {
            baseUrl: window.location.hostname === "localhost"
                ? "http://localhost:3000/api"
                : "https://api.example.com"
        }
    };
}

// ---- project/config/merge.js ----
export function mergeConfigs(...configs) {
    function deepMerge(target, source) {
        const result = { ...target };
        for (const key of Object.keys(source)) {
            if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
                result[key] = deepMerge(target[key] || {}, source[key]);
            } else {
                result[key] = source[key];
            }
        }
        return result;
    }

    return configs.reduce((acc, config) => deepMerge(acc, config), {});
}

// ---- project/services/auth.js ----
let currentUser = null;
let authListeners = new Set();

export function initializeAuth(apiConfig) {
    // Check for saved session
    const savedSession = localStorage.getItem("auth_session");
    if (savedSession) {
        try {
            currentUser = JSON.parse(savedSession);
        } catch {
            localStorage.removeItem("auth_session");
        }
    }

    return {
        login(email, password) {
            // Simulate API call
            currentUser = { email, name: email.split("@")[0], loggedInAt: new Date().toISOString() };
            localStorage.setItem("auth_session", JSON.stringify(currentUser));
            notifyListeners();
            return currentUser;
        },
        logout() {
            currentUser = null;
            localStorage.removeItem("auth_session");
            notifyListeners();
        },
        isLoggedIn() {
            return currentUser !== null;
        },
        getUser() {
            return currentUser ? { ...currentUser } : null;
        },
        onAuthChange(callback) {
            authListeners.add(callback);
            return () => authListeners.delete(callback);
        }
    };
}

function notifyListeners() {
    authListeners.forEach(fn => fn(currentUser));
}

// ---- project/ui/index.js — Barrel File ----
export { initializeUI } from "./app.js";
export { renderHeader } from "./components/header.js";
export { renderFooter } from "./components/footer.js";

// ---- project/ui/app.js ----
import { renderHeader } from "./components/header.js";
import { renderFooter } from "./components/footer.js";
import { initializeAuth } from "../services/auth.js";

export function initializeUI(config) {
    renderHeader(config.appName);
    renderFooter(config.version);

    // Dynamic import for theme
    if (config.ui.theme) {
        import(`./themes/${config.ui.theme}.js`)
            .then(theme => theme.apply())
            .catch(() => console.log(`Theme "${config.ui.theme}" not found, using default.`));
    }
}

// ---- project/ui/components/header.js ----
export function renderHeader(title) {
    const header = document.createElement("header");
    header.innerHTML = `<h1>${title}</h1>`;
    document.body.prepend(header);
}

// ---- project/ui/components/footer.js ----
export function renderFooter(version) {
    const footer = document.createElement("footer");
    footer.innerHTML = `<p>Version ${version}</p>`;
    document.body.appendChild(footer);
}

// ---- project/ui/themes/dark.js ----
export function apply() {
    document.documentElement.style.setProperty("--bg-color", "#222");
    document.documentElement.style.setProperty("--text-color", "#eee");
    console.log("🌙 Dark theme applied");
}

// ---- project/ui/themes/light.js ----
export function apply() {
    document.documentElement.style.setProperty("--bg-color", "#fff");
    document.documentElement.style.setProperty("--text-color", "#222");
    console.log("☀️ Light theme applied");
}

6. Common Mistakes

MistakeWhy it's wrongCorrect approach
Forgetting type="module" in HTMLBrowser loads script as regular script (no import/export)Always add type="module" to <script> tag
Using import inside a function (static)Static imports must be at top levelMove to top of file or use dynamic import()
Forgetting .js extension in importsMay fail to resolve the fileAlways include file extension: import from "./file.js"
Circular dependenciesCreates infinite loop or undefined importsRefactor to remove circular references
Mutating imported values (primitives)Primitives from modules are read-only live bindingsExport getter functions instead
Expecting export default to work with export { ... }Default is separate from named exportsCombine: import defaultExport, { named } from "./module.js"
Mixing CommonJS (require) with ES6 modulesIncompatible in the same file (Node.js)Use one style consistently per file
Assuming module-level variables are globalEach module has its own scopeExport what you need; keep rest private

7. Best Practices

  • Use modules for everything — avoid creating large monolithic files. Split by responsibility.
  • Prefer named exports over default exports — named exports are explicit, support tree-shaking, and provide better IDE autocompletion.
  • Use default exports sparingly — typically for the main class/function of a module (e.g., the primary component).
  • Create barrel files (index.js) to simplify imports from a directory.
  • Use meaningful file and export names — the name should reflect the content.
  • Keep modules focused — each module should do one thing well (Single Responsibility Principle).
  • Avoid side effects at module level — side effects make code harder to test and reason about.
  • Use dynamic imports for code splitting — load heavy modules only when needed.
  • Use consistent naming conventions — typically camelCase for exports, PascalCase for classes.
  • Export constants as named exports rather than a single config object — enables tree-shaking.

8. Challenge Assignment (Optional)

"Plugin Architecture with Dynamic Modules" Challenge

Build a plugin-based application where features can be added as separate module files.

Requirements:

  1. Create a PluginManager class that:

    • registerPlugin(name, modulePath) — registers a plugin to be loaded later
    • loadPlugin(name) — dynamically imports and initializes a plugin
    • loadAllPlugins() — loads all registered plugins in parallel
    • getPlugin(name) — returns an already-loaded plugin's API
    • unloadPlugin(name) — removes a plugin's effects (calls destroy())
    • getLoadedPlugins() — returns list of loaded plugin names
  2. Each plugin module should export:

    • name — string identifier
    • version — semver string
    • initialize(context) — called when plugin loads, receives app context
    • destroy() — called when plugin is unloaded
    • metadata — optional info object
  3. Create sample plugins:

    • logger.js — logs all app events to console
    • analytics.js — tracks user actions (simulated)
    • theme-manager.js — allows switching themes

Bonus:

  • Add dependency resolution between plugins
  • Add plugin configuration via a config object
  • Implement hot-reloading (watch for file changes and reload)
  • Create a plugin marketplace simulation

Example usage:

const manager = new PluginManager();
manager.registerPlugin("logger", "./plugins/logger.js");
manager.registerPlugin("analytics", "./plugins/analytics.js", { trackingId: "UA-12345" });

await manager.loadAllPlugins();
// Logger initialized
// Analytics initialized with trackingId: UA-12345

manager.getPlugin("logger").log("Application started");

9. Knowledge Check (Quiz)

  1. How do you load a script as an ES6 module in HTML?

    • a) <script src="app.js">
    • b) <script type="module" src="app.js">
    • c) <script module="true" src="app.js">
    • d) <script es6 src="app.js">
  2. How many default exports can a module have?

    • a) 0
    • b) 1
    • c) Unlimited
    • d) 2
  3. What is the correct way to import both default and named exports?

    • a) import defaultExport, { namedExport } from "./module.js"
    • b) import { default as defaultExport, namedExport } from "./module.js"
    • c) Both a and b
    • d) import defaultExport, namedExport from "./module.js"
  4. Which statement about module scope is true?

    • a) All top-level variables are global
    • b) Top-level variables are scoped to the module
    • c) Variables must be explicitly marked private
    • d) var declarations leak to global scope
  5. What is a "barrel file"?

    • a) A file that contains only default exports
    • b) An index.js file that re-exports from multiple modules
    • c) A file that imports everything from a single module
    • d) A file that exports only functions
  6. How do you rename an import?

    • a) import { originalName as newName } from "./module.js"
    • b) import { originalName: newName } from "./module.js"
    • c) import newName = originalName from "./module.js"
    • d) import { originalName -> newName } from "./module.js"
  7. What does dynamic import() return?

    • a) The exported values directly
    • b) A Promise that resolves to the module namespace object
    • c) undefined
    • d) A callback
  8. Can you use import inside an if statement with static import syntax?

    • a) Yes, anywhere
    • b) No, static imports must be at top level
    • c) Yes, but only with const
    • d) Only with var

Answers: 1-b, 2-b, 3-c (both syntaxes work), 4-b, 5-b, 6-a, 7-b, 8-b


10. Additional Resources


11. Summary

Today you learned:

  • What modules are — reusable, scoped pieces of code with explicit dependencies
  • Named exportsexport const, export function, export { ... } with aliases
  • Default exports — one per module, imported without curly braces
  • Import syntax — named, default, aliased, and namespace imports
  • Re-exporting — barrel files and export * from
  • Dynamic importsimport() as a function for code splitting and lazy loading
  • Module scope — top-level variables are private unless exported
  • Browser vs Node.jstype="module" for browsers, "type": "module" in package.json for Node
  • Practical application — modular application shell with config, auth, UI components, and dynamic theme loading

You now have the skills to organize JavaScript code into clean, maintainable modules — essential for any real-world project of significant size. Tomorrow we'll explore generators and iterators — powerful tools for working with sequences of data.


"Modules are the architecture of your code — they define boundaries, manage dependencies, and keep complexity under control. Master them, and you'll build applications that scale."

Comments

EARN AT THE COMFORT OF YOUR HOME

Sponsored content

Popular posts from this blog

Day 5 – Functions, Scope, and Docstrings

Day 10 – Exception Handling and Debugging

Day 1 – Python Foundations for AEC Professionals