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.
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.
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:
// catalog.mjs
export function createCatalog(rows) { /* implementation */ }A separate consumer imports the named export:
// 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.
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.