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));
mathUtils.js file.add is what the module would export. Real syntax: export function add(a, b) { ... }.main.js that consumes the module.{ add } mirrors import { add } from "./mathUtils.js". Name must match the export.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));
stringUtils.js.main.js.import { capitalize, truncate } from "..." — pull only what you need.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));
multiply.multiply: times = import { multiply as times }. Local name can differ from the export name.times, not multiply.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"));
greeter.js.export default function greet(name) { ... }.main.js.sayHello here).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");
logger.js.printLog, log, etc. No as needed.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"));
apiClient.js.apiClient.BASE_URL and buildHeaders from the same file.apiClient.import apiClient, { BASE_URL, buildHeaders } from "...".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));
stringUtils.js.stringUtils.capitalize mirrors import * as stringUtils from "...". All exports live under one prefix — useful when a module has many related functions.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.");
export { x } from "./other.js" forwards exports without importing them into this file first.index.js only. Internal file layout can change without breaking imports.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();
import("./report.js").await the import.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());
counterStore.js.count. Only increment and getCount are exposed. Modules work the same way — top-level variables stay private unless exported.increment changes the same shared count.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));
validators.js and formatters.js.checkout imports and uses both modules. Real apps are built this way.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"supportsimport/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
exportshares a variable, function, or class from a module;importpulls 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 namegathers 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));
mathUtils.js.square and cube.main.js.cube: toCube keeps the export name in the module but uses a clearer local name.toCube, not cube. Try adding another helper.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.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.