Free Handbook · Runs in your browser

Modules & Packages

ES modules with import and export, the older CommonJS require, npm and package.json, the Node.js versus browser split, and a tour of the standard library you get for free — plus the two module errors every beginner hits.

0 / 111 lessons🔥 0 day streak
ShareXLinkedIn

Module 05 · what you'll be able to do

  • Split code into files with export and import, default and named
  • Recognise CommonJS (require / module.exports) and know when you will still see it
  • Start a project with npm init, install packages, and read package.json
  • Know which globals exist in Node, which in the browser, and which in both
  • Find your way around the built-in modules: fs, path, crypto, URL, timers
01

ES modules: export and import

A module is a file. Everything in it is private unless you export it, and other files get it with import. This is the modern system (ESM), understood by every browser, Node.js and bundler. Modules are strict mode by default, run once no matter how many files import them, and their imports are resolved before any code runs — which is why import must be at the top level.

javascriptmath.js
// Named exports — as many as you like
export const PI = 3.14159
export function area(r) {
  return PI * r * r
}

// A default export — one per file, imported under any name
export default function circumference(r) {
  return 2 * PI * r
}

// Or export a list at the bottom
function helper() {}
export { helper as internalHelper }
javascriptmain.js
import circumference, { PI, area } from "./math.js"     // default + named
import * as math from "./math.js"                       // everything, namespaced
import { area as circleArea } from "./math.js"          // rename

console.log(PI, area(2), circumference(2), math.PI, circleArea(1))

// Lazy loading: import() returns a promise (Module 06)
const { default: heavy } = await import("./heavy-chart.js")

// Node built-ins use the node: prefix; packages use their bare name
import { readFile } from "node:fs/promises"
import express from "express"

Relative paths need ./ and, in Node and browsers, the .js extension. Bundlers (Vite, webpack) relax that — but write it and everything works everywhere.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
12.57 undefined
3
Error you will hit

SyntaxError: Cannot use import statement outside a module

javascript
// app.js, run with: node app.js
import { readFile } from "node:fs/promises"
SyntaxError: Cannot use import statement outside a module
Why the engine said that

Node treated the file as CommonJS (the old system) because nothing told it otherwise. In a browser, the same message appears when a <script> tag lacks type="module".

The fix

Node: add "type": "module" to package.json, or name the file .mjs. Browser: <script type="module" src="app.js">.

02

CommonJS: require and module.exports

Before ESM, Node invented its own module system: require() to load, module.exports to expose. You will meet it in older tutorials, most of npm, config files (webpack.config.js), and any project without "type": "module". New code should be ESM; you still need to read both.

ES modules (write this)

  • export const x = 1
  • export default fn
  • import { x } from "./m.js"
  • Static: analysed before running, tree-shakeable
  • Async by nature; top-level await works

CommonJS (read this)

  • module.exports.x = 1 or exports.x = 1
  • module.exports = fn
  • const { x } = require("./m")
  • Dynamic: require is a normal function call, runs synchronously
  • No top-level await; __dirname and __filename exist
javascriptlegacy.cjs
const fs = require("fs")
const { join } = require("path")

function readConfig(dir) {
  return JSON.parse(fs.readFileSync(join(dir, "config.json"), "utf8"))
}

module.exports = { readConfig }

// Mixing: ESM can import CommonJS (default import gets module.exports);
// CommonJS can only require ESM via dynamic import() from Node 22.
03

npm and package.json

npm is the package registry (two million packages) and the command-line tool that installs them. package.json is your project's manifest: name, scripts, dependencies. node_modules/ is where packages land — never commit it; package-lock.json pins exact versions so a teammate gets the same tree.

bash
mkdir my-app && cd my-app
npm init -y                       # creates package.json
npm install express               # dependency → "dependencies"
npm install -D vitest eslint      # dev-only → "devDependencies"
npm run test                      # runs scripts.test
npx prettier --write .            # run a package binary without a global install
npm outdated && npm update        # keep up
npm ls --depth=0                  # what is installed
jsonpackage.json
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch src/server.js",
    "test": "vitest",
    "lint": "eslint ."
  },
  "dependencies": {
    "express": "^5.1.0"
  },
  "devDependencies": {
    "eslint": "^9.0.0",
    "vitest": "^3.0.0"
  },
  "engines": { "node": ">=20" }
}

^5.1.0 means "any 5.x at or above 5.1.0" — patch and minor updates allowed, no major. ~5.1.0 allows patches only. An exact 5.1.0 pins.

Semantic versioning
MAJOR.MINOR.PATCH. Major = breaking changes, minor = new features, patch = fixes. The ^ in package.json trusts non-breaking updates.
package-lock.json
The exact resolved versions of every package and sub-package. Commit it. npm ci installs exactly what it says — what CI should run.
npx
Runs a package binary from node_modules (or downloads it temporarily). npx create-vite, npx tsc.
pnpm / yarn / bun
Alternatives to npm with the same package.json. pnpm saves disk with a shared store; bun is a fast runtime + package manager. The registry is the same.
Error you will hit

Error: Cannot find module 'express'

javascript
import express from "express"
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'express' imported from /app/src/server.js
Why the engine said that

The package is not in node_modules: you cloned a repo and never ran npm install, or installed it in a different folder, or it is in devDependencies and you deployed with --omit=dev.

The fix

npm install in the folder with package.json. If the module is your own file, the path needs ./ and the extension.

04

Node.js vs the browser

Same language, two hosts. The browser gives you window, document, the DOM, fetch, localStorage — and no file system. Node.js gives you process, fs, path, network servers — and no DOM. Since Node 18 the overlap is large: fetch, URL, TextEncoder, crypto.randomUUID, timers and console exist in both.

BrowserNode.jsBoth
Global objectwindowglobalglobalThis
I/ODOM, fetch, localStoragefs, process.stdin, httpfetch, console, URL
Modules<script type="module">ESM or CommonJSimport / export
Asyncevents, timers, fetchevents, timers, streamsPromises, async/await
Secretsnever — the user can see everythingprocess.env, .env files
Runs wherethe user's machineyour server / CI / laptop
javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
undefined true
object function function
not a page
/a/b
a%20b%26c aGk= hi
2
05

Standard library tour

JavaScript's built-in objects are small compared with Python's standard library — that is what npm is for — but Node adds a solid core. Here is what you get without installing anything.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
5 3 1.5
0 true
{"a":[1]} { deep: { x: 1 } }
a b c true
₹1,23,456.70
1 Jan 1970
ehllo [ 0, 1, 2 ]
uuid ok
javascriptnode-core.js
import { readFile, writeFile, readdir, mkdir } from "node:fs/promises"
import { join, extname, basename } from "node:path"
import { createServer } from "node:http"
import { randomUUID, createHash } from "node:crypto"
import { setTimeout as sleep } from "node:timers/promises"
import { parseArgs } from "node:util"
import { EventEmitter } from "node:events"

const text = await readFile("notes.txt", "utf8")
await writeFile(join("out", "copy.txt"), text)
console.log((await readdir(".")).filter(f => extname(f) === ".js").map(basename))

console.log(randomUUID(), createHash("sha256").update("hello").digest("hex").slice(0, 8))
await sleep(100)

const { values } = parseArgs({ options: { port: { type: "string", default: "3000" } } })
createServer((req, res) => res.end("ok")).listen(Number(values.port))

const bus = new EventEmitter()
bus.on("saved", id => console.log("saved", id))
bus.emit("saved", 42)

// node --test runs *.test.js files with the built-in runner
import { test } from "node:test"
import assert from "node:assert/strict"
test("adds", () => assert.equal(1 + 1, 2))

The node: prefix is optional but makes it obvious what is core versus npm. Node 22 also has a built-in test runner, --watch, --env-file and a stable fetch.

JuniorWhat is the difference between dependencies and devDependencies?

dependencies are needed at runtime in production (express, a database driver). devDependencies are needed only to build or test (eslint, vitest, TypeScript). npm install --omit=dev in production skips the second group, which keeps images small. Front-end apps that are bundled often put everything in devDependencies because the built output has no runtime deps.

Finish the JavaScript handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.