← All JavaScript lessons

JavaScript / Chapter 06

Modules, Classes, Storage & Testing

Organize a maintainable application with ES modules, model behavior with classes when appropriate, persist local data, and test pure logic.

Chapter roadmap

Organize a maintainable application with ES modules, model behavior with classes when appropriate, persist local data, and test pure logic. Work through the concepts in order, type every example yourself, and keep a short debugging log that records predictions, errors, fixes, and what each fix taught you.

Learning objectives

  • Split code into modules
  • Use classes and composition
  • Persist JSON safely
  • Write focused automated tests

06.1

ES modules

Modules give each file its own scope and explicit imports and exports. Named exports make dependencies visible; default exports are useful when a module has one primary value. Avoid circular imports by keeping dependency direction simple.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
// math.js
export function average(values) {
  return values.reduce((sum, value) => sum + value, 0) / values.length;
}

// app.js
import { average } from "./math.js";
console.log(average([80, 90, 100]));
Result
90
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

06.2

Classes and composition

A class is useful when many objects share behavior and internal state. Constructor validation protects invariants, private fields hide implementation details, and methods expose intentional operations. Composition—objects containing focused helper objects—is often clearer than deep inheritance.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
class Timer {
  #seconds = 0;
  tick() { this.#seconds += 1; }
  reset() { this.#seconds = 0; }
  get seconds() { return this.#seconds; }
}

const timer = new Timer();
timer.tick();
console.log(timer.seconds);
Result
1
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

06.3

localStorage and JSON

localStorage persists string values for one origin. JSON.stringify converts compatible data to text and JSON.parse reconstructs it. Storage may be unavailable or corrupted, so parse inside try/catch, validate the result, and version data when the shape may evolve.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
function saveTasks(tasks) {
  localStorage.setItem("tasks-v1", JSON.stringify(tasks));
}
function loadTasks() {
  try {
    const value = JSON.parse(localStorage.getItem("tasks-v1") ?? "[]");
    return Array.isArray(value) ? value : [];
  } catch {
    return [];
  }
}
Result
Valid tasks reload; missing or malformed storage safely returns an empty array.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

06.4

Testing pure behavior

Tests arrange input, act by calling one unit, and assert an observable result. Pure functions are easiest to test because they do not depend on DOM, time, storage, or network state. Move calculations and transformations out of event handlers and test boundary and failure cases.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
import { test } from "node:test";
import assert from "node:assert/strict";
import { total } from "./cart.js";

test("total adds item subtotals", () => {
  assert.equal(total([{ price: 4, quantity: 3 }, { price: 2, quantity: 1 }]), 14);
});
Result
1 passing test
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

Chapter assignments

Complete these in order. Later assignments assume that earlier skills are working. Do not only test the sample values; design tests that challenge boundaries, missing values, incorrect types, empty collections, and other likely failures.

  1. A6.1

    Split a one-file application into data, validation, rendering, and controller modules with explicit dependencies.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  2. A6.2

    Model a small domain with two composed classes and enforce invariants through constructors and methods.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  3. A6.3

    Add versioned localStorage persistence and recovery from missing, malformed, and outdated data.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  4. A6.4

    Write tests for at least four pure functions, including boundaries, invalid input, empty collections, and normal cases.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

Chapter project

Offline Study Planner

Offline Study Planner — Build a modular application for courses, sessions, goals, and progress. Persist versioned data locally, separate pure calculations from DOM code, model only genuinely stateful concepts with classes, and provide automated tests for scheduling and progress logic.

Required process

  1. Write a short specification listing inputs, outputs, rules, and failure cases.
  2. Break the work into small functions, queries, modules, or classes appropriate to the language.
  3. Build the smallest working version before adding optional features.
  4. Test normal, boundary, empty, and invalid cases and record the results.
  5. Refactor names and duplication, then write a concise user guide.

Key terms

  • module
  • export
  • import
  • dependency
  • class
  • private field
  • composition
  • serialization
  • localStorage
  • unit test