Why Modules: A Shop App That Grew Too Big
Mr. Gupta wants his kirana shop online. His nephew Aman starts building the app in one file, script.js. At first it has 50 lines. Then he adds GST, the cart, delivery charges, offers and the bill. Soon the file has 1,500 lines. Finding anything takes minutes, and when his friend joins to help, they both keep changing the same file.
The professional answer is to split the code into modules. A module is simply a JavaScript file that decides what it shares with other files (export) and what it needs from them (import). Aman’s app becomes three small files, each with one job:
kirana-shop/
├── index.html
├── app.js (starts the app, talks to the page)
├── cart.js (adds items, works out the total)
└── price.js (GST and rupee formatting)
In this lesson you will learn why modules help, how to export and import, how to load modules in a web page and the one setup step that confuses almost every beginner.
The Problem Modules Solve
Modules give you two big benefits:
- Organisation. Each file has one job, so code is easy to find, test and reuse. The price code can be used by the cart, the bill and the offers page.
- No name clashes. With normal script tags, every file shares one global space. If two files create something with the same name, one silently replaces the other. Run this to see the danger:
// Imagine this part is price.js, written by you
function format(value) {
return "Rs " + value;
}
// ...and this part is delivery.js, written by your friend
function format(value) {
return value + " days";
}
// Both files were added to the page with normal script tags.
// They share ONE global space, so the second format() silently replaced the first.
console.log(format(499)); // 499 days (you wanted "Rs 499")
No error, just a wrong answer. Bugs like this are very hard to find. In a module, every top-level variable and function is private to that file unless you export it, so two modules can both have a format function with no problem.
Exporting: Sharing from a File
There are two kinds of exports:
- Named exports: write
exportin front of aconst,let,functionorclass. A file can have many. - A default export: write
export defaultin front of the file’s main thing. A file can have at most one.
Here is Aman’s price.js. It has two named exports and one default export. (To keep the example simple, every item uses one 5% GST rate.)
// price.js
export const GST_RATE = 0.05;
export function addGst(amount) {
return Math.round(amount * (1 + GST_RATE));
}
export default function formatRupees(amount) {
return "Rs " + amount.toLocaleString("en-IN");
}
You can also export a list at the end of a file. This keeps all exports in one place, which some teams prefer:
// Export while declaring
export const SHOP_NAME = "Gupta Kirana";
export function addGst(amount) { /* ... */ }
// Or declare first and export a list at the end (you can rename here too)
const DELIVERY_FEE = 30;
function freeDeliveryFrom() { return 499; }
export { DELIVERY_FEE, freeDeliveryFrom as minimumForFreeDelivery };
// One default export per file (no name needed)
export default function formatRupees(amount) { /* ... */ }
Importing: Using Code from Another File
An import statement goes at the top of a file. The part after from is the path to the file. In the browser it must start with ./ (this folder) or ../ (the parent folder), and it must include .js.
// These lines show different options. In a real file, import each name only once.
// Named imports: the names in { } must match the exported names exactly
import { addGst, GST_RATE } from "./price.js";
// Default import: you choose the name, no { }
import formatRupees from "./price.js";
// Both in one line: default first, then the named ones
import formatRupees, { addGst } from "./price.js";
// Rename with "as" to avoid a clash or to get a clearer name
import { addGst as withTax } from "./price.js";
// Everything as one object
import * as price from "./price.js";
price.addGst(100); // 105
price.default(1250); // the default export is called "default"
| Named export | Default export | |
|---|---|---|
| How many per file | As many as you like | At most one |
| Export syntax | export function addGst() | export default function formatRupees() |
| Import syntax | import { addGst } from "./price.js" | import formatRupees from "./price.js" |
| Name when importing | Must match (or rename with as) | Any name you choose |
Many teams prefer named exports for most things, because the names stay the same in every file, and editors can find and rename them easily.
Trying Modules in the Browser
About the demo trick: the Try it yourself box runs one page and cannot hold several files, so normalimportlines cannot run there. To let you experiment anyway, the runnable examples write a tiny module as text, turn it into a data URL (an address that contains the file itself) and load it withimport(). This trick is only for learning. In real projects, always use real.jsfiles and normalimportstatements.
The function import(url), with brackets, is called a dynamic import. It loads a module while the code runs and gives back a promise. When the module is ready, .then receives an object that holds all the exports:
// DEMO TRICK: we write a small module as text, turn it into a "data URL"
// and load it with import(). Real projects use real files instead.
const priceModule = [
"export const GST_RATE = 0.05;",
"export function addGst(amount) { return Math.round(amount * (1 + GST_RATE)); }"
].join("\n");
const url = "data:text/javascript," + encodeURIComponent(priceModule);
import(url).then(function (price) {
// price is an object that holds everything the module exported
console.log(price.GST_RATE); // 0.05
console.log(price.addGst(640)); // 672
console.log(price.addGst(1000)); // 1050
});
With async and await (covered fully in the Async and Await lesson), the same thing reads more simply. Here you can also see that the default export is stored under the name default, and how renaming works:
const priceModule = [
"export const GST_RATE = 0.05;",
"export function addGst(amount) { return Math.round(amount * (1 + GST_RATE)); }",
"export default function formatRupees(amount) { return 'Rs ' + amount.toLocaleString('en-IN'); }"
].join("\n");
const url = "data:text/javascript," + encodeURIComponent(priceModule);
async function main() {
const price = await import(url);
// The default export is stored under the name "default"
console.log(price.default(125000)); // Rs 1,25,000
// Like: import formatRupees, { addGst as withTax } from "./price.js";
const { default: formatRupees, addGst: withTax } = price;
console.log(formatRupees(withTax(1200))); // Rs 1,260
}
main();
Module Scope: Private by Default
Everything at the top level of a module stays inside that module. Other files can use only what is exported. This is called module scope. It also means a module runs only once: every file that imports it gets the same copy, so the cart below keeps counting.
const cartModule = [
"const items = [];",
"export function addItem(name) { items.push(name); return items.length; }"
].join("\n");
const url = "data:text/javascript," + encodeURIComponent(cartModule);
async function main() {
const cart = await import(url);
console.log(cart.addItem("Atta, 10 kg")); // 1
console.log(cart.addItem("Sugar, 1 kg")); // 2
// items was NOT exported, so nobody outside can see or break it
console.log(cart.items); // undefined
console.log(typeof items); // undefined (it is not a global)
// Importing the same module again gives the SAME module: it runs only once
const sameCart = await import(url);
console.log(sameCart.addItem("Tea, 250 g")); // 3
}
main();
Modules have a few more automatic rules. They always run in strict mode, which turns some silent mistakes into clear errors. They also wait until the HTML has loaded before they run, as if they had the defer attribute.
Loading Modules in a Web Page
To use modules on a page, add type="module" to the script tag. You only load the starting file (app.js). The browser follows the import lines and loads cart.js and price.js by itself.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Gupta Kirana Online</title>
<script type="module" src="app.js"></script>
</head>
<body>
<h1>Gupta Kirana Online</h1>
<p id="summary"></p>
</body>
</html>
Because modules wait for the page to load, the script can sit in the <head> and still find the #summary paragraph.
Modules need a web server
Browsers load modules with the same safety rules they use for files from other websites. A page opened from file:// does not pass those rules, so you see an error like this in the console:
Access to script at 'file:///C:/Users/you/kirana-shop/app.js' from origin 'null'
has been blocked by CORS policy
Use Live Server. If you openindex.htmlby double-clicking it, the address starts withfile://and the browser refuses to load modules. In VS Code, right-clickindex.htmland choose Open with Live Server. The address becomes something likehttp://127.0.0.1:5500and modules work.
Real-Life Example: The Kirana Shop App in Three Files
Here is the complete shop app. price.js (shown above) knows about GST and rupees. cart.js keeps the items private and imports only addGst. app.js imports from both files and updates the page.
// cart.js
import { addGst } from "./price.js";
const items = []; // private to this module
export function addItem(name, price, qty) {
items.push({ name, price, qty });
}
export function itemCount() {
return items.length;
}
export function cartTotal() {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return addGst(subtotal);
}
// app.js
import formatRupees, { GST_RATE } from "./price.js";
import { addItem, itemCount, cartTotal as total } from "./cart.js";
addItem("Basmati rice, 5 kg", 640, 1);
addItem("Toor dal, 1 kg", 160, 2);
addItem("Sunflower oil, 1 L", 155, 2);
document.getElementById("summary").textContent =
itemCount() + " items, total " + formatRupees(total()) +
" (including " + GST_RATE * 100 + "% GST)";
Create the four files in one folder, open index.html with Live Server and the page shows: “3 items, total Rs 1,334 (including 5% GST)”. Notice how app.js imports cartTotal under the shorter name total.
Dynamic import() is also useful in real projects: it can load a large part of the app only when someone needs it. Here the bill code is loaded only when the button is clicked:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Load a Module When Needed</title>
</head>
<body>
<h2>Gupta Kirana: Your Order</h2>
<p>Subtotal: Rs 1,110</p>
<button id="billBtn">Show bill with GST</button>
<p id="bill"></p>
<script>
// In a real project this would be: await import("./bill.js")
const billModule = [
"export default function makeBill(subtotal) {",
" const gst = Math.round(subtotal * 0.05);",
" return 'Subtotal Rs ' + subtotal + ' + GST Rs ' + gst + ' = Rs ' + (subtotal + gst);",
"}"
].join("\n");
const billUrl = "data:text/javascript," + encodeURIComponent(billModule);
document.getElementById("billBtn").addEventListener("click", async function () {
const bill = await import(billUrl); // loaded only on the first click
document.getElementById("bill").textContent = bill.default(1110);
console.log("Bill module loaded");
});
</script>
</body>
</html>
Try these changes:
- In the real files, add a
removeItem(name)export tocart.jsand use it inapp.js. - Create
offers.jswith a default export that gives 10% off orders above Rs 1,000, and use it inapp.js. - Try to read
itemsfromapp.jsand see the error you get.
Common Mistakes
- Opening the page from
file://. Modules are blocked. Fix: use Live Server or another local web server. - Forgetting
type="module". The browser shows “Cannot use import statement outside a module”. Fix: add the attribute to the script tag. - Leaving out
./or.js.import { addGst } from "price"fails in the browser. Fix: write"./price.js". (Tools like Vite allow shorter paths, but plain browsers do not.) - Mixing up named and default imports.
import addGst from "./price.js"gives you the default export, notaddGst. Fix: use braces for named exports. - Expecting module variables to be global. You cannot type a module’s variable name in the console. Fix: export what other files need, and use DevTools breakpoints to inspect the rest.
Practice Task: A Marksheet in Modules
Split a small marksheet program into two modules. Create marks.js, report.js and index.html in a new folder and run them with Live Server. (This task uses real files, so it cannot run in the Try it yourself box.)
marks.jsexportsPASS_MARKandpercentage()as named exports, andgrade()as the default export.report.jsimports all three and prints one line per subject in the console.index.htmlloadsreport.jswithtype="module".
// marks.js
// TODO 1: export a constant PASS_MARK = 33
// TODO 2: export a function percentage(obtained, total) that returns a whole number
// TODO 3: export default a function grade(percent): "A" for 90+, "B" for 75+, "C" for 33+, else "Fail"
// report.js
// TODO 4: import grade (default), percentage and PASS_MARK from "./marks.js"
// TODO 5: for each subject, print "Maths: 82% (B)" and say whether it is a pass
const subjects = [
{ name: "Maths", obtained: 82, total: 100 },
{ name: "Science", obtained: 28, total: 100 },
{ name: "English", obtained: 46, total: 50 }
];
<!-- index.html -->
<!-- TODO 6: load report.js as a module -->
Show one possible answer (try it yourself first!)
// marks.js
export const PASS_MARK = 33;
export function percentage(obtained, total) {
return Math.round((obtained / total) * 100);
}
export default function grade(percent) {
if (percent >= 90) return "A";
if (percent >= 75) return "B";
if (percent >= PASS_MARK) return "C";
return "Fail";
}
// report.js
import grade, { percentage, PASS_MARK } from "./marks.js";
const subjects = [
{ name: "Maths", obtained: 82, total: 100 },
{ name: "Science", obtained: 28, total: 100 },
{ name: "English", obtained: 46, total: 50 }
];
subjects.forEach(function (s) {
const percent = percentage(s.obtained, s.total);
const result = percent >= PASS_MARK ? "pass" : "not a pass";
console.log(s.name + ": " + percent + "% (" + grade(percent) + "), " + result);
});
// Maths: 82% (B), pass
// Science: 28% (Fail), not a pass
// English: 92% (A), pass
<!-- index.html -->
<script type="module" src="report.js"></script>
Key Takeaways
- A module is a file that shares code with
exportand uses other files withimport. - Modules keep code organised and stop global name clashes, because their variables are private by default.
- Use named exports for most things and at most one
export defaultper file; rename imports withas. - Load the starting file with
<script type="module" src="app.js">, and use paths like"./price.js". - Modules need a web server such as Live Server, not
file://. - Dynamic
import()loads a module while the code runs, which is useful for loading features only when needed.