Skip to main content

Managing Node Packages: An npm Commands Cheat Sheet for Real Projects

A practical npm cheat sheet covering install, save-dev, global, scripts, semver ranges (^ vs ~), npm ci vs install, and auditing without the filler.

Published By Li Lei
#npm #node #package-management #cheatsheet #devtools

Managing Node Packages: An npm Commands Cheat Sheet for Real Projects

npm is the part of Node you touch every single day and learn the least about. You memorize three commands, copy the rest from Stack Overflow, and never look back until a deploy breaks at 11pm. This is the cheat sheet I wish someone had handed me on day one: the commands that matter, the flags that change behavior in ways you can feel, and the two or three traps that cost real hours.

If you want the interactive version with a search box and the yarn/pnpm equivalents on every row, it lives over at the npm cheat sheet. This post is the narrative tour.

Installing: save, save-dev, and global

The default install is the boring one, and that is fine:

npm install lodash        # adds to "dependencies"
npm install lodash --save-dev   # adds to "devDependencies"
npm install -D vitest     # -D is the short form of --save-dev
npm install -g pnpm       # installs globally, on your PATH, not in this project

The split between dependencies and devDependencies is not cosmetic. Anything your shipped code imports at runtime belongs in dependencies. Anything that only runs on your machine or in CI — test runners, bundlers, type checkers, linters — belongs in devDependencies with --save-dev. The payoff arrives at build time: npm install --omit=dev (or --production) skips the dev tree entirely, which shrinks a Docker image and cuts install time. Put vitest in the wrong bucket and your production image carries a test framework it will never run.

Global installs (-g) are the ones to be stingy with. A global CLI is invisible to your project's lock file, so it is not reproducible across machines. When you hit EACCES errors installing globally, the fix is almost never sudo — it is pointing npm at a prefix you own, or just running the tool with npx so nothing gets installed at all.

Semantic versioning ranges: ^ vs ~

Open any package.json and you will see version specifiers with little symbols in front. They are not decoration; they decide which upgrades npm is allowed to pull silently.

{
  "dependencies": {
    "react": "^18.2.0",
    "left-pad": "~1.3.0",
    "internal-tool": "4.1.2"
  }
}
  • ^1.2.3 (caret) allows anything up to, but not including, the next major: >=1.2.3 <2.0.0. So 1.2.4, 1.9.0, and 1.99.0 all qualify, but 2.0.0 does not. This is npm's default when you npm install a package.
  • ~1.2.3 (tilde) allows only patch bumps within the same minor: >=1.2.3 <1.3.0. You get 1.2.4 and 1.2.9 but never 1.3.0.
  • 1.2.3 with no symbol is an exact pin. Nothing moves. Use npm install pkg --save-exact to write versions this way by default.

The mental model: caret trusts the package to follow semver and only break on majors. Tilde says "I only trust patches." For a library you control and a teammate maintains, caret is reasonable. For something that has burned you before, pin it exactly and upgrade on purpose.

Scripts: the part of package.json that earns its keep

Most of your day-to-day npm usage is running scripts, not installing things.

npm run build       # runs the "build" script in package.json
npm test            # "test" is special — no "run" needed
npm start           # so is "start"
npm run lint -- --fix   # the -- passes --fix to the underlying tool

That -- separator is the single most common script mistake. npm run build --prod does not send --prod to your build tool — npm eats the flag itself and your script runs with nothing. You have to write npm run build -- --prod. Once you have been bitten by this, you never forget it, but everyone gets bitten once.

npm also runs lifecycle hooks automatically: a prebuild script fires before build, a postinstall after install. Handy for codegen, occasionally a security footgun when an untrusted dependency ships a postinstall. That is exactly when --ignore-scripts earns its place.

npm ci vs npm install: the CI lesson I learned the hard way

Here is the distinction that quietly determines whether your pipeline is reproducible.

npm install does dependency resolution every time. It reads package.json, walks the tree, decides which versions satisfy your ranges, and then writes or mutates package-lock.json to match. If your package.json drifted ahead of the lock, it silently rewrites the lock to agree.

npm ci (clean install) skips resolution entirely. It deletes node_modules, then installs strictly from the lock file, and it errors out if package.json and the lock disagree.

npm ci   # wipe node_modules, install exactly what the lock says, fail on drift

A worked scenario: I once spent an afternoon chasing a bug that only appeared on the deploy server and never locally. The cause was embarrassingly simple — someone had run npm install in CI, the lock was slightly stale, and resolution had pulled a newer patch of a transitive dependency than what I had tested against. Locally I had the old version cached; the fresh CI box resolved the new one. Switching the pipeline to npm ci fixed it permanently. ci is faster (no resolution work), deterministic (the lock is law), and it hard-fails the day someone forgets to commit the lock file — which is precisely the failure you want to catch in CI rather than in production. The yarn equivalent is --frozen-lockfile; pnpm uses the same flag. If your CI step still says npm install, that is the easiest reliability upgrade you will make all quarter.

Auditing, updating, and keeping the tree healthy

Dependencies rot. New CVEs land, packages go stale, and the gap between "what I installed" and "what is current" widens silently.

npm outdated          # table of installed vs wanted vs latest
npm update            # bumps within your declared ranges (respects ^ and ~)
npm audit             # report known vulnerabilities in the tree
npm audit fix         # patch what it can without breaking ranges
npm audit fix --force # apply breaking upgrades too — read the diff first

Two cautions worth internalizing. First, npm update only moves within your existing range — a ~1.3.0 dep will never cross to 1.4.0 no matter how many times you run it. To cross minor or major boundaries you change the range in package.json or reach for a tool like npx npm-check-updates. Second, npm audit fix --force is allowed to apply major-version bumps that change your code's behavior, so never run it on a Friday and never without reading what it touched. Pair audits with a quick npm ls <pkg> or npm explain <pkg> to see why a vulnerable transitive dependency is even in your tree before you try to remove it.

For a broader walk through the developer tooling I lean on around npm, the git cheat sheet covers the version-control side of the same daily loop — commit the lock file, branch before the upgrade, bisect when an update breaks something.

A short closing checklist

  • Use --save-dev for anything that does not ship; reserve dependencies for runtime imports.
  • Default to caret (^) ranges, pin exact (--save-exact) for the deps that have burned you.
  • Always write -- before passing flags to a script: npm run x -- --flag.
  • Use npm ci in CI, npm install on your laptop.
  • Run npm outdated and npm audit on a schedule, not in a panic.

None of this is hard once the model clicks. The trouble is that npm lets you be wrong silently — a misplaced flag, a stale lock, a dev dependency in production — and the bill arrives later. Keep the cheat sheet open until the muscle memory takes over.


Made by Toolora · Updated 2026-06-13