Module 11 · Modules: import, export and How Files Find Each Other
Modules, the Picture: One File, One Scope, and What Crosses Between Files
In this lesson
- Split a program into files where each file keeps its own names, and share only what you
export. - Bring a name in with
import, and read the same idea in CommonJS asrequireandmodule.exports. - Explain why a module's code runs once, however many files import it.
Lima builds the website of a small bookshop. Everything lives in one file, app.js, now 900 lines long. In March she wrote a format function for prices. In June she added a date feature with its own format. She never noticed the clash.
// app.js, the price feature, written in March
function format(n) {
return "Tk " + n.toFixed(2);
}
// ... 600 lines later, the date feature, written in June
function format(d) {
return d.toISOString().slice(0, 10);
}
console.log(format(new Date(Date.UTC(2026, 10, 5))));
console.log(format(250));
2026-11-05
TypeError: d.toISOString is not a function
The date prints fine. Then the price line stops with TypeError: d.toISOString is not a function. The second format silently replaced the first, because one file has one set of top-level names. Every feature in the file shares them, so any two features can collide.
The cure is to give each feature its own file, with its own names. That is what a module is. This module teaches the whole system: what leaves a file, what comes in, and how Node and the browser find the files.
A module is a file with its own scope
A module is a file whose top-level names belong to that file alone. A const, a function or a class written at the top of a module is invisible to every other file. Module 6 called the space where a name can be seen its scope. A module is one more kind of scope: the file's own.
So two modules may both have a format. Neither knows about the other. A name leaves its file only when the file says so, and enters another file only when that file asks for it.
export says what may leave
The keyword export in front of a declaration makes that name public. Other files can now ask for it. Everything without export stays private to the file.
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
save("tax.mjs", `const rate = 15;
export function withTax(n) {
return n + (n * rate) / 100;
}
`);
save("app.mjs", `import { withTax } from "./tax.mjs";
console.log(withTax(200));
console.log(typeof rate);
`);
import("./app.mjs");
230
undefined
The first three lines and the save helper are plumbing. The Playground runs one file, so this program writes two module files next to itself and then starts app.mjs. The .mjs ending tells Node that a file is an ES module. Every program in this module uses the same trick.
Now read the two files. tax.mjs exports withTax and keeps rate to itself. app.mjs can call withTax, and withTax still sees rate, because a function keeps the scope it was made in (Module 6's closure). But app.mjs cannot see rate: typeof rate is "undefined" there.
import says what comes in
The other side is import. A line like import { withTax } from "./tax.mjs" names what it wants in braces, then the file it comes from. The file is given as a path: ./ means "in the same folder as me".
Two features can now each keep their own format. When one file needs both, it renames them as they come in with as.
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
save("price.mjs", `export function format(n) {
return "Tk " + n.toFixed(2);
}
`);
save("date.mjs", `export function format(d) {
return d.toISOString().slice(0, 10);
}
`);
save("app.mjs", `import { format as formatPrice } from "./price.mjs";
import { format as formatDate } from "./date.mjs";
console.log(formatDate(new Date(Date.UTC(2026, 10, 5))));
console.log(formatPrice(250));
`);
import("./app.mjs");
2026-11-05
Tk 250.00
This is Lima's program again, split into three files, and both lines are right. Inside price.mjs the function is still called format. Only app.mjs calls it formatPrice. The rename is local to the file that imports.
The older form: require and module.exports
import and export are ES modules, the standard since ES2015, often shortened to ESM. Node is older than that standard. It had its own module system first, called CommonJS, and it still runs it.
In CommonJS a file puts what it shares on an object called module.exports. Another file gets that object by calling require with a path. You have used require since Module 1: the starter's first line is require("fs").
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
save("price.js", `function format(n) {
return "Tk " + n.toFixed(2);
}
module.exports = { format };
`);
const price = require("./price.js");
console.log(price.format(250));
console.log(Object.keys(price));
Tk 250.00
[ 'format' ]
Same output, other form. Here price.js is a plain .js file and main.js loads it directly. require is an ordinary function call that returns the exports object.
Why your Playground programs use require
The Playground and the judge run one file, main.js, with node main.js. Node 22 reads such a file as CommonJS. If the file contains import or export, Node 22 notices and runs it as an ES module instead. Then require stops existing in that file.
That is why the starter uses require, and why a judged program never writes import. The two systems teach the same idea, and this module shows both. Lesson 02 has the full rules for how Node decides.
A module runs once
When a module is imported the first time, its top-level code runs, from top to bottom. Every later import of the same file gets the same module, already run. Its code does not run again.
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
save("logger.mjs", `console.log("logger.mjs runs");
export const lines = [];
`);
save("cart.mjs", `import { lines } from "./logger.mjs";
lines.push("cart ready");
`);
save("search.mjs", `import { lines } from "./logger.mjs";
lines.push("search ready");
`);
save("app.mjs", `import "./cart.mjs";
import "./search.mjs";
import { lines } from "./logger.mjs";
console.log(lines);
`);
import("./app.mjs");
logger.mjs runs
[ 'cart ready', 'search ready' ]
logger.mjs is imported three times and prints once. All three files share one lines array, so the two pushes land in one place. A module is a single shared thing, like an object that every importer holds a reference to. Lesson 05 shows the record Node keeps to make this work.
An import is a live view, not a copy
One more property sets ES modules apart. An imported name is not a copy of the value. It is a live view into the exporting module's variable. When that module changes the variable, every importer sees the change.
Step through the widget. The code on the left is app.mjs. The boxes are scopes: the global scope at the top, and one box per module under it.
Step 2 is the key picture: the name count sits in app.mjs, but its value lives in counter.mjs. Step 4 shows the view updating by itself. Step 5 is the privacy rule at work: step was never exported, so app.mjs cannot find it. CommonJS behaves differently in step 4, and Lesson 05 shows how.
Modules at a glance
// math.mjs, an ES module
export const PI = 3.14159; // a named export
export function area(r) { return PI * r * r; }
export default area; // the one default export
// app.mjs
import area, { PI } from "./math.mjs"; // the default, then named ones
import { area as circle } from "./math.mjs"; // a named import, renamed
import * as math from "./math.mjs"; // everything, as one object
// the same in CommonJS, the form the starter uses
module.exports = { PI, area }; // in math.js
const { PI, area } = require("./math.js"); // in app.js
The input is a list of prices. money.mjs knows how to print taka and the VAT rate. receipt.mjs builds the receipt and is the file's default export. main.js reads the input and hands it over.
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
const prices = fs.readFileSync(0, "utf8").split(/\s+/).filter(Boolean).map(Number);
save("money.mjs", `export const VAT = 0.15;
export function taka(n) {
return "Tk " + n.toFixed(2);
}
`);
save("receipt.mjs", `import { VAT, taka } from "./money.mjs";
export default function receipt(prices) {
const sub = prices.reduce((s, p) => s + p, 0);
return [
"items " + prices.length,
"subtotal " + taka(sub),
"vat " + taka(sub * VAT),
"total " + taka(sub * (1 + VAT)),
];
}
`);
import("./receipt.mjs").then((mod) => console.log(mod.default(prices).join("\n")));
items 3
subtotal Tk 500.00
vat Tk 75.00
total Tk 575.00
import() returns the module's namespace, so the default export arrives as mod.default. The receipt is an array of lines, and main.js joins and prints it. Only receipt.mjs knows about money.mjs; main.js never imports it.
A module can hold state that no other file can touch. Here ids.mjs keeps a private counter and exports only a function that hands out the next id.
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
save("ids.mjs", `let last = 100;
export function nextId() {
last += 1;
return "ORD-" + last;
}
`);
save("app.mjs", `import { nextId } from "./ids.mjs";
import * as ids from "./ids.mjs";
console.log(nextId(), nextId());
console.log(Object.keys(ids));
console.log(ids.last);
`);
import("./app.mjs");
ORD-101 ORD-102
[ 'nextId' ]
undefined
The namespace ids lists only nextId. ids.last is undefined, because last was never exported. Nobody outside can reset the counter by mistake. Module 6 built the same privacy with a closure, and a module gives it to a whole file.
Where this is used
- Node.js: its built-in modules load by name, as
require("node:fs")in CommonJS orimport fs from "node:fs"in an ES module. - Vite: in development it serves each of your source files to the browser as its own ES module, so a change reloads one file.
- Chrome, Firefox and Safari: they load
<script type="module">and follow itsimportlines themselves, fetching one file per module. - Deno: it runs ES modules first, imported by path or by URL.
Common mistakes
1. Mixing require and import in one file
const fs = require("fs");
import path from "path";
console.log(path.basename("a/b.txt"));
ReferenceError: require is not defined in ES module scope, you can use import instead
Node 22 sees the import and runs the file as an ES module. In an ES module require does not exist, so the first line stops with ReferenceError: require is not defined in ES module scope, you can use import instead. Pick one system per file. In a Playground program, that means require.
2. Importing a name that was not exported
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
save("tax.mjs", `const rate = 15;
export function withTax(n) {
return n + (n * rate) / 100;
}
`);
save("app.mjs", `import { rate } from "./tax.mjs";
console.log("app.mjs runs");
`);
import("./app.mjs").catch((err) => console.log(err.name + ": " + err.message));
SyntaxError: The requested module './tax.mjs' does not provide an export named 'rate'
The program catches the error and prints it. Notice what is missing: "app.mjs runs" never prints. Node checks every import against the exports before any line of app.mjs runs. The fix is to export rate, or to stop asking for it.
3. Assigning to exports instead of module.exports
const fs = require("fs");
const path = require("path");
const save = (name, text) => fs.writeFileSync(path.join(__dirname, name), text);
save("price.js", `function format(n) {
return "Tk " + n.toFixed(2);
}
exports = { format };
`);
const price = require("./price.js");
console.log(price);
console.log(typeof price.format);
{}
undefined
In CommonJS, exports is only a shortcut name for module.exports. Assigning a new object to exports points the shortcut elsewhere, and require still returns the old, empty object. Write module.exports = { format };, or add to the shortcut with exports.format = format;.
Nadia reviews pull requests for a shop's code. Before she reads a new module, she wants its public face: the names other files can import. She asks you for a tool that reads a module's source and lists what it exports.
Input. The first line holds n. The next n lines are the module's source, one line each. A line that starts with export and a space is an export line. Its forms are export const NAME = ..., export let NAME = ..., export function NAME(..., export class NAME ... and export default .... The last form is export { ... };, a comma-separated list where each item is name or name as other.
Output. The exported names in the order they appear, one per line. A default export prints as default. If the module exports nothing, print none.
Constraints. 1 <= n <= 200; Each line is at most 80 characters. Every name is a JavaScript identifier. The last line is never empty..
Sample.
7
import { readFileSync } from "node:fs";
const rate = 15;
export const tax = (amount) => (amount * rate) / 100;
function round(x) { return Math.round(x); }
export function total(a, b) { return round(a + b); }
export { round as roundPrice, rate };
export default total;
tax
total
roundPrice
rate
default
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Run in Compiler
Graded as export-names, a free problem in this module's problem set.
Kenji's app has a db.js module that opens a database connection when it runs. Ten files import it, and Kenji fears ten connections. You will replay the app's imports and show him what really runs.
Input. The first line holds n. The next tokens are n module names, the imports in the order the app makes them. Names are case-sensitive, as file names are on Linux.
Output. One line per import: run NAME if this is the first import of that module, or cached NAME if it ran before. Then one last line: ran K, where K is how many modules ran.
Constraints. 1 <= n <= 1000; Each name is 1 to 20 characters: letters, digits, dots, dashes or underscores..
Sample.
6
db.js cart.js db.js auth.js cart.js db.js
run db.js
run cart.js
cached db.js
run auth.js
cached cart.js
cached db.js
ran 3
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Run in Compiler
Graded as run-once, a free problem in this module's problem set.
Sumon cleans up a library. He has the list of every export and the list of every import in the project. An export that nobody imports is dead weight. A namespace import, import * as, may use any export, so it keeps all of that module's exports.
Input. e, then e exports as module name. Then i, then i imports as module name, where name * is a namespace import of that module.
Output. every export that is never imported, in the input order, as module name. If every export is used, print none.
Constraints. 1 <= e, i <= 1000. Module names and export names are 1 to 10 letters or digits.
Sample. the input below.
5
tax rate
tax withTax
money taka
money VAT
date format
3
tax withTax
money *
cart total
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Sample output.
tax rate
date format
Run in Compiler
Common doubts
Why does this module use .mjs files?
The ending tells Node that a file is an ES module, with no setting needed. A
.jsfile can be an ES module too, when the nearestpackage.jsonsays"type": "module". Lesson 02 lists every rule. Here.mjskeeps the examples short and clear.Is a module the same as a library or a package?
No. A module is one file. A package is a folder of modules with a
package.json, shared through npm. A library is a loose word for code you reuse, often one package.Can a module change a variable it imported?
No.
count = 5in the importing file throws aTypeError, because an import is a read-only view. Only the module that owns the variable may change it, asincdid in the widget.Which system should I learn first?
Both, because you will meet both. New code uses
importandexport, and the browser understands only them. Much of Node and npm still usesrequire, and so does every judged program here.
Key takeaways
- A module is a file with its own scope; its top-level names are private unless exported.
exportmarks what may leave a file;import { name } from "./file.mjs"brings it in, andasrenames it.- CommonJS does the same with
module.exportsandrequire, the form the starter uses. - A module's code runs once, at its first import; every importer shares the same module.
- An ES import is a live, read-only view into the exporting module's variable.
- Go deeper: Under the Hood, Live Bindings, the Three Phases, the Module Wrapper and the Bundler (Pro).
Next: every rule of the system, one by one, with what each gives you.
End of lesson 1
Mark it done, and your progress moves with you.
Next: Every Module Rule, One by One: Exports, Imports, Paths and the Two Systems