IntermediateJavaScript · Lesson 7 of 10

ES Modules & Build Tools

Split code into modules with import/export, load them in the browser, and use npm with Vite.

ES modules split code into files that export what they share and import what they need. Each module has its own scope, so variables don't leak into the global page. Load the entry file with <script type="module" src="main.js"> — modules are deferred automatically.

Module imports in the browser need full relative paths, including .js. Use import() (dynamic import) to load rarely used code only when it's needed, making the first page load faster.

For larger projects, use npm and a build tool such as Vite: it serves your code instantly during development, lets you import npm packages, and bundles and minifies everything for production with npm run build.

index.htmlHTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Modules</title>
    <script type="module" src="main.js"></script>
  </head>
  <body>
    <p id="summary"></p>
    <button id="report">Show detailed report</button>
    <pre id="output"></pre>
  </body>
</html>
lib/grading.jsJavaScript
export const PASS_MARK = 30;

export function average(values) {
  return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
}

export default function gradeFor(score) {
  if (score >= 75) return "A";
  if (score >= 65) return "B";
  if (score >= 45) return "C";
  if (score >= PASS_MARK) return "D";
  return "F";
}
lib/report.jsJavaScript
import gradeFor from "./grading.js";

export function buildReport(scores) {
  return scores.map((s, i) => `Student ${i + 1}: ${s} (${gradeFor(s)})`).join("\n");
}
main.jsJavaScript
import gradeFor, { average, PASS_MARK } from "./lib/grading.js";

const scores = [88, 42, 71, 25];
const avg = average(scores);
document.querySelector("#summary").textContent =
  `Average ${avg.toFixed(1)} (${gradeFor(avg)}), ${scores.filter((s) => s >= PASS_MARK).length} passed`;

document.querySelector("#report").addEventListener("click", async () => {
  const { buildReport } = await import("./lib/report.js");   // loaded only on first click
  document.querySelector("#output").textContent = buildReport(scores);
});
TerminalShell
npm create vite@latest results-app -- --template vanilla
cd results-app
npm install
npm run dev        # development server with instant reload
npm run build      # optimised files in dist/ ready to deploy

Key points

  • Modules = one file, one responsibility, explicit import/export.
  • In the browser, use type="module" and include .js in import paths.
  • Dynamic import() loads code on demand; Vite handles npm packages and production builds.

Exercise

Split the attendance counter from the Beginner track into modules: state.js (data and update functions), view.js (rendering) and main.js (event wiring). Then create a Vite project and move the code into it.

Show solution

Try the exercise yourself first — then compare your approach with this one.

Each module has one job: state.js owns the data and the rules for changing it, view.js turns state into DOM updates, and main.js connects button clicks to state changes and re-renders.

index.htmlHTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Attendance</title>
    <style>.full { color: #b5541e; font-weight: bold; }</style>
    <script type="module" src="main.js"></script>
  </head>
  <body>
    <p>Present: <span id="count">0</span> / 5</p>
    <button id="add">Mark present</button>
    <button id="reset">Reset</button>
    <ul id="log"></ul>
  </body>
</html>
state.jsJavaScript
export const LIMIT = 5;

export const state = { present: 0, arrivals: [] };

export function markPresent(time) {
  if (state.present >= LIMIT) return;
  state.present++;
  state.arrivals.push(time);
}

export function reset() {
  state.present = 0;
  state.arrivals = [];
}
view.jsJavaScript
import { LIMIT } from "./state.js";

const countEl = document.querySelector("#count");
const addBtn = document.querySelector("#add");
const log = document.querySelector("#log");

export function render(state) {
  countEl.textContent = state.present;
  countEl.classList.toggle("full", state.present === LIMIT);
  addBtn.disabled = state.present === LIMIT;
  log.replaceChildren(
    ...state.arrivals.map((time, i) => {
      const li = document.createElement("li");
      li.textContent = `Student ${i + 1} arrived at ${time}`;
      return li;
    }),
  );
}
main.jsJavaScript
import { markPresent, reset, state } from "./state.js";
import { render } from "./view.js";

document.querySelector("#add").addEventListener("click", () => {
  markPresent(new Date().toLocaleTimeString());
  render(state);
});

document.querySelector("#reset").addEventListener("click", () => {
  reset();
  render(state);
});

render(state);

Check your understanding

  1. How do you load an ES module as a page's entry point?

  2. Which browser import path is correct?

  3. What does dynamic import("./report.js") let you do?

  4. What does npm run build produce in a Vite project?

Ask AI