Modules
A small script fits in one file. A real app does not: a shop site may have code for the cart, the product list, payments, form checks and price formatting. Keeping all of it in one giant file makes it hard to read, test and reuse. Modules let you split code into files, where each file shares only what it chooses to and states exactly what it needs from others.
In this lesson you will export and import values, load modules in the browser, load code only when it is needed with import(), map short names to files with import maps, and import JSON files with the ES2025 with { type: "json" } syntax.
Why modules?
A module is simply a JavaScript file loaded as a module. Compared with an old-style script, it has four important differences:
- Its own scope. Variables and functions in a module are private to that file. Nothing leaks into the global
windowobject, so two files can both have a variable calledtotalwithout a clash. - Explicit connections. A module uses
exportto share things andimportto use things from other modules. Reading the top of a file tells you what it depends on. - Strict mode, always. Mistakes such as assigning to an undeclared variable throw errors instead of failing silently.
- Run once. However many files import a module, it runs only once, and every importer shares the same exports.
Exporting and importing
Put export in front of any declaration you want to share. Here is a small shop example made of three files:
// utils/price.js
export const GST_RATE = 0.18;
export function withGst(amount) {
return Math.round(amount * (1 + GST_RATE));
}
export function formatRupees(amount) {
return `₹${amount.toLocaleString("en-IN")}`;
}
const secret = "not exported"; // private to this file// cart.js
import { withGst } from "./utils/price.js";
export default class Cart {
#items = [];
add(name, price) {
this.#items.push({ name, price });
}
get total() {
return withGst(this.#items.reduce((sum, item) => sum + item.price, 0));
}
}// main.js
import Cart from "./cart.js";
import { formatRupees as rupees, GST_RATE } from "./utils/price.js";
import * as price from "./utils/price.js";
const cart = new Cart();
cart.add("Cricket bat", 2499);
cart.add("Batting gloves", 899);
console.log(`GST: ${GST_RATE * 100}%`);
console.log(`Total: ${rupees(cart.total)}`);
console.log(Object.keys(price));
console.log(typeof price.secret);GST: 18% Total: ₹4,010 [ 'GST_RATE', 'formatRupees', 'withGst' ] undefined
There are two kinds of export:
| Kind | Export | Import |
|---|---|---|
| Named (many per file) | export function withGst() {} | import { withGst } from "./price.js" |
| Default (one per file) | export default class Cart {} | import Cart from "./cart.js" |
With named imports, the name inside { } must match the export; use as to rename it locally, as with formatRupees as rupees. With a default import, you choose any name you like. import * as price collects every named export into one object, and secret is not in it because it was never exported.
Many teams prefer named exports everywhere: the same name is used in every file, which makes searching and renaming easier.
Loading modules in the browser
Load your entry file with type="module". The browser then follows the import lines and downloads the other files itself:
<script type="module" src="main.js"></script>Module scripts are deferred automatically: they wait until the HTML is parsed, even when placed in the <head>. Run this example to see the other differences too:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Module scripts</title>
<script type="module">
const shopName = "Rohan's Sports"; // module scope: not a global variable
function isStrict() {
return this === undefined; // true only in strict mode
}
document.querySelector("#out").textContent = [
`Heading found from the head: ${document.querySelector("h1") !== null}`,
`this at the top level: ${this}`,
`shopName on window: ${"shopName" in window}`,
`Strict mode: ${isStrict()}`,
].join("\n");
</script>
</head>
<body>
<h1>Module scripts</h1>
<pre id="out"></pre>
</body>
</html>Heading found from the head: true this at the top level: undefined shopName on window: false Strict mode: true
In the browser, import paths must be full: start with ./, ../ or / (or be a full URL) and include the file extension. import { withGst } from "price" fails with an error about a bare specifier unless you add an import map, shown below.
Dynamic import()
A normal import line loads its module before your code starts. Sometimes you want to load code only when it is needed, such as a big chart library that only appears after a button click. The function-like import() loads a module on demand and returns a promise for its exports:
const button = document.querySelector("#show-report");
button.addEventListener("click", async () => {
const { drawChart } = await import("./chart.js"); // downloaded on the first click only
drawChart(document.querySelector("#report"), salesData);
});The first page load stays small and fast, and the browser caches the module, so later clicks do not download it again. import() also works in classic scripts.
Import maps
An import map tells the browser what short names, called bare specifiers, should point to. It is a JSON block in a <script type="importmap"> element, and it must appear before any module that uses it:
<script type="importmap">
{
"imports": {
"price": "/js/utils/price.js",
"ui/": "/js/components/"
}
}
</script>
<script type="module">
import { formatRupees } from "price";
import { showToast } from "ui/toast.js"; // becomes /js/components/toast.js
</script>A key ending with / maps a whole folder. Import maps are useful for sharing code across pages, and for using packages from a CDN by name without a build tool. Larger projects often use a bundler such as Vite instead, which handles bare imports for you.
JSON modules
Top-level await, which you met in async & await, also works only in modules.
Common mistakes
- Forgetting
type="module". The browser reportsCannot use import statement outside a module. - Leaving out
./or.js. Browser imports need a full relative path with the extension. - Opening the page as a file. Use a local web server.
- Mixing up default and named imports. If
cart.jshasexport default class Cart, writeimport Cart from "./cart.js", notimport { Cart } .... - Expecting module variables on
window. They are private. Export them, or pass them as arguments.
Key points
- Each module has its own scope, runs in strict mode and runs only once.
- Share with
export(named or default) and use withimport; rename withas. - Load the entry file with
<script type="module" src="...">; modules are deferred automatically. import()loads a module on demand and returns a promise.- Import maps let bare names such as
"price"point to real files. - JSON files can be imported with
with { type: "json" }(ES2025).
Exercise
Plan a tiny module setup for a school results page. Write grades.js that exports a named function gradeFor(marks) (90 or more is "A", 75 or more is "B", 33 or more is "C", otherwise "Fail") and a default export passMark with the value 33. Then write main.js that imports both, renames gradeFor to grade, and logs the grade for 82 and for 20. Finally, write the <script> line that loads main.js in a page.
Show solution
// grades.js
export function gradeFor(marks) {
if (marks >= 90) return "A";
if (marks >= 75) return "B";
if (marks >= 33) return "C";
return "Fail";
}
const passMark = 33;
export default passMark;// main.js
import passMark, { gradeFor as grade } from "./grades.js";
console.log(`Pass mark: ${passMark}`); // Pass mark: 33
console.log(grade(82)); // B
console.log(grade(20)); // Fail<script type="module" src="main.js"></script>The default import comes first, then the named imports in braces. Serve the folder with a local web server so the browser can load both files.
Quick Quiz
1. What happens to a const declared at the top level of a module?
Modules have their own scope; only exported values are visible to other modules.
2. cart.js contains export default class Cart {}. Which import is correct?
A default export is imported without braces, and browser paths need ./ and the extension.
3. What does import("./chart.js") return?
Dynamic import() is asynchronous and gives a promise for the module namespace object.
4. Why is with { type: "json" } required when importing a JSON file?
The import attribute is a safety check, so a JSON import can never execute JavaScript.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
