📦 Introduction to NPM
You just learned to organize your own code with ES modules. But you rarely write everything yourself — nobody hand-rolls a date library or an HTTP server for every project. npm is how the JavaScript world shares code: a registry of over three million ready-made packages, plus the command-line tool that installs them, tracks them, and keeps your teammates on exactly the same versions you are.
Week 3 · Day 2 (Tuesday: NPM and Package Management) · Lecture 1
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what npm is — registry, CLI, and website — and how they fit together
- Initialize a project with
npm initand read the resultingpackage.json - Decode a semantic version range (
^,~, exact) and predict which updates it allows - Install, update, and remove packages, choosing correctly between dependencies and devDependencies
- Describe the roles of
node_modulesandpackage-lock.jsonand why one is committed and the other is not - Run npm scripts and audit dependencies for known vulnerabilities
Estimated Time: 70 minutes
Practice: Spin up a real project, install a few packages, and wire up a dev script.
In This Lesson
What Is npm?
Imagine building a house. You wouldn't fire the clay for every brick or forge every nail — you'd buy pre-made materials from a supplier and spend your time on what makes your house unique. npm is that supplier for JavaScript developers: a massive, searchable warehouse of code other people have already written, tested, and documented, ready to drop into your project.
The name is a little overloaded, because "npm" actually refers to three connected things:
- The registry — the world's largest software registry, hosting millions of public (and optionally private) packages at registry.npmjs.org.
- The CLI — the
npmcommand that ships with Node.js. It installs packages, tracks them in your project, runs scripts, and checks for security issues. - The website — npmjs.com, where you search for packages, read their docs, and see download stats before you trust them.
where packages live] A --> C[CLI Tool
the npm command] A --> D[Website
npmjs.com] B --> E[Public packages] B --> F[Private packages] C --> G[Install packages] C --> H[Manage dependencies] C --> I[Run scripts] D --> J[Search & discover] D --> K[Docs & stats]
💡 npm vs. Node vs. the alternatives
npm is bundled with Node.js — install one and you get both. It is not the only package manager: Yarn and pnpm read the same package.json and pull from the same registry, just with different speed and disk-space trade-offs. Learn npm first; the concepts transfer directly.
Getting Started & npm init
Confirm your toolchain
Because npm rides along with Node, checking one is a good moment to check both:
# Is npm installed, and which version?
npm --version # e.g. 10.8.2
# Node.js version (npm ships inside Node)
node --version # e.g. v20.16.0
# Update npm itself to the latest release
npm install -g npm@latest
Create a project with npm init
Every Node project starts with a package.json — a small JSON file that records your project's name, its dependencies, and the scripts you can run. npm init generates it for you.
# Make a folder and move into it
mkdir my-awesome-project
cd my-awesome-project
# Interactive: npm asks you a few questions
npm init
# Fast lane: accept every default, no questions
npm init -y
The interactive walkthrough looks like this — press Enter to accept a default shown in parentheses, or type your own answer:
$ npm init
This utility will walk you through creating a package.json file.
package name: (my-awesome-project)
version: (1.0.0)
description: A demo project for learning npm
entry point: (index.js)
test command: jest
git repository: https://github.com/username/my-awesome-project
keywords: demo, npm, learning
author: Your Name <you@example.com>
license: (ISC) MIT
Is this OK? (yes)
The result is a starter package.json. Every field here is just data npm reads later:
{
"name": "my-awesome-project",
"version": "1.0.0",
"description": "A demo project for learning npm",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": ["demo", "npm", "learning"],
"author": "Your Name <you@example.com>",
"license": "MIT"
}
✅ Use npm init -y while learning
The interactive prompts matter most when you're about to publish. For everyday projects, npm init -y creates a sensible file in one second — you can edit the name, description, and scripts by hand afterward. It's just a text file.
Semantic Versioning
Before you install anything, you need to read version numbers the way npm does. npm packages follow SemVer (Semantic Versioning): every version is three numbers, MAJOR.MINOR.PATCH, and each position carries a promise about what changed.
Range syntax — the ^ and ~ prefixes
When npm records a dependency, it usually writes a small symbol in front of the version. That symbol is a range: it tells npm which future versions are acceptable to install.
// Exact version — install this and nothing else
"lodash": "4.17.21"
// Caret: allow minor + patch updates, lock the major
"lodash": "^4.17.21" // matches >=4.17.21 <5.0.0
// Tilde: allow patch updates only, lock major + minor
"lodash": "~4.17.21" // matches >=4.17.21 <4.18.0
// Wildcards and ranges
"lodash": "4.x" // any 4.x.x
"lodash": ">=4.0.0 <5.0.0"
// Multiple ranges with OR
"lodash": "^4.17.0 || ^5.0.0"
Here's how those ranges play out in a real dependency list. The comments explain why each choice was made:
{
"dependencies": {
"react": "^18.2.0", // trust minor updates from a stable, well-tested library
"react-dom": "^18.2.0", // keep it in lockstep with react
"axios": "^1.4.0", // new features welcome
"lodash": "~4.17.21" // only bug fixes — no surprise behavior
}
}
⚠️ Why 0.x versions are special
SemVer treats versions below 1.0.0 as unstable, so ^0.3.1 does not allow 0.4.0 — it only allows patch updates like 0.3.2. A caret on a 0.x package behaves like a tilde. If a library is still on 0.x, treat every minor bump as potentially breaking.
Installing Packages
Installing is the everyday heart of npm. The command is npm install (or just npm i), and a couple of flags decide where the package gets recorded.
# Add to "dependencies" (needed to run the app)
npm install express
npm i express # same thing, shorthand
# Add to "devDependencies" (needed only while developing)
npm install --save-dev jest
npm i -D jest # shorthand
# Install a tool globally (available in any project / your shell)
npm install -g typescript
# Pin an exact version
npm install react@18.2.0
# Install straight from Git or a local folder
npm install git+https://github.com/user/repo.git
npm install ../my-local-package
💡 dependencies vs. devDependencies
Ask: "Would the deployed app break without this?" Express serves your requests — it's a dependency. Jest and ESLint only run on your machine while you build — they're devDependencies. Getting this split right keeps production installs lean and fast.
What happens when you run npm install
A single install command sets off a short chain of events. Understanding it demystifies where your files come from:
Notice npm installed 57 packages when you only asked for one: Express itself has dependencies, and those have dependencies. npm resolves the whole tree for you.
node_modules & the Lockfile
The node_modules folder
Everything you install lands in a folder called node_modules. It is the library where your dependencies — and their dependencies — physically live. It grows large fast:
my-awesome-project/
├── node_modules/ ← installed packages live here (big!)
│ ├── express/
│ │ ├── lib/
│ │ ├── index.js
│ │ └── package.json
│ ├── lodash/
│ └── ... (dozens or hundreds more)
├── src/
│ └── index.js
├── package.json ← your declared dependencies
└── package-lock.json ← exact resolved versions
Two rules keep node_modules from becoming a headache:
- Never commit it to Git. It's huge, machine-specific, and fully reproducible. Add it to
.gitignore. - It's disposable. Delete it any time and run
npm installto rebuild it exactly frompackage.jsonand the lockfile.
# .gitignore — the essentials for a Node project
node_modules/
npm-debug.log*
# Environment secrets — never commit these
.env
.env.local
# Build output
dist/
build/
# Editor & OS cruft
.vscode/
.DS_Store
Thumbs.db
The lockfile: package-lock.json
Your package.json says "I want express ^4.18.2" — a range. But which exact version got installed? And which exact versions of Express's own dependencies? That's what package-lock.json records: a precise, machine-generated snapshot of the entire resolved tree, down to cryptographic integrity hashes.
{
"name": "my-awesome-project",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "my-awesome-project",
"dependencies": { "express": "^4.18.2" }
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-...",
"dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1" }
}
}
}
✅ Commit the lockfile, ignore node_modules
The lockfile is the opposite of node_modules: it's small, and you should commit it. It guarantees every teammate — and your CI server — installs byte-for-byte identical dependencies. Never hand-edit it; let npm regenerate it. In CI, prefer npm ci, which installs strictly from the lockfile and errors if it's out of sync.
npm Scripts
The scripts field turns long, easy-to-forget commands into short, memorable aliases. Instead of typing webpack --mode production, you type npm run build. Scripts are the project's control panel.
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest",
"test:watch": "jest --watch",
"build": "webpack --mode production",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write ."
}
}
# start and test are special — no "run" needed
npm start
npm test
# every custom script needs "run"
npm run build
npm run dev
# pass extra flags through with a -- separator
npm run test -- --coverage
# chain scripts together
npm run lint && npm test
Pre and post hooks
npm automatically runs a script named pre<name> before, and post<name> after, any script you invoke. This is perfect for guardrails — for example, always lint before testing:
{
"scripts": {
"pretest": "npm run lint", // runs automatically first
"test": "jest",
"posttest": "echo 'All tests complete!'"
}
}
Output of npm test
> pretest
> eslint . ✔ no lint errors
> test
> jest ✔ 12 passing
> posttest
All tests complete!
Updating & Auditing
Finding and applying updates
# See what's out of date
npm outdated
# Example output:
# Package Current Wanted Latest Location
# express 4.17.1 4.17.3 4.18.2 my-project
# jest 27.0.0 27.5.1 29.5.0 my-project
The columns matter: Wanted is the newest version your range allows (safe to take), while Latest may be a major bump with breaking changes.
# Update everything to the "Wanted" version (respects your ranges)
npm update
# Jump a package to the newest major — may break things
npm install express@latest
# Remove a package
npm uninstall express
npm rm express # shorthand
Security audits
npm cross-references your dependency tree against a database of known vulnerabilities. Run this routinely:
# Report known vulnerabilities
npm audit
# → found 7 vulnerabilities (2 low, 3 moderate, 2 high)
# Auto-fix what can be fixed without breaking changes
npm audit fix
# Also apply breaking updates (review carefully first!)
npm audit fix --force
# Audit only production dependencies
npm audit --omit=dev
⚠️ Think before --force
npm audit fix --force will happily install major version bumps to clear a warning — and those bumps can break your app. Treat it as a suggestion, not a command: read what it wants to change, then update deliberately and re-run your tests.
Practice & Quiz
🏋️ Exercise 1: Bootstrap a real project
Goal: Create a small CLI project from scratch and wire up its dependencies and scripts.
# In a fresh folder called "todo-cli":
# 1. Initialize npm
# 2. Install runtime deps: commander chalk
# 3. Install a dev dep: nodemon
# 4. Add scripts: "start" runs "node index.js"
# "dev" runs "nodemon index.js"
💡 Hint
Use npm init -y to skip the prompts, then npm i commander chalk for runtime deps and npm i -D nodemon for the dev dep. Edit the scripts block of package.json by hand.
✅ Solution
mkdir todo-cli && cd todo-cli
npm init -y
npm install commander chalk
npm install --save-dev nodemon
// then edit package.json:
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
}
Check package.json: commander and chalk sit under dependencies, nodemon under devDependencies, and a package-lock.json now exists.
🏋️ Exercise 2: Read the ranges
Goal: Given the ranges below, decide which target version each would install if it's the newest available.
"a": "^2.3.4" // newest published: 2.9.0 and 3.1.0 exist
"b": "~2.3.4" // newest published: 2.3.9 and 2.4.0 exist
"c": "2.3.4" // newest published: 2.3.9 exists
✅ Solution
a → 2.9.0 (caret allows minor/patch, locks major, so 3.x is excluded).
b → 2.3.9 (tilde allows patch only, so 2.4.0 is excluded).
c → 2.3.4 (exact — no updates).
🎯 Quick Quiz
Question 1: Which file should you commit to Git, and which should you ignore?
Question 2: The range "~4.17.21" allows npm to install which of these?
Question 3: Jest, a testing framework, belongs in which section of package.json?
Best Practices & Pitfalls
✅ Do
- Commit
package-lock.jsonso everyone installs identical versions - Use
npm ciin CI/CD for fast, lockfile-exact, reproducible installs - Split runtime vs. tooling correctly with
--save-dev - Run
npm auditregularly and keep dependencies reasonably current - Keep your dependency count lean — every package is code you now depend on
❌ Don't
- Commit
node_modules— add it to.gitignore - Hand-edit
package-lock.json— let npm regenerate it - Use
"*"or"latest"as a version range in production — it's unpredictable - Reach for
npm audit fix --forcereflexively — it can install breaking majors - Install a package to run one line of code you could write yourself
⚠️ Typosquatting is real
Attackers publish malicious packages with names one keystroke away from popular ones (crossenv vs. cross-env). Double-check spelling, look at weekly download counts on npmjs.com, and be suspicious of a "popular" package with 12 downloads.
Summary
🎉 Key Takeaways
- npm is a registry, a CLI, and a website — the JavaScript world's shared code supply
npm initcreatespackage.json, the manifest that records your dependencies and scripts- Versions follow SemVer:
MAJOR.MINOR.PATCH, with^allowing minor/patch and~allowing patch only - Commit the lockfile, ignore
node_modules— one pins versions, the other is disposable - npm scripts automate tasks;
npm auditguards against known vulnerabilities
📚 Additional Resources
- npm Docs — official documentation
- npm Docs — About semantic versioning
- semver.org — the SemVer specification
- nodejs.org — Introduction to the npm package manager
🚀 What's Next?
You can now install and manage packages. Next we open the manifest itself and study every field: the dependency types (including peer and optional), engine constraints, and advanced version resolution in Package.json and Dependency Management.
🎉 Warehouse unlocked!
You've joined an ecosystem of millions of packages. From here, you build on the shoulders of the entire JavaScript community.