NodeJSInstalling Packages

Installing Packages

npm install is the command you will run more than any other. On the surface it just downloads code, but underneath it resolves a dependency graph, deduplicates it into a flat tree, writes the lockfile, and runs lifecycle scripts. This page covers the everyday forms, the flags worth knowing, and what actually happens on disk.

The core forms

Command

What it does

npm install

Install everything in package.json (alias: npm i)

npm install <pkg>

Add a package to dependencies and install it

npm install <pkg> -D

Add to devDependencies (--save-dev)

npm install <pkg> -g

Install globally onto your PATH

npm install <pkg>@<ver>

Install a specific version or tag

npm uninstall <pkg>

Remove a package and update the lockfile

Bash
npm install express                 # latest, into dependencies
npm install jest --save-dev          # dev tooling
npm install typescript@5.4.0         # exact version
npm install lodash@latest            # the "latest" dist-tag
npm install express@^4.18.0          # a range — resolved & pinned in the lock
What happens on disk

A single npm install express triggers a whole pipeline. Understanding it demystifies why the first install is slow and later ones are fast:

The install pipeline

Text
1. Read package.json + package-lock.json
2. Build the dependency graph (express → its deps → their deps …)
3. Resolve every range to a concrete version (reuse lock if present)
4. Fetch tarballs (or reuse the global cache at ~/.npm/_cacache)
5. Verify each tarball against its integrity hash
6. Lay out a FLAT node_modules (hoist shared deps to the top)
7. Run lifecycle scripts (preinstall/install/postinstall)
8. Write the updated package.json + package-lock.json
The cache makes re-installs fast
npm keeps every tarball it has ever downloaded in a content-addressed cache (`~/.npm/_cacache`). A second install of the same version copies from disk instead of hitting the network — which is why deleting `node_modules` and reinstalling is quick the second time. Inspect or clear it with `npm cache verify` and `npm cache clean --force`.
The flat node_modules

Since npm 3, dependencies are hoisted: shared packages are flattened to the top of node_modules so they are not duplicated. Conflicting versions get nested. This is why node_modules is wide and shallow, not deeply nested:

Hoisting in action

Text
Your app needs:  A (needs lodash@4)   B (needs lodash@4)   C (needs lodash@3)

node_modules/
   ├─ A/
   ├─ B/
   ├─ lodash/          ← v4, hoisted, shared by A and B
   └─ C/
       └─ node_modules/
           └─ lodash/  ← v3, nested because it conflicts
Hoisting enables “phantom dependencies”
Because a hoisted package sits at the top of `node_modules`, your code can `require` it even if it is **not** in your `package.json` — it is only there as someone else's transitive dep. This works until that other package drops it, and your app breaks with no diff to explain why. Only import what you have explicitly installed. (pnpm prevents this by *not* hoisting — see [Yarn & pnpm](/nodejs/yarn-pnpm).)
Useful flags

Flag

Effect

--save-dev, -D

Record under devDependencies

--save-exact, -E

Pin the exact version (no ^ range) in package.json

--global, -g

Install to the global prefix, onto PATH

--omit=dev

Skip devDependencies (production install)

--no-save

Install without touching package.json

--force

Override caches and conflicting peer deps (use sparingly)

--legacy-peer-deps

Ignore peer-dependency conflicts (npm 7+ escape hatch)

Production installs

On a server or in a container, you want runtime deps only — fast, reproducible, no build tooling:

Bash
npm ci --omit=dev           # lockfile-exact, runtime deps only
# (older syntax: npm ci --production)
Peer-dependency errors block installs in npm 7+
Since npm 7, an unmet `peerDependencies` constraint *fails* the install instead of just warning. The right fix is to install a compatible version of the peer. `--legacy-peer-deps` (restore the old warn-and-continue behavior) and `--force` exist as escape hatches, but they paper over a real version mismatch — use them only when you understand why the conflict is safe to ignore.
Verifying what you got

Bash
npm ls                      # the installed top-level tree
npm ls <pkg>                # where a specific package resolved
npm outdated                # which packages have newer versions
npm audit                   # known vulnerabilities in the tree
Next
The single most important classification of what you install — runtime vs build-time: [dependencies vs devDependencies](/nodejs/dependencies-types).