Modules

JavaScript · Lesson 49 of 61 8 min read Updated

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 window object, so two files can both have a variable called total without a clash.
  • Explicit connections. A module uses export to share things and import to 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:

JavaScript
// 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
JavaScript
// 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));
  }
}
JavaScript
// 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);
Output
GST: 18%
Total: ₹4,010
[ 'GST_RATE', 'formatRupees', 'withGst' ]
undefined

There are two kinds of export:

KindExportImport
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:

HTML
<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:

HTMLTry it yourself
<!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>
Output
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:

JavaScript
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:

HTML
<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 reports Cannot 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.js has export default class Cart, write import Cart from "./cart.js", not import { 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 with import; rename with as.
  • 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
JavaScript
// 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;
JavaScript
// 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
HTML
<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?

2. cart.js contains export default class Cart {}. Which import is correct?

3. What does import("./chart.js") return?

4. Why is with { type: "json" } required when importing a JSON file?

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