Modules, Node, and npm

Table of Contents

  1. Why Split Your Code
  2. Named Exports and Imports
  3. Default Exports
  4. Using Modules in the Browser
  5. Running JavaScript with Node
  6. npm and package.json
  7. Installing a Package
  8. npm Scripts and Versions
  9. 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.js holds project data. helpers.js holds small utility functions. You know where to look.
  • No accidental clashes. Two files can both have a variable called total without 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 .js ending.
  • import statements 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"
KindExportImport
Namedexport const a = 1;import { a } from "./file.js";
Defaultexport 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:

  1. import and export are allowed in the script.
  2. 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>.
  3. Each module has its own scope. A variable in main.js is not a global variable, and window.projects will 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:

  1. A folder called node_modules appears. It holds the package and everything it depends on, which can be hundreds of folders.
  2. package.json gets a "dependencies" section listing date-fns and its version.
  3. A file called package-lock.json records 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.

PartChanges whenSafe to update?
PATCH (3.6.1)A bug is fixedYes
MINOR (3.7.0)A feature is added, nothing breaksUsually
MAJOR (4.0.0)Something breaksRead 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

  1. Split the data. Create js/projects.js and js/helpers.js as shown. Import both into js/main.js and log each project title.
  2. Add a helper. Write and export sortByYear(list) in helpers.js. Use [...list].sort(...) so the original array is not changed. Import it and print the result.
  3. Break an import. Remove ./ from "./projects.js", or misspell an exported name. Read the error message in the browser console and again in Node.
  4. Try a default export. Create js/theme.js with the default export from section 3. Import it under a different name from the original.
  5. Make a package. Run npm init -y, add "type": "module", create .gitignore, and install any small package. Look inside node_modules, then delete the folder and restore it with npm install.
  6. 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!