Node.js and npm Foundation
npm and packages
Install, update and remove packages with npm, read package.json and package-lock.json, understand version ranges, write npm scripts, share code with workspaces, see what publishing would send, and keep the packages you install safe.
What npm is
A package is a folder of code with a package.json file, published so other people can use it. Every ZudoJS module you will use later is a package, and so are most of the tools a JavaScript developer runs.
npm is two things with one name:
- the registry at npmjs.com, a huge public library of packages, and
- the npm command, installed with Node.js, which downloads packages from the registry into your project, runs your scripts and publishes your own packages.
Every example in this lesson is a real session in a terminal. Make a new project to follow along:
mkdir task-tools cd task-tools npm init -y Wrote to ~/task-tools/package.json: … npm pkg set type=module
Installing a package
Suppose your tasks need due dates like "in 2 days". Converting that to milliseconds is fiddly, and a small, popular package called ms already does it. Before installing anything, look it up. npm view shows what the registry knows about a package:
npm view ms ms@2.1.3 | MIT | deps: none | versions: 32 Tiny millisecond conversion utility https://github.com/vercel/ms#readme … dist-tags: latest: 2.1.3 …
It has no dependencies of its own (deps: none), a clear description, a link to its source code, and an MIT licence. Now install it:
npm install ms added 1 package, and audited 2 packages in 20s found 0 vulnerabilities
Three things changed in your folder:
node_modules/msnow holds the package's code. Node.js looks innode_moduleswhen you import a name without./.package.jsonhas a new section,"dependencies": { "ms": "^2.1.3" }.- A new file,
package-lock.json, appeared. You will see why below.
Use the package like any module:
import ms from "ms";
console.log(ms("2 days"));
console.log(ms("1.5h"));
console.log(ms(90_000));
console.log(ms(90_000, { long: true }));
node due.js 172800000 5400000 2m 2 minutes
Text in, milliseconds out, and the other way round. 90,000 ms is 1.5 minutes, which ms rounds to 2m. Reading a package's README tells you about details like that before they surprise you.
NOTE
Never commitnode_modules. It can hold thousands of files, and anyone can rebuild it from package.json and package-lock.json with one command. Put node_modules/ in .gitignore, next to .env.package.json and dev dependencies
Some packages are tools you only need while writing code: a formatter, a test runner, TypeScript. Install those as dev dependencies with -D (short for --save-dev). A server in production installs only the normal dependencies, which keeps it smaller and gives attackers less code to aim at. Prettier formats code consistently:
npm install -D prettier added 1 package, and audited 3 packages in 5s 1 package is looking for funding run `npm fund` for details found 0 vulnerabilities
"Looking for funding" just means the authors accept donations; npm fund lists where. After you add a few scripts in the next section, package.json looks like this:
{
"name": "task-tools",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node due.js",
"format": "prettier --write .",
"check-format": "prettier --check ."
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"ms": "^2.1.3"
},
"devDependencies": {
"prettier": "^3.9.9"
}
}
- name and version identify the package. They only matter much if you publish it.
- type:
"module"makes.jsfiles ES modules. - main (or the newer exports) says which file another project gets when it imports this package.
- scripts are named commands, run with
npm run. - dependencies are needed to run the program; devDependencies only to develop it.
- license says what others may do with the code.
ISCandMITare both short, permissive licences.
package.json is plain JSON, so your own code can read it too. Tools do this all the time, for example to print a version number. This program lists the dependencies of the package.json above:
import { readFile } from "node:fs/promises";
const pkg = JSON.parse(await readFile("package.json", "utf8"));
console.log(`${pkg.name} ${pkg.version}`);
for (const section of ["dependencies", "devDependencies"]) {
for (const [name, range] of Object.entries(pkg[section] ?? {})) {
console.log(` ${section}: ${name} ${range}`);
}
}
node deps.jstask-tools 1.0.0 dependencies: ms ^2.1.3 devDependencies: prettier ^3.9.9
npm scripts
The scripts section gives names to the commands your project uses, so nobody has to remember them. You can edit package.json by hand, or let npm pkg do it. These commands add the three scripts you saw above and remove the placeholder test script. They print nothing when they work:
npm pkg set scripts.start="node due.js" scripts.format="prettier --write ." scripts.check-format="prettier --check ." npm pkg delete scripts.test
To give the formatter something to do, save this badly formatted file as messy.js:
const x = {a:1,
b:2}
console.log( x )
npm run on its own lists the scripts. Then run them:
npm run Lifecycle scripts included in task-tools@1.0.0: start node due.js available via `npm run`: format prettier --write . check-format prettier --check . npm start > task-tools@1.0.0 start > node due.js 172800000 5400000 2m 2 minutes npm run check-format > task-tools@1.0.0 check-format > prettier --check . Checking formatting... [warn] messy.js [warn] Code style issues found in the above file. Run Prettier with --write to fix. npm run format > task-tools@1.0.0 format > prettier --write . due.js 94ms (unchanged) messy.js 15ms package-lock.json 14ms (unchanged) package.json 3ms (unchanged)
- A few names are special:
npm startandnpm testwork withoutrun. Everything else needsnpm run <name>. - Scripts can call tools from
node_modules(hereprettier) directly, withoutnpx. check-formatfailed with a non-zero exit code, which is exactly what an automated check needs. Exit codes came up in What Node.js is.
After npm run format, messy.js reads const x = { a: 1, b: 2 }; and console.log(x);. A common script while writing a server is "dev": "node --watch server.js", which restarts the program every time you save a file. You will add one in the practice section.
Versions and ranges
Package versions follow semantic versioning (semver): three numbers, MAJOR.MINOR.PATCH, such as 2.1.3.
- PATCH goes up for bug fixes:
2.1.3to2.1.4. - MINOR goes up for new features that don't break existing code:
2.1.3to2.2.0. - MAJOR goes up when something changes in a way that can break your code:
2.1.3to3.0.0.
In package.json, a dependency is a range: which versions you accept. npx runs a package's command without installing it into your project, so you can ask the official semver package which versions each range allows. The first time, npx asks before downloading:
npx semver --range "^2.1.0" 2.0.0 2.1.0 2.1.3 2.9.0 3.0.0 Need to install the following packages: semver@7.8.5 Ok to proceed? (y) y 2.1.0 2.1.3 2.9.0 npx semver --range "~2.1.0" 2.0.0 2.1.0 2.1.3 2.9.0 3.0.0 2.1.0 2.1.3 npx semver --range "2.1.0" 2.0.0 2.1.0 2.1.3 2.9.0 3.0.0 2.1.0
The command prints the versions from the list that fit the range:
^2.1.0(caret) accepts new minor and patch versions, never a new major. This is whatnpm installwrites by default.~2.1.0(tilde) accepts only new patch versions.2.1.0with no symbol is exact: that version and nothing else.
The caret rule is simple enough to write yourself, which is a good way to be sure you understand it. This version handles plain MAJOR.MINOR.PATCH numbers:
function parse(version) {
return version.split(".").map(Number);
}
function isNewerOrSame(a, b) {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) return a[i] > b[i];
}
return true;
}
function satisfiesCaret(version, range) {
const v = parse(version);
const base = parse(range.slice(1));
return v[0] === base[0] && isNewerOrSame(v, base);
}
for (const version of ["2.0.0", "2.1.0", "2.1.3", "2.9.0", "3.0.0"]) {
console.log(version, satisfiesCaret(version, "^2.1.0"));
}
node caret.js and of the browser terminal2.0.0 false 2.1.0 true 2.1.3 true 2.9.0 true 3.0.0 false
Same answers as the real semver package: the major number must match, and the version must be at least the one in the range. The real rules have more cases, such as versions starting with 0., where ^0.2.0 only accepts 0.2.x, because before 1.0.0 every minor version may break things. That is why you use the semver package rather than your own function in real code.
npx RUNS CODE FROM THE INTERNET
The prompt is there for a reason:npx some-name downloads a package and runs it on your computer, with your permissions. Read the name carefully before you type y. A spelling mistake can run someone else's package.The lockfile
A range like ^2.1.3 could mean a different version next month. package-lock.json records the exact version npm actually installed, where it came from, and a fingerprint of its contents:
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
}
The integrity value is a SHA-512 hash, the same idea as the SHA-256 fingerprints in Files, paths and your computer. If the downloaded file does not match it, npm refuses to install it. So the lockfile gives everyone on the team, and your server, exactly the same code. Commit it.
On a server or in automated builds, install with npm ci ("clean install") instead of npm install. It deletes node_modules, installs exactly what the lockfile says, and refuses to run if package.json and the lockfile disagree, rather than quietly changing versions:
npm ci added 2 packages, and audited 3 packages in 11s 1 package is looking for funding run `npm fund` for details found 0 vulnerabilities # after someone edits package.json by hand without running npm install: npm ci npm error code EUSAGE npm error npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing. npm error npm error Invalid: lock file's ms@2.1.3 does not satisfy ms@1.0.0 …
Updating and removing
Packages get bug fixes and security fixes, so you need to update them. To see how that works, install an older version on purpose, then ask npm what is out of date:
npm install ms@2.0.0 changed 1 package, and audited 3 packages in 3s 1 package is looking for funding run `npm fund` for details found 0 vulnerabilities npm outdated Package Current Wanted Latest Location Depended by ms 2.0.0 2.1.3 2.1.3 node_modules/ms task-tools npm update changed 1 package, and audited 3 packages in 3s 1 package is looking for funding run `npm fund` for details found 0 vulnerabilities npm ls task-tools@1.0.0 ~/task-tools +-- ms@2.1.3 `-- prettier@3.9.9
- Current is what is installed, Wanted is the newest version your range allows, and Latest is the newest version published.
npm updatemoves every package up to its Wanted version, and updates the lockfile. It never crosses a major version. For that, runnpm install ms@latestdeliberately, and read the package's changelog first: a major version can break your code.npm lsshows what is installed.
To remove a package, and its entry in package.json:
npm uninstall prettier removed 1 package, and audited 2 packages in 3s found 0 vulnerabilities
Local packages: workspaces
As a project grows, you may want to split out a piece of it as its own package, used by several parts of the project, without publishing it. npm workspaces do that: one root folder lists the folders that hold packages. The root package.json:
{
"name": "task-monorepo",
"private": true,
"type": "module",
"workspaces": ["packages/*"]
}
A package in packages/format/package.json, with its code in packages/format/index.js:
{
"name": "@tasks/format",
"version": "1.0.0",
"type": "module",
"exports": "./index.js"
}
export function formatTask(task) {
return `${task.done ? "[x]" : "[ ]"} #${task.id} ${task.title}`;
}
And main.js at the root imports it by its package name, as if it came from the registry:
import { formatTask } from "@tasks/format";
console.log(formatTask({ id: 1, title: "Buy milk", done: true }));
console.log(formatTask({ id: 2, title: "Write report", done: false }));
npm install added 1 package, and audited 3 packages in 3s found 0 vulnerabilities npm ls task-monorepo@ ~/task-monorepo `-- @tasks/format@1.0.0 -> ./packages/format node main.js [x] #1 Buy milk [ ] #2 Write report
npm install linked node_modules/@tasks/format to the folder (the arrow in npm ls), so a change in packages/format is seen at once. A name like @tasks/format is scoped: @tasks groups related packages, the same way every ZudoJS package is @zudojs/something. "private": true stops the root from ever being published by accident.
Publishing a package
To share a package with the world you would create an account on npmjs.com, turn on two-factor authentication, run npm login, then npm publish. Don't publish these demo packages. The registry is public and permanent, and names are first come, first served.
What you should always do first is look at exactly which files would be uploaded. npm pack --dry-run shows that without sending anything. Here it runs in the packages/format folder from above, after adding a README.md and a test file, index.test.js:
cd packages/format npm pack --dry-run npm notice npm notice package: @tasks/format@1.0.0 npm notice Tarball Contents npm notice 53B README.md npm notice 104B index.js npm notice 41B index.test.js npm notice 99B package.json npm notice Tarball Details npm notice name: @tasks/format npm notice version: 1.0.0 npm notice filename: tasks-format-1.0.0.tgz npm notice package size: 367 B npm notice unpacked size: 297 B npm notice shasum: 0c826c2d753d39791fad4921e16e97067249740c npm notice integrity: sha512-qDREbD78Tfa1W[...]6TURDl9LkVUxg== npm notice total files: 4 npm notice tasks-format-1.0.0.tgz
By default npm packs everything in the folder, including the test file nobody needs. Worse, it would also pack a .env file. Here is a different demo package with a .env in its folder and no .gitignore:
npm pack --dry-run … npm notice 25B .env npm notice 20B index.js npm notice 69B package.json …
Publishing that would hand your secrets to everyone. The safe habit is an allow-list: the files field in package.json names what to include, and everything else stays out:
npm pkg set description="Formats a task as one line of text" license=MIT "files[]=index.js" npm pack --dry-run npm notice npm notice package: @tasks/format@1.0.0 npm notice Tarball Contents npm notice 53B README.md npm notice 104B index.js npm notice 207B package.json … npm notice total files: 3
npm always adds package.json and the README. A real release would also bump the version first, with npm version patch (or minor or major, following the semver rules above).
Installing packages safely
Every package you install is code written by strangers that runs with your permissions, on your computer and on your server. Real attacks go through npm packages. These habits protect you.
Check for known vulnerabilities: npm audit
npm compares your installed versions with a public list of known security problems. Here is an old version of minimist, a popular argument parser, installed on purpose:
npm install minimist@1.2.5 added 1 package, and audited 3 packages in 7s 1 critical severity vulnerability To address all issues, run: npm audit fix Run `npm audit` for details. npm audit # npm audit report minimist 1.0.0 - 1.2.5 Severity: critical Prototype Pollution in minimist - https://github.com/advisories/GHSA-xvch-5gv4-984h fix available via `npm audit fix` node_modules/minimist 1 critical severity vulnerability To address all issues, run: npm audit fix npm audit fix changed 1 package, and audited 3 packages in 6s 1 package is looking for funding run `npm fund` for details found 0 vulnerabilities npm ls task-tools@1.0.0 ~/task-tools +-- minimist@1.2.8 `-- ms@2.1.3
The report names the package, the affected versions, how serious it is and a link to the details. npm audit fix moved minimist to 1.2.8, a fixed version inside your range. Sometimes npm suggests npm audit fix --force instead. --force is allowed to jump major versions and break your code, so only use it after reading what it will change.
Install scripts: npm 11 asks first
A package can declare an install script: a command that runs automatically when you install it. Some packages need one, for example to download a program built for your system. But an install script can do anything your user can do, which makes it the favourite trick of malicious packages: several real attacks, including a self-spreading worm in 2025, stole tokens and keys this way.
So npm 11 no longer runs install scripts of your dependencies unless you approve them. esbuild, a fast code bundler, has one:
npm install -D esbuild added 2 packages, and audited 5 packages in 12s 1 package is looking for funding run `npm fund` for details found 0 vulnerabilities npm warn install-scripts 1 package has install scripts not yet covered by allowScripts: npm warn install-scripts esbuild@0.28.2 (postinstall: node install.js) npm warn install-scripts npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow. npm install-scripts ls 1 package has install scripts not yet covered by allowScripts: esbuild@0.28.2 (postinstall: node install.js) Run `npm install-scripts approve <pkg>` to allow, or `npm install-scripts deny <pkg>` to deny.
Look at what the script does before approving it. esbuild's install.js only checks that the right esbuild program for your system was downloaded, and it is a widely used, well-known package. Approve it, then run its script once with npm rebuild:
npm install-scripts approve esbuild Approved esbuild: added esbuild@0.28.2 npm rebuild esbuild rebuilt dependencies successfully
The approval is saved in package.json, pinned to the version you reviewed. When esbuild releases a new version, npm asks you again:
"allowScripts": {
"esbuild@0.28.2": true
}
If you don't recognise a package asking to run a script, run npm install-scripts deny <pkg> instead, and find out why it is in your project (npm ls <pkg> shows which of your dependencies pulled it in).
Typosquatting
Attackers publish packages whose names are one small typo away from popular ones, and wait for someone to mistype. In 2017, a package called crossenv copied the popular cross-env and sent the environment variables of everyone who installed it, including their secrets, to its author. Before installing:
- Copy the exact name from the project's official documentation, not from memory.
- Run
npm view <name>and check the description, the source repository link, the maintainers and how long the package has existed. - Be suspicious of a brand-new package with a well-known-sounding name, or one that has an install script it does not obviously need.
Lockfiles and signatures
Your committed package-lock.json plus npm ci means your server installs the code you tested, byte for byte, instead of whatever was published this morning. npm can also check that each package really came from the registry, and which ones carry provenance: a signed statement saying which source code repository and which automated build produced the package:
npm audit signatures audited 4 packages in 76s 4 packages have verified registry signatures 2 packages have verified attestations (use --json --include-attestations to view attestation details)
A package with provenance can be traced back to its source. When you publish your own packages from GitHub Actions, add --provenance to npm publish so your users get the same guarantee.
Practice
TRY IT YOURSELF
Read a version range
Your package.json says "ms": "~2.0.1". Which of 2.0.0, 2.0.1, 2.0.9, 2.1.0 and 3.0.0 could npm install? Work it out, then check with npx semver.
Show a solution
npx semver --range "~2.0.1" 2.0.0 2.0.1 2.0.9 2.1.0 3.0.0 2.0.1 2.0.9
A tilde range allows newer patch versions only, starting at the version given.
TRY IT YOURSELF
A dev script with --watch
Add a script called dev that runs due.js with node --watch. Run it, change a line in due.js, save, and watch it restart. Press Ctrl + C to stop it.
Show a solution
npm pkg set scripts.dev="node --watch due.js" npm run dev > task-tools@1.0.0 dev > node --watch due.js 172800000 5400000 2m 2 minutes Completed running 'due.js'. Waiting for file changes before restarting...
--watch is built into Node.js, so no extra package is needed. You will use the same idea for a server, where it restarts after every save.
TRY IT YOURSELF
Which files would you publish?
In the packages/format folder, add a notes.txt file. Run npm pack --dry-run and check whether it would be published. Explain the result.
Show a solution
echo "private notes" > notes.txt npm pack --dry-run npm notice npm notice package: @tasks/format@1.0.0 npm notice Tarball Contents npm notice 53B README.md npm notice 104B index.js npm notice 207B package.json … npm notice total files: 3
notes.txt is not in the list, because the files field only allows index.js. With an allow-list, new files stay private until you add them on purpose.
Recap
- npm is the public registry and the command that installs from it. Check a package with
npm viewbefore you install it. dependenciesare needed at run time;devDependencies(-D) only while developing.- Versions are
MAJOR.MINOR.PATCH.^accepts new minor versions,~only patches, and no symbol means exact. The lockfile records the exact versions; commit it and usenpm cion servers. npm outdated,npm updateandnpm uninstallkeep packages current.npm runlists and runs scripts;npxruns a package's command.- Workspaces link local packages. Before publishing, check
npm pack --dry-runand use afilesallow-list. - Run
npm audit, review install scripts beforenpm install-scripts approve, type package names carefully, and checknpm audit signatures.
Next, you will use Node.js's own node:http module to build a web server, with no packages at all.
Test yourself
Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.