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
exportandexport default - Import values using
importwith 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
| Rule | Explanation |
|---|---|
| Strict mode by default | Modules automatically run in strict mode ("use strict") |
| Static structure | Imports/exports are resolved at parse time, not runtime |
| Live bindings | Exported values are live bindings (changes in the exporting module are reflected in importing modules) |
| Single default export | Only one export default per module |
| Top-level only | import 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
| Environment | File Extension | Module Type | Notes |
|---|---|---|---|
| Browser | .js, .mjs | ES6 modules | Use <script type="module" src="app.js"> |
| Node.js (>= 12) | .mjs | ES6 modules | Set "type": "module" in package.json |
| Node.js (legacy) | .js | CommonJS | Uses 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
- Create a file
greeting.jsthat exports a functionsayHello(name)returning"Hello, [name]!". Import and use it in another file. - Create a module
constants.jsthat exportsPIandEas named exports. Import them in another file and log their values. - Create a default export in a file
utils.jsthat exports an object with acapitalizefunction. Import it.
Intermediate
- Create a
math.jsmodule that exportsadd,subtract,multiply,divideas named exports and a default exportcalculate(op, a, b)that uses a switch to call the right function. - Create a barrel file
shapes/index.jsthat re-exportsCircle,Rectangle,Trianglefrom separate files. Import all three inapp.js. - Create a module
store.jsthat uses module-level private state (like the counter example) to manage a simple todo list withaddTodo(text),removeTodo(id),getTodos(), andgetStats().
Advanced
- Build a plugin system using dynamic imports: create a
plugins/folder with multiple plugin modules, each exportingname,execute(data), andversion. Create aPluginManagermodule that dynamically loads all plugins from a list and runs them. - 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
| Mistake | Why it's wrong | Correct approach |
|---|---|---|
Forgetting type="module" in HTML | Browser 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 level | Move to top of file or use dynamic import() |
Forgetting .js extension in imports | May fail to resolve the file | Always include file extension: import from "./file.js" |
| Circular dependencies | Creates infinite loop or undefined imports | Refactor to remove circular references |
| Mutating imported values (primitives) | Primitives from modules are read-only live bindings | Export getter functions instead |
Expecting export default to work with export { ... } | Default is separate from named exports | Combine: import defaultExport, { named } from "./module.js" |
Mixing CommonJS (require) with ES6 modules | Incompatible in the same file (Node.js) | Use one style consistently per file |
| Assuming module-level variables are global | Each module has its own scope | Export 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:
Create a
PluginManagerclass that:registerPlugin(name, modulePath)— registers a plugin to be loaded laterloadPlugin(name)— dynamically imports and initializes a pluginloadAllPlugins()— loads all registered plugins in parallelgetPlugin(name)— returns an already-loaded plugin's APIunloadPlugin(name)— removes a plugin's effects (callsdestroy())getLoadedPlugins()— returns list of loaded plugin names
Each plugin module should export:
name— string identifierversion— semver stringinitialize(context)— called when plugin loads, receives app contextdestroy()— called when plugin is unloadedmetadata— optional info object
Create sample plugins:
logger.js— logs all app events to consoleanalytics.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)
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">
- a)
How many default exports can a module have?
- a) 0
- b) 1
- c) Unlimited
- d) 2
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"
- a)
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)
vardeclarations leak to global scope
What is a "barrel file"?
- a) A file that contains only default exports
- b) An
index.jsfile that re-exports from multiple modules - c) A file that imports everything from a single module
- d) A file that exports only functions
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"
- a)
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
Can you use
importinside anifstatement 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
- MDN: JavaScript Modules → developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- MDN: export → developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
- MDN: import → developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
- MDN: Dynamic import → developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
- JavaScript.info: Modules → javascript.info/modules
- JavaScript.info: Dynamic imports → javascript.info/modules-dynamic-imports
- Video: "JavaScript ES6 Modules" by Web Dev Simplified
- Video: "JavaScript Modules in 100 Seconds" by Fireship
- Practice: freeCodeCamp: ES6 Modules
- Book: Chapter 10 of Eloquent JavaScript — "Modules"
11. Summary
Today you learned:
- What modules are — reusable, scoped pieces of code with explicit dependencies
- Named exports —
export 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 imports —
import()as a function for code splitting and lazy loading - Module scope — top-level variables are private unless exported
- Browser vs Node.js —
type="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
Post a Comment