Most developers know npm install, npm run, and npm publish. The rest of the CLI tends to be discovered only when something breaks — which is the worst possible time to learn a debugging tool.

These are the commands I’ve found myself reaching for repeatedly, in rough order of how often they actually come up.


npm explain

This one earns its place every time a dependency audit or lockfile conflict appears. npm explain traces why a package is in your node_modules — which package required it, and what required that.

npm explain hosted-git-info

Output shows the full dependency chain: the direct dependency that pulled it in, what version constraint triggered it, and how deep the chain goes. When you’re trying to understand why [email protected] is still lurking in a project that ostensibly moved to 4.x, this is the tool for it.

npm why is an alias that does the same thing — slightly shorter to type and easier to remember in the moment.


npm pack –dry-run

Before publishing a package, npm pack --dry-run shows exactly what would be included in the tarball — without creating the file or touching the registry.

npm pack --dry-run

It respects .npmignore and the files field in package.json. The output is a manifest: file name, size on disk, size packed. Reviewing this before npm publish has saved me from shipping test fixtures, local config files, and — on one memorable occasion — a .env that had drifted out of .gitignore.

If you want the actual tarball without publishing (useful for testing a package install in a consumer project), drop --dry-run:

npm pack

This produces a .tgz you can install directly with npm install ./your-package-1.0.0.tgz. Useful for verifying a package works as a dependency before it hits the registry.


npm ci

npm install is not the right command for CI pipelines. npm ci is.

npm ci

The differences are meaningful: npm ci installs exclusively from package-lock.json, never updates it, errors if the lockfile and package.json are out of sync, and removes node_modules before installing to guarantee a clean state. npm install will helpfully update the lockfile, resolve conflicts, and make decisions on your behalf — which is convenient locally and a liability in automation.

If your CI pipeline is running npm install, it’s probably not deterministic in the way you think it is.


npm query

Added in npm v8.16.0 and still largely unknown. npm query lets you run CSS selector-like queries against your dependency graph.

# All production dependencies
npm query ":root > .prod"

# All packages with a specific keyword
npm query "[keywords~=cli]"

# Packages that are both a dev dependency and have a specific license
npm query ".dev[license=MIT]"

The output is JSON — a list of matching package descriptors. Pipe it into jq and you have a reasonably powerful way to audit your dependency tree without pulling in a separate tool. I’ve used this to find packages with restrictive licenses before they make it to a release build.


npm outdated

Not unknown, but persistently underused. npm outdated shows the current version, the wanted version (the highest matching your range), and the latest version on the registry — as a table.

npm outdated

The distinction between wanted and latest matters. If you have "lodash": "^4.0.0", wanted shows the highest 4.x release, latest shows the absolute newest version regardless of range. Red rows are outside your range entirely. Yellow rows are within range but not at the latest patch.

Running this before a quarterly dependency update gives you a clear picture of what’s drifted. npm update handles the within-range updates; manual version bumps handle the rest.


npm version

The npm version command is worth knowing if you publish packages. It bumps version in package.json, creates a git commit, and tags it — all in one step.

npm version patch    # 1.0.0 → 1.0.1
npm version minor    # 1.0.0 → 1.1.0
npm version major    # 1.0.0 → 2.0.0

For pre-release cycles, the prerelease subcommand with a --preid flag gives you tagged versions that won’t be pulled in by loose version constraints:

npm version prerelease --preid beta
# 1.0.0 → 1.0.1-beta.0

Subsequent calls to the same command increment the prerelease counter: 1.0.1-beta.1, 1.0.1-beta.2, and so on. These versions require an exact match — ^1.0.0 won’t resolve to a beta, which is the point. Consumers who want to test the beta pin explicitly; everyone else stays on stable.


npm dedupe

Over time, node_modules accumulates duplicate packages at different versions — usually because multiple dependencies have overlapping sub-dependencies with incompatible version constraints. npm dedupe attempts to collapse duplicates by hoisting shared versions where semver allows.

npm dedupe

It won’t violate any package’s version constraints, so it can’t always eliminate all duplicates. But it can meaningfully reduce bundle size and the number of distinct module instances, which matters more than it appears — two copies of a library with module-level state will not share that state, which can produce subtle bugs.

Worth running after a significant dependency update.


npm prune

npm prune removes packages from node_modules that are not listed in package.json. These accumulate when packages are removed without running npm uninstall, or after manual edits to package.json.

npm prune

With --omit=dev, it strips dev dependencies — useful for slimming down a production build:

npm prune --omit=dev

npm view

npm view (alias: npm info, npm show) queries the registry for package metadata without installing anything.

npm view react version        # latest version
npm view react versions       # all published versions
npm view react dependencies   # declared dependencies at latest
npm view react dist.tarball   # URL of the latest tarball

The full metadata object:

npm view react --json

Useful for checking what’s available before deciding whether to upgrade, or for scripting version checks in build automation.


npm config

npm’s configuration is layered — project-level .npmrc, user-level, and global. npm config list shows the effective configuration:

npm config list      # user and project overrides
npm config list -l   # everything, including defaults

Values can be set at any layer:

npm config set save-exact=true   # pin exact versions by default
npm config set fund=false        # suppress funding messages

save-exact=true is the configuration option I enable on most projects. ^1.0.0 in package.json feels precise but isn’t — the range will resolve to different versions over time. Exact versions make package.json the authoritative source of truth and the lockfile a verification tool rather than a fallback.


For developing packages that are consumed by other local projects, npm link creates a symlink in the global node_modules pointing at your local package directory. Any project that then runs npm link <package-name> will resolve that package to your local copy rather than the registry version.

# In your local package directory
npm link

# In the consuming project
npm link my-local-package

Changes to the local package are immediately reflected in the consuming project — no packing and reinstalling between iterations. When you’re done, npm unlink reverses it.

The main caveat: symlinked packages can produce multiple instances of singleton dependencies (React being the common example), because the linked package and the consuming project may each resolve their own copy. Worth knowing before spending an hour on a mysterious “invalid hook call” error.


When the CLI Isn’t Enough

These commands cover the core of what npm’s CLI exposes. For deeper dependency graph analysis — particularly for identifying transitive vulnerabilities, license compliance across deep trees, or understanding why a specific version was selected — tools like Depcheck or npm-why fill the gap. The built-in npm audit handles known CVEs; the rest is still a more manual process.

The CLI is more capable than most developers give it credit for. Most of what you’d reach for a GUI tool to accomplish is already in there — it just requires knowing what to ask for.