CodeRigor

FREE LESSON

Modules and private application state

Expose a CommonJS factory API while keeping catalog records private and returning safe snapshots.

A module gives a program a boundary: some names remain private, and an explicit interface becomes available to other files. These labs use Node's CommonJS convention. require loads a module, and the module's module.exports value is the result. Top-level variables live in a module wrapper rather than automatically becoming globals.

JavaScript
const { basename } = require("node:path");
console.log(basename("/data/jobs.jsonl"));

The node: prefix identifies a built-in Node module. No package installation is needed for this example. Relative paths such as require("./catalog.js") identify nearby files in a normal project. CodeRigor supplies one editable solution.js file for each lab, so the graded public interface is that file's exported object.

An explicit module boundary
start / 4
Consumers receive the exported factory, while its implementation remains local.
An explicit module boundary
Consumers receive the exported factory, while its implementation remains local.

exports initially references the same object as module.exports. Adding exports.createCatalog = createCatalog changes that object. Assigning exports = {createCatalog} only rebinds the local alias; it leaves the actual public value unchanged. Use module.exports = {createCatalog} when replacing the whole interface. This distinction is ordinary reference behavior applied to a module API.

Node also supports ES modules, whose interface uses import and export. Their loading rules are different. A project's package configuration and file extensions determine module interpretation; do not mix the two syntaxes by guessing. The following is an ES module fragment for comparison, not the CommonJS submission format:

JavaScript
// catalog.mjs
export function createCatalog(rows) { /* implementation */ }

A separate consumer imports the named export:

JavaScript
// consumer.mjs
import { createCatalog } from "./catalog.mjs";

Resolved CommonJS modules are normally cached after their first load. Therefore top-level initialization can create shared state, and top-level logging or file changes can surprise every importer. Exporting a factory lets the caller choose when to create an instance and gives each instance separate private state.

Protect the catalog on both sides
start / 1
Fresh records separate caller changes from private state.
Protect the catalog on both sides
Fresh records separate caller changes from private state.
JavaScript
function createStore() {
  const entries = new Map();
  return {
    put(key, value) { entries.set(key, {...value}); },
    get(key) { const value = entries.get(key); return value ? {...value} : null; }
  };
}
const store = createStore();
const row = {label: "original"};
store.put("a", row);
row.label = "changed";
console.log(store.get("a").label);

Copying on entry protects the instance from later caller changes. Copying on exit protects it from changes to returned records. The build records contain only string fields, so a shallow record copy is sufficient; nested mutable values would require a more detailed policy. Returning a fresh array of ids is another ownership boundary, separate from copying each record.

Test module wiring before diagnosing the implementation: does the exported factory exist, and does it create the expected methods? Then test independent instances, duplicate ids, missing lookups and mutation through every public boundary. A private Map is useful only if your API never hands out an alias that bypasses its rules.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Watch 29:34–53:47 for module setup, exports and import-time side effects. Use the current Node documentation for CommonJS versus ES module rules.