Organizing a Small Project Across Files
Objectives
By the end of this chapter, you should be able to:
- Split a small project into files with clear, separate responsibilities
- Explain why a module’s non-exported variables stay private
- Re-export from a single “barrel” file to simplify imports elsewhere
💡 Why this matters: Knowing
importandexportsyntax is one thing. Deciding how to actually split a project into files is the more useful skill, this lesson walks through doing it for something small and realistic.
A Project Split by Responsibility
Take a small shopping cart feature. Instead of one file handling storage, display, and setup all together, split it by what each part actually does.
// cart.js
let items = [];
export function addItem(item) {
items.push(item);
}
export function removeItem(index) {
items.splice(index, 1);
}
export function getItems() {
return [...items];
}
cart.js owns the cart’s data and nothing else, it doesn’t know or care how that data gets displayed.
// ui.js
export function renderCart(items) {
if (items.length === 0) return "Cart is empty";
return items.map((item, i) => `${i + 1}. ${item}`).join("\n");
}
ui.js owns turning data into something displayable, it doesn’t know or care where that data came from. In a real page, renderCart() would build DOM elements (Module 1) instead of returning a string, the separation of concerns is the point here, not the specific output.
// main.js
import { addItem, getItems } from "./cart.js";
import { renderCart } from "./ui.js";
addItem("Apples");
addItem("Bread");
console.log(renderCart(getItems()));
// 1. Apples
// 2. Bread
main.js ties the two together. Neither cart.js nor ui.js imports the other, they’re independent, main.js is the only file that knows about both. This is a common, useful shape: focused files that don’t depend on each other directly, combined by one file that does.
Module Scope Keeps Things Private
items in cart.js is never exported. That’s not an oversight, it’s the entire point of a module’s private scope: nothing outside cart.js can reach items directly, only through the exported functions that control access to it.
import * as cart from "./cart.js";
console.log(cart.items);
// undefined, "items" was never exported
This matters for the same reason encapsulation matters in a class (Course 1, OOP): cart.js can change how items is stored internally, an array, something else entirely, without breaking any file that imports from it, as long as addItem(), removeItem(), and getItems() keep working the same way.
Re-Exporting from a Barrel File
Once a project has several small files, importing from each one individually gets verbose. A barrel file collects and re-exports pieces from several modules through one file.
// utils/math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// utils/string.js
export function shout(text) {
return text.toUpperCase() + "!";
}
// utils/index.js
export { add, subtract } from "./math.js";
export { shout } from "./string.js";
Now anything that needs a mix of these utilities imports from one path instead of three:
import { add, subtract, shout } from "./utils/index.js";
console.log(add(2, 3));
// 5
console.log(shout("hi"));
// HI!
utils/index.js doesn’t define anything itself, it only re-exports. This is a common enough pattern that many bundlers and tools automatically treat index.js as the default file a folder path resolves to, so "./utils" alone often works the same as "./utils/index.js".
Try It
- Create a file
counter.jswith a privatecountvariable and exported functionsincrement(),decrement(), andgetCount(). Confirmcountitself isn’t accessible from another file that imports from it. - Create a second file that imports from
counter.js, callsincrement()twice, and logs the result ofgetCount(). - Create two small files, each exporting one or two related functions (your choice), and a third
index.jsthat re-exports from both. Import everything from that one barrel file in a fourth file and use it.
Recap
- Splitting a project by responsibility, one file per clear job, keeps each file focused and independent of the others.
- A module’s non-exported variables are private to that file, only reachable through whatever functions the module chooses to export, the same idea as encapsulation in a class.
- A barrel file re-exports pieces from several modules through one file, simplifying imports for anything that needs a mix of them.
Next lesson: dynamic imports, loading a module only when you actually need it.