Modules, Node, and npm
Table of Contents
- Why Split Your Code
- Named Exports and Imports
- Default Exports
- Using Modules in the Browser
- Running JavaScript with Node
- npm and package.json
- Installing a Package
- npm Scripts and Versions
- Try It Yourself
1. Why Split Your Code
In the last chapter you built arrays of objects and functions to work with them. If you keep adding to one file, it grows into a long page that is hard to read, hard to search, and hard to change without breaking something.
The fix is to split code into modules. A module is simply a file that decides which of its values are shared with other files and which stay private. Good modules give you:
- Clear names.
projects.jsholds project data.helpers.jsholds small utility functions. You know where to look. - No accidental clashes. Two files can both have a variable called
totalwithout interfering with each other. - Reuse. One function can be imported by many files.
JavaScript's built-in module system is called ES modules (ESM). It uses two keywords: export to share something, and import to use something shared by another file.
2. Named Exports and Imports
Dara's project data is currently inside one script. Move it into its own file. Inside the dara-profile folder, create a js folder and a file js/projects.js:
export const projects = [
{ id: 1, title: "Personal Profile", tags: ["html", "css"], year: 2025 },
{ id: 2, title: "Budget Tracker", tags: ["javascript"], year: 2026 },
{ id: 3, title: "Weather Card", tags: ["html", "javascript"], year: 2026 },
];
The word export in front of const makes projects available to other files. Now create js/helpers.js with two small functions:
export function formatYear(project) {
return `${project.title} (${project.year})`;
}
export function filterByTag(list, tag) {
return list.filter((project) => project.tags.includes(tag));
}
A file can have as many named exports as it likes. To use them, write an import statement in a third file, js/main.js:
import { projects } from "./projects.js";
import { formatYear, filterByTag } from "./helpers.js";
const jsProjects = filterByTag(projects, "javascript");
for (const project of jsProjects) {
console.log(formatYear(project));
}
Three rules to remember:
- The names inside
{ }must match the exported names exactly. - The path must start with
./(same folder) or../(parent folder), and you must include the.jsending. importstatements go at the top of the file.
You can rename on the way in with as, which helps when two modules export the same name:
import { formatYear as formatProject } from "./helpers.js";
3. Default Exports
A module can also have one default export: the main thing the file offers.
// js/theme.js
export default function toggleTheme(current) {
return current === "dark" ? "light" : "dark";
}
You import a default export without braces, and you can choose any name for it:
import toggleTheme from "./theme.js";
console.log(toggleTheme("light")); // "dark"
| Kind | Export | Import |
|---|---|---|
| Named | export const a = 1; | import { a } from "./file.js"; |
| Default | export default a; | import anyName from "./file.js"; |
Which should you use? Named exports are the safer habit for beginners, because the name is fixed and your editor can autocomplete it. Use a default export when a file has exactly one main purpose.
4. Using Modules in the Browser
To use modules in index.html, change the script tag so it has type="module":
<script type="module" src="js/main.js"></script>
This one attribute changes three things:
importandexportare allowed in the script.- The script is deferred automatically: it runs after the HTML is parsed, so you can put the tag in
<head>or at the end of<body>. - Each module has its own scope. A variable in
main.jsis not a global variable, andwindow.projectswill not exist.
There is one catch. Browsers refuse to load modules from a file:/// address for security reasons. If you double-click index.html, the console shows an error about CORS. Open the page with Live Server instead, so the address starts with http://127.0.0.1:5500. This is the same client and server idea from How the Web Works: modules are fetched with HTTP requests, so you need a server, even a local one.
Open DevTools and select the Network tab, then reload. You will see three requests: main.js, projects.js, and helpers.js. The browser reads the import lines and fetches each file it needs.
5. Running JavaScript with Node
The browser is not the only place JavaScript runs. Node.js is a program that runs JavaScript on your computer, with no web page. It uses the same engine as Chrome. Check that it is installed:
node --version
Create a file hello.mjs in any folder:
const name = "Dara";
console.log(`Hello from Node, ${name}!`);
Run it:
node hello.mjs
The .mjs extension tells Node "this file is an ES module." If you prefer the plain .js extension, there is a second way, which we set up in the next section. Notice that console.log prints to your terminal here, not the browser console. Node is how you will run tests, build tools, and servers later.
Your project modules use only plain JavaScript and no browser features, so Node can run them too, once it knows they are modules (see below).
Node lacks browser objects such as document and window. Code that touches the page belongs in the browser; code that only works with data can run in both.
6. npm and package.json
Other programmers share their code as packages. npm (Node Package Manager) is installed together with Node, and it downloads packages from a public registry at npmjs.com.
Every project that uses npm has a file named package.json that describes it. Create one in the dara-profile folder:
npm init -y
The -y flag accepts the default answers. Open the new file and add the "type" line:
{
"name": "dara-profile",
"version": "1.0.0",
"type": "module",
"description": "Dara Sok's personal profile page",
"scripts": {}
}
"type": "module" tells Node to treat every .js file in this project as an ES module, so node js/main.js now works without renaming anything. Run it and you should see the two project titles printed in your terminal:
Budget Tracker (2026)
Weather Card (2026)
JSON is strict: keys and strings use double quotes, and no trailing commas are allowed.
7. Installing a Package
Suppose Dara wants to show dates like "3 months ago." Instead of writing that logic, she installs a package. Install date-fns:
npm install date-fns
Three things happen:
- A folder called
node_modulesappears. It holds the package and everything it depends on, which can be hundreds of folders. package.jsongets a"dependencies"section listingdate-fnsand its version.- A file called
package-lock.jsonrecords the exact versions installed, so every computer gets the same result.
Now use the package in Node:
import { format } from "date-fns";
console.log(format(new Date(2026, 0, 15), "dd MMM yyyy"));
Imports without ./ mean "look in node_modules." The program prints 15 Jan 2026.
Never commit node_modules. It is large, and anyone can recreate it from package.json. Tell Git to ignore it by creating a file named .gitignore with this line:
node_modules
Commit package.json and package-lock.json. When a teammate (or you, on another computer) clones the project, one command restores everything:
npm install
Packages that you only need while developing (testing tools, formatters) are installed with npm install --save-dev <name> and appear under "devDependencies".
8. npm Scripts and Versions
The "scripts" section stores commands under short names. Edit package.json:
"scripts": {
"start": "node js/main.js",
"hello": "node hello.mjs"
}
Run them with npm run:
npm run hello
npm start
npm start is a special case that needs no run. Scripts become more valuable as commands grow longer, and they give every project the same simple entry points.
Package versions look like ^3.6.0. This is semantic versioning (semver): MAJOR.MINOR.PATCH.
| Part | Changes when | Safe to update? |
|---|---|---|
| PATCH (3.6.1) | A bug is fixed | Yes |
| MINOR (3.7.0) | A feature is added, nothing breaks | Usually |
| MAJOR (4.0.0) | Something breaks | Read the notes first |
The ^ in front means "accept new minor and patch versions, but never a new major one." Because of that, npm install will not surprise you with breaking changes.
9. Try It Yourself
- Split the data. Create
js/projects.jsandjs/helpers.jsas shown. Import both intojs/main.jsand log each project title. - Add a helper. Write and export
sortByYear(list)inhelpers.js. Use[...list].sort(...)so the original array is not changed. Import it and print the result. - Break an import. Remove
./from"./projects.js", or misspell an exported name. Read the error message in the browser console and again in Node. - Try a default export. Create
js/theme.jswith the default export from section 3. Import it under a different name from the original. - Make a package. Run
npm init -y, add"type": "module", create.gitignore, and install any small package. Look insidenode_modules, then delete the folder and restore it withnpm install. - Write two scripts. Add
"start"and"hello"scripts and run both. Then check the Network tab to confirm the browser loads your three module files.
Next, we'll connect these modules to the page itself, using the DOM and events to make Dara's project list filterable.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!