CodingNic

Modules & Code Organization

Organizing a Small Project Across Files

Modules & Code Organization 20 min read

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 import and export syntax 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.

javascript
// 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.

javascript
// 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.

javascript
// 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.

javascript
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.

javascript
// utils/math.js
export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
}
javascript
// utils/string.js
export function shout(text) {
  return text.toUpperCase() + "!";
}
javascript
// 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:

javascript
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

  1. Create a file counter.js with a private count variable and exported functions increment(), decrement(), and getCount(). Confirm count itself isn’t accessible from another file that imports from it.
  2. Create a second file that imports from counter.js, calls increment() twice, and logs the result of getCount().
  3. Create two small files, each exporting one or two related functions (your choice), and a third index.js that 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.