Academic Block

Modules (import / export) Get Certified

Modules let you split JavaScript code into separate files that can share values with each other using export and import. This keeps projects organized, avoids name collisions, and makes code reuse straightforward.

Why Use Modules?

Without modules, every script shares one global scope. Modules give each file its own scope and a clean way to share only the pieces you choose.

What this code does: Shows the basic idea of exporting a function from one “file” and importing it into another (simulated so it runs in the editor).

// mathUtils.js (simulated)
const mathUtils = {
    add(a, b) {
        return a + b;
    }
};

// main.js (simulated)
const { add } = mathUtils;
console.log(add(5, 7));

Named Exports

A named export makes a specific binding available under its original name. You import it with curly braces, and the name must match (unless you rename it with as).

What this code does: Demonstrates multiple named exports from a single module and importing them with matching names.

// stringUtils.js (simulated)
const stringUtils = {
    capitalize(text) {
        return text.charAt(0).toUpperCase() + text.slice(1);
    },
    truncate(text, maxLength) {
        return text.length > maxLength
            ? text.slice(0, maxLength) + "..."
            : text;
    }
};

// main.js (simulated)
const { capitalize, truncate } = stringUtils;
console.log(capitalize("hello"));
console.log(truncate("A longer sentence", 8));

Renaming on Import or Export

You can rename a binding while exporting or while importing using the as keyword.

What this code does: Renames a named export during the import step so the local name is different from the original export name.

// mathUtils.js (simulated)
const mathUtils = {
    multiply(a, b) {
        return a * b;
    }
};

// main.js – rename on import
const { multiply: times } = mathUtils;
console.log(times(4, 6));

Default Exports

A file may have one default export. When you import a default export you omit the curly braces and may choose any local name.

What this code does: Shows a default export being imported under a completely different local name.

// greeter.js (simulated)
const greeterModule = {
    default: function greet(name) {
        return "Hello, " + name + "!";
    }
};

// main.js
const sayHello = greeterModule.default;
console.log(sayHello("Michael"));

What this code does: Another default-export example that lets you pick any local name without curly braces.

// logger.js (simulated)
const loggerModuleDefault = function log(message) {
    console.log("[LOG]", message);
};

// Importing a default export lets you name it anything you want:
const printLog = loggerModuleDefault;
printLog("Server started");

Combining Default and Named Exports

A single file can have both a default export and any number of named exports at the same time.

What this code does: Combines a default export with additional named exports from the same module file.

// apiClient.js (simulated)
const apiClientModule = {
    default: function apiClient(endpoint) {
        return "Requesting: " + endpoint;
    },
    BASE_URL: "https://api.example.com",
    buildHeaders(token) {
        return { Authorization: "Bearer " + token };
    }
};

const apiClient = apiClientModule.default;
const { BASE_URL, buildHeaders } = apiClientModule;

console.log(apiClient("/users"));
console.log(BASE_URL);
console.log(buildHeaders("xyz123"));

Importing Everything With * as

import * as name gathers all of a module’s named exports into a single object, accessed through that name.

What this code does: Imports every named export into a single namespace object with import * as (simulated).

// stringUtils.js (simulated)
const stringUtils = {
    capitalize(text) {
        return text.charAt(0).toUpperCase() + text.slice(1);
    },
    truncate(text, maxLength) {
        return text.length > maxLength
            ? text.slice(0, maxLength) + "..."
            : text;
    }
};

console.log(stringUtils.capitalize("hi"));
console.log(stringUtils.truncate("Long text here", 5));

Re-exporting From Another Module

A module can import something and immediately export it again, without an intermediate variable — commonly used to create a single “index” file that gathers exports from several files.

What this code does: Illustrates the idea of re-exporting selected members through a central barrel (index) file.

// index.js would contain:
// export { capitalize, truncate } from "./stringUtils.js";
// export { circleArea } from "./mathUtils.js";

// Other files can then import everything from just index.js:
// import { capitalize, circleArea } from "./index.js";

console.log("Re-exporting keeps import paths short and centralized.");

Dynamic Imports

import() loads a module on demand and returns a Promise. This is useful for loading heavy code only when it’s actually needed (for example after a button click).

What this code does: Loads a module on demand with a simulated dynamic import() that returns a Promise.

function simulateDynamicImport() {
    console.log("Loading module on demand...");
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve({
                generateReport() {
                    console.log("Report generated!");
                }
            });
        }, 300);
    });
}

async function onButtonClick() {
    const { generateReport } = await simulateDynamicImport();
    generateReport();
}

onButtonClick();

Modules Are Only Loaded Once

No matter how many files import from it, a module only runs once — every import receives the exact same instance of its exported values. This makes modules a natural way to share state.

What this code does: Shows that module state is shared: every importer sees the same live binding of the exported value.

// counterStore.js (simulated)
const counterStore = (function () {
    let count = 0;
    return {
        increment() { return ++count; },
        getCount() { return count; }
    };
})();

// Two different “files” both importing the same module instance:
const fileA = counterStore;
const fileB = counterStore;

fileA.increment();
fileB.increment();

console.log("Count seen from fileA:", fileA.getCount());
console.log("Count seen from fileB:", fileB.getCount());

Export and Import Styles Overview

Style Export Syntax Import Syntax
Named export export const value = …; import { value } from “…”;
Default export export default value; import anyName from “…”;
Import everything (multiple named exports) import * as name from “…”;
Renaming export { value as newName }; import { value as newName } from “…”;

Practical Example

Let’s simulate a small app that pulls together a validation module and a formatting module — a common pattern when splitting real code across files.

What this code does: Illustrates organizing related helpers into focused modules (validators, formatters) that other code can import.

// validators.js & formatters.js (simulated)
const validators = {
    isValidEmail(email) {
        return email.includes("@") && email.includes(".");
    }
};

const formatters = {
    formatCurrency(amount) {
        return "$" + amount.toFixed(2);
    }
};

function checkout(email, total) {
    if (!validators.isValidEmail(email)) {
        return "Please enter a valid email address.";
    }
    return "Order confirmed! Total: " + formatters.formatCurrency(total);
}

console.log(checkout("shopper@example.com", 49.5));
console.log(checkout("not-an-email", 49.5));

Common Mistakes With Modules

  • Mixing up named and default import syntax — using curly braces for a default import, or forgetting them for a named one.
  • Trying to have more than one default export in the same file, which isn’t allowed.
  • Forgetting the file extension or relative path (./) when importing a local module file.
  • Assuming a script tag without type="module" supports import/export.
  • Creating circular imports (two modules importing from each other), which can lead to partially-initialized values.

Best Practices for Modules

  • Prefer named exports for most cases — they make renamed imports and refactoring tools work more reliably.
  • Reserve default exports for files that genuinely represent one main thing (a single component or class).
  • Keep each module focused on a single responsibility.
  • Use dynamic import() for code that’s only needed occasionally, to reduce upfront load.
  • Avoid circular imports by moving shared logic into its own separate module.

Quick Summary

  • export shares a variable, function, or class from a module; import pulls it into another file.
  • Named exports use curly braces on import and must match the exported name (unless renamed with as).
  • A file can have one default export, imported without curly braces under any chosen name.
  • import * as name gathers every named export into one object.
  • import() loads a module dynamically and returns a Promise.
  • A module only runs once, no matter how many files import from it — every importer shares the same instance.

Try It Yourself

Modify the simulated modules below. Try adding a new function to mathUtils, or renaming one during “import” using destructuring.

What this code does: A practice example that simulates named exports and destructured imports in a single runnable snippet.

// mathUtils.js (simulated)
const mathUtils = {
    square(n) { return n * n; },
    cube(n) { return n * n * n; }
};

// main.js – rename on import
const { square, cube: toCube } = mathUtils;

console.log(square(6));
console.log(toCube(3));
Tip: When deciding between a named and default export, ask: “If I added a second thing worth exporting from this file, would it still make sense?” If yes, named exports usually fit better than forcing everything through a single default.

Understanding modules is essential for organizing real-world JavaScript projects, and pairs naturally with build tools and bundlers as your codebase grows.

🧪 Test Your JavaScript Code

Edit the code on the left and click “Run Code” to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click “Run Code” to see the output here.

Ctrl + Enter to run code Esc to close