Modules & Project Layout
Everything so far lived in one file - fine for ten lines, but it falls apart fast for anything real. Programs grow, and you want related code grouped into files you can find, reuse, and reason about separately. The tool for that is modules. This phase sets the stage for the async and browser work in Phase 6.
The mental model: each file is a module
A module is a .js file that keeps its contents private by default and explicitly shares (exports)
the pieces other files may use; other files then import exactly what they need. Nothing leaks between
files unless you say so.
This system is called ES modules (or "ESM" - the official, modern standard built into the language).
Two keywords run it: export to share, import to borrow.
📝 Terminology. You may also hear about CommonJS (require(...) / module.exports), the older
system Node used for years and that you'll still meet in existing code. We teach ES modules since
they're the standard going forward and work in both browser and Node. Recognize require; write
import.
export and import
Let's split a tiny program into two files. First, a file that provides some helpers:
// math.js
What just happened: The export keyword marks add and PI as the parts of this file other files may
use. Anything without export (a helper variable, say) stays private to math.js, invisible from
outside. The file is now a reusable module with a clear public surface.
Now a file that uses them:
// main.js
;
;
5
3.14159
What just happened: import { add, PI } from "./math.js" reached into math.js and pulled out the two
exported names. The ./ at the front means "a file right next to me" (a relative path), resolving the
same way regardless of which folder you run the program from. ES modules in Node need the .js
extension. Run with node main.js and Node loads math.js automatically because main.js asked for it. (For this to run, your project needs the "type": "module" setting covered in the next section - or, to skip setup entirely and run right now, name the files math.mjs/main.mjs: the .mjs extension tells Node a file is a module all on its own, with no package.json.)
Here's the relationship as a picture - a small module graph:
Reading it: arrows point from a file to the files it depends on. main.js is the entry point; it pulls
in math.js and dates.js, and math.js pulls in utils.js. Node starts at your entry file and follows
these arrows, loading each module once. This graph is your program's structure - a clean tree, not a
tangle where everything imports everything, is most of what "good architecture" means here.
Default exports
A second flavor of export, default, is for when a file's main purpose is to provide one thing:
// greet.js
// main.js
// no curly braces for a default
;
Hello, Ada!
What just happened: A file can have one export default. Import it without curly braces and pick any
name for it on the importing side. Use a default when a module is really "about" one thing (a single
function or class); use named { ... } exports when a file offers several helpers. Plenty of code mixes
both.
What package.json is
The moment a project is more than a couple of loose files, it gets a package.json - a small file at
the project root describing the project. Create one by running:
$ npm init -y
Wrote to /home/ada/my-project/package.json
What just happened: npm init -y created a starter package.json with sensible defaults (-y says
"yes to all the prompts"). npm is Node's package manager, shipped with Node - more in
Phase 8. For an ES module project you want it to look roughly like this:
What just happened: This file is your project's ID card and control panel. The fields that matter early:
"type": "module"- tells Node to treat your.jsfiles as ES modules soimport/exportwork. ⚠️ Without this line, Node assumes the old CommonJS system and yourimportstatements throwSyntaxError: Cannot use import statement outside a module. If you hit that error, this missing line is almost always why (npm init -ydoesn't always add it - set it yourself)."scripts"- named shortcuts run withnpm run <name>(e.g.npm run start), saving you retyping long commands and documenting how the project is meant to run."dependencies"- the outside packages your project uses, filled in as you install them.
What node_modules is
Installing an outside package (npm install some-package) makes npm download it - and everything it
depends on - into a folder called node_modules at your project root, recording the package in
package.json.
⚠️ node_modules is huge and disposable - never commit it. It can hold thousands of files, and it's
fully rebuildable: anyone with your package.json can recreate it via npm install. List it in
.gitignore and leave it out of version control. The recipe (package.json and its lockfile) is what
you track; the downloaded result (node_modules) is not. New developers clone the repo, run
npm install, and node_modules reappears.
💡 Key point. The split is the whole idea: package.json is the recipe you keep; node_modules is
the meal you can always re-cook. Track the recipe, ignore the meal.
A sane small project layout
You don't need an elaborate structure to start. Here's a layout that scales from tiny to medium without ceremony:
my-project/
package.json the recipe: name, scripts, dependencies
package-lock.json exact versions npm installed (commit this)
.gitignore lists node_modules/ so Git ignores it
node_modules/ downloaded packages (ignored, rebuildable)
src/ your actual code lives here
main.js the entry point you run
math.js a module of related helpers
dates.js another module
README.md what this project is and how to run it
Reading it: the principle is "code in src/, config at the root, downloaded stuff ignored." main.js
is your entry point - the file you run, the root of the module graph. As the project grows, add more
files under src/ (and eventually subfolders that group related modules), but the shape stays the same.
Resist inventing structure you don't need yet - let folders appear when the code calls for them.
Recap
- A module is a file that keeps its contents private and shares only what it
exports; other files pull pieces in withimport. - Named exports (
export const x→import { x }) for several helpers; default export (one per file, no braces on import) when a file is about one thing. Use relative paths with the.jsextension, e.g.from "./math.js". package.jsonis your project's recipe - set"type": "module"soimport/exportwork, definescripts, and letdependenciestrack outside packages.node_modulesholds downloaded packages; huge, rebuildable withnpm install, never committed - gitignore it.- A sane layout keeps code in
src/, config at the root,node_modulesignored - grow it only as the code demands.
Next: what makes JavaScript truly distinctive - doing things that take time (network calls, timers, clicks) without freezing, and reaching into a live web page from your code.
← Phase 4: Control Flow & Functions · Guide overview · Phase 6: Async & the DOM →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. A JavaScript module...
2. The node_modules folder should be...