In short
JavaScript modules let you split your code into small files that share code with import and export. Learn named and default exports, import paths and dynamic import(). Why use JavaScript modules? When a project grows, one big script.js file becomes hard to read and easy to break.
JavaScript modules let you split your code into small files that share code with import and export. Learn named and default exports, import paths and dynamic import().
Why use JavaScript modules?
When a project grows, one big script.js file becomes hard to read and easy to break. Modules let you split code into many small files, each with one job, like rooms in a house. Each file decides what it shares (export) and what it needs from other files (import).
Modules also fix an old problem. With normal scripts, every top-level variable is global, so two files can accidentally use the same name and overwrite each other. In a module, variables are private to that file unless you export them.
How to load a module with type="module"
To use modules in the browser, add type="module" to your script tag:
<script type="module" src="main.js"></script>
Module scripts behave a little differently from normal scripts:
- They are deferred. They wait until the HTML is fully parsed, so you can use
document.querySelectorwithout worrying about where the tag is placed. - They are always in strict mode. Strict mode turns silent mistakes into real errors. For example, using a variable you never declared throws an error instead of creating a global.
- They run only once, even if many files import them.
- They need a web server. Opening the HTML file directly (
file://) blocks modules for safety. Use a local server, such as the VS Code "Live Server" extension or XAMPP.
Note: Top-level
thisisundefinedin a module, and top-levelawaitis allowed, as you saw in the async/await lesson.
What is the difference between named and default exports?
A named export shares something by its name. A file can have many named exports. You import them with curly braces and the exact same names.
A default export is the one "main" thing a file shares. A file can have only one. You import it without curly braces and can give it any name you like.
// file: math.js
export const TAX_RATE = 0.2; // named export
export function addTax(price) { // named export
return price * (1 + TAX_RATE);
}
export default function formatPrice(n) { // default export
return `$${n.toFixed(2)}`;
}
// file: main.js
import formatPrice, { addTax, TAX_RATE } from "./math.js";
console.log(TAX_RATE); // Output: 0.2
console.log(formatPrice(addTax(10))); // Output: $12.00
You can rename a named import with as, which helps when two files use the same name: import { addTax as withTax } from "./math.js";. You can also import everything as one object: import * as math from "./math.js";, then use math.addTax(10).
Tip: Many teams prefer named exports, because the names stay the same in every file. That makes searching and auto-import in your editor work better.
How do import paths work?
In the browser, the path must point to a real file:
"./math.js"means "math.js in the same folder as this file"."../utils/date.js"means "go up one folder, then into utils"."/js/math.js"means "from the root of the website".
Always include the .js extension in the browser. A bare name like import ... from "lodash" does not work in a plain browser page; it needs a build tool (like Vite) or an import map, which are outside this lesson.
A small multi-file module example
Let's build a tiny shopping cart with three files. Put them in the same folder and open index.html through a local server.
<!-- file: index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Module cart</title>
<script type="module" src="app.js"></script>
</head>
<body>
<h1>My cart</h1>
<ul id="cart"></ul>
<p id="total"></p>
</body>
</html>
// file: cart.js
const items = []; // private: other files cannot touch it directly
export function addItem(name, price) {
items.push({ name, price });
}
export function getItems() {
return [...items]; // return a copy
}
export function getTotal() {
return items.reduce((sum, item) => sum + item.price, 0);
}
// file: app.js
import { addItem, getItems, getTotal } from "./cart.js";
addItem("Notebook", 4.5);
addItem("Pen", 1.25);
const list = document.querySelector("#cart"); // works: modules are deferred
getItems().forEach((item) => {
const li = document.createElement("li");
li.textContent = `${item.name}: $${item.price.toFixed(2)}`;
list.append(li);
});
document.querySelector("#total").textContent = `Total: $${getTotal().toFixed(2)}`;
The page shows "Notebook: $4.50", "Pen: $1.25" and "Total: $5.75". The script is in the <head>, but it still finds the elements because modules wait for the HTML. The items array is hidden inside cart.js, so the only way to change it is through the exported functions.
How to use dynamic import()
Normal import statements load at the start. Sometimes you only need code later, for example a big chart library when the user clicks "Show chart". import() used like a function loads a module on demand and returns a promise.
// file: app.js (add this at the end)
const button = document.createElement("button");
button.textContent = "Show total with tax";
document.body.append(button);
button.addEventListener("click", async () => {
const math = await import("./math.js"); // loaded only on first click
const withTax = math.addTax(getTotal()); // named exports are properties
console.log(math.default(withTax)); // the default export is called "default"
// Output: $6.90
});
Put the math.js file from earlier in the same folder. The first click downloads math.js; later clicks reuse it. This keeps the first page load small and fast.
Common mistake: Forgetting
type="module"on the script tag. You will see "Cannot use import statement outside a module". Add the attribute and serve the page from a local server.
Practice exercise
Create a file greetings.js with a named export sayHello(name) and a default export sayBye(name). Import both in main.js and print the messages to the console. Then add a button that uses import() to load a third file, jokes.js, only when clicked.
Hint: import the default without braces and the named one with braces: import sayBye, { sayHello } from "./greetings.js";.
Key takeaways
- Modules split code into files; each file's variables are private unless exported.
- Use
<script type="module">; modules are deferred, strict and need a web server. - A file can have many named exports (imported with
{ }) and one default export. - Browser import paths start with
./,../or/and include.js. import()loads a module on demand and returns a promise.