If you are here, it probably means you are not 100% sure whether you should be using the --save flag when running npm install --save. Since this post was written in 2024, the short answer is no, you don’t need to. Let’s see why.
When using, npm install --save (shorthand npm install -S), what you are basically asking npm is to record the packages you are installing as dependencies to your project’s package.json file.
Starting with npm version 5, you no longer need to use the
--saveflag
Starting with npm version 5, you no longer need to use the --save flag when installing packages. By default, npm automatically saves installed packages as dependencies in your package.json file. This means that whether you include the --save flag or not, the outcome is the same: the dependencies will be recorded in the package.json file.
What about developer dependencies?
For packages that you should handle as developer dependencies – devDependencies -, you should still use --save-dev or -D.
Interesting npm install --save flags#
By the way, while writing this post, I got curious about other npm flags since I have mostly been using the two mentioned earlier. As you’d expect, there are more. Here are a few that I found particularly interesting if you want to explore further:
--save-optional
The --save-optional flag saves packages as optionalDependencies. As the name suggests, these are packages that our application may not require. During the installation process, npm will not fail if, for any reason, an optional dependency fails to install.
--save-prod
The --save-prod flag is somewhat confusing at first glance, as it behaves similarly to the --save flag. It explicitly saves any installed package to the dependencies section of the package.json file. This ensures that your application’s production dependencies include the package.
--save-peer
The --save-peer made an impression! This one saves the dependencies into a peerDependencies section. Peer dependencies specify that your package is compatible with a particular version of another package without directly including it as a dependency.
So why did --save exist in the first place? 🕰️#
This is the bit that makes the whole thing click.
In npm 4 and earlier, npm install express did exactly what it said — it downloaded Express into node_modules and that was all. Your package.json was left untouched. If you forgot the flag, everything worked perfectly on your machine and then fell over the moment a teammate cloned the repo, because nothing had ever been written down.
So --save wasn’t a nicety, it was the thing that stopped you shipping a broken project. Everybody typed it. Every tutorial included it. Then npm 5 flipped the default, and a decade of blog posts, Stack Overflow answers and half-remembered muscle memory kept it alive long after it stopped doing anything.
Typing it today is harmless. It’s just noise.
Every save flag, in one table#
Here’s the whole family, with the short aliases — these are the ones npm documents:
| Flag | Alias | Where the package lands |
|---|---|---|
--save-prod |
-P |
dependencies — the default, unless -D or -O is present |
--save-dev |
-D |
devDependencies |
--save-optional |
-O |
optionalDependencies |
--save-peer |
— | peerDependencies |
--save-bundle |
-B |
also added to bundleDependencies |
--save-exact |
-E |
pins the exact version instead of a range |
--no-save |
— | installs it, writes nothing to package.json |
--save |
-S |
dependencies — the default since npm 5 |
You can check any of these yourself without leaving the terminal:
npm install -h--no-save: the one that’s actually still useful#
--save does nothing, but its opposite does something real:
npm install some-package --no-saveThat installs the package into node_modules without touching package.json. I use it maybe twice a year, and both times for the same reason — I want to try a library for ten minutes before deciding whether it earns a place in the project. When it doesn’t, rm -rf node_modules && npm install puts everything back exactly as it was.
dependencies vs devDependencies, and why deploys break#
This is the distinction worth actually caring about, because it’s the one that produces a confusing failure.
dependencies— needed for the app to run. Express, React, your database driver.devDependencies— needed to build or test it. Vitest, ESLint, TypeScript, and nodemon if you still need it.
Nothing breaks locally either way, because npm install installs both. It breaks on your host, where the build step commonly runs:
npm ci --omit=dev--omit=dev skips devDependencies entirely. So if TypeScript lives in devDependencies and your production build needs to run tsc, the deploy dies with a “command not found” that made no sense on your laptop. It’s one of the usual suspects when a Next.js deploy fails on Render after building perfectly at home. (If you have seen npm install --production in an older guide, that is the deprecated spelling of the same thing.)
While we’re here — npm ci rather than npm install is the right command in CI. It installs strictly from package-lock.json, deletes node_modules first, and refuses to run at all if the lockfile and package.json disagree. That last part is a feature: it turns a silent version drift into a loud failure.
--save-exact and that ^ in front of your versions#
Open your package.json and you’ll see something like "express": "^5.1.0". That caret is a range, not a version. It means “5.1.0 or any later 5.x release”, so a fresh npm install next month can quietly give you 5.4.0.
^5.1.0— allows minor and patch updates (5.x.x)~5.1.0— allows patch updates only (5.1.x)5.1.0— exactly that version, which is what--save-exactwrites
npm install express --save-exactYou mostly don’t need this, because package-lock.json already pins the precise version for everyone who installs from your repo. --save-exact matters when you’re publishing a package of your own, where consumers get your ranges and never see your lockfile.
peerDependencies and the ERESOLVE error 😖#
--save-peer is the flag you’ll never type unless you’re publishing a library — but peerDependencies is why you’ll one day see this:
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency treeIt means a package you’re installing declared a peer dependency — “I work with React 18” — and your project has something else installed. npm is refusing to build a tree it knows is broken rather than letting you find out at runtime.
The right fix is to update the packages until the versions genuinely agree. The escape hatch, when you’re blocked by a library that just hasn’t updated its peer range yet, is:
npm install --legacy-peer-depsThat tells npm to ignore peer dependency conflicts the way npm 6 did. It is a “get me through today” flag, not a solution — you have overruled a warning that was probably right.
Frequently asked questions#
Is npm install --save deprecated?#
Not deprecated, just redundant. npm still accepts --save and -S and nothing warns you about them; they simply describe the behaviour you were getting anyway. Old scripts using them will keep working.
Is npm i the same as npm install?#
Yes, i is a built-in alias. npm i -D vitest and npm install --save-dev vitest do exactly the same thing.
What happens if I forget --save-dev?#
The package installs and works, it just lands in dependencies instead. Fix it by hand — move the line into the devDependencies block in package.json — or reinstall it with -D, which rewrites the entry for you.
Should I commit package-lock.json?#
Yes. It records the exact version of every package in the tree, including your dependencies’ dependencies, which is the only thing that makes your install reproducible. It is also what npm ci reads.
Do I ever need --save with npm update?#
That is the one place the default flips: npm documents save as defaulting to true except for npm update, where it defaults to false. So if you want npm update to write the new ranges back into package.json, --save there is doing real work.
