Tutorials

npm install | how it works

Key Takeaways

  • npm install without package arguments installs your project's dependencies, using compatible locked versions when available.
  • npm install express adds a package and normally saves it to dependencies; -D saves to devDependencies.
  • package.json declares requirements. package-lock.json records the resolved dependency tree.
  • Use npm ci for clean, lockfile-driven installations and --omit=dev when the installation doesn't need development tools.
  • Global installs are mainly for command-line tools, not application imports. npm 12 also introduces security defaults that affect Git dependencies and dependency install scripts.

npm install installs packages and the dependencies they need. Without package arguments, it installs the dependency tree declared by your project. With package arguments, it installs the specified packages and normally records them in your project files.

If package managers are new to you, start with our beginner's guide to npm. Here, we'll focus on what the install command changes, how it chooses versions, and which options matter.

npm install without arguments

npm install
# Equivalent shorthand:
npm i

npm i is an alias for npm install. With ordinary development settings, a local install includes the project's dependencies and devDependencies, plus their required dependencies. Omission settings can change what gets installed.

Keep these three pieces separate:

  • package.json: declares your direct dependencies and acceptable versions or other package sources.
  • package-lock.json: records the resolved dependency tree, including dependencies of dependencies.
  • node_modules: contains the installed packages and links used by the project.

The manifest describes what your project accepts; the lockfile records a particular resolution; the installed directory is the result on disk.

How package.json and package-lock.json interact

When the lockfile's resolved versions satisfy the ranges in package.json, a no-argument install uses those locked versions. If the manifest and lockfile disagree, npm install can resolve acceptable versions and update the lockfile. Without a lockfile, npm resolves the requirements and normally creates one.

Commit package-lock.json for applications and review its changes alongside package.json. It helps reproduce dependency resolutions, but doesn't guarantee identical build output across different npm versions, operating systems, architectures, or lifecycle-script inputs.

Where npm installs packages, including workspaces

Running the command from the project root is the clearest convention, not an absolute requirement. Outside workspace-specific handling, npm searches upward for a directory containing package.json or node_modules to establish the local package root.

A workspace is a package managed within a larger, root npm project. Running npm install at that root installs dependencies and links configured workspace packages into the installation tree. Dependencies may be hoisted to the root, so don't assume every workspace has a separate node_modules beside its manifest.

# From a monorepo root that already declares packages/api as a workspace:
npm install express --workspace=packages/api

This targets the packages/api workspace's dependency declaration rather than adding Express to the root package.

How npm resolves the dependency tree

A direct dependency is one your project declares. A transitive dependency is one required by another dependency. npm must choose versions and locations that satisfy those relationships; one package name doesn't necessarily mean one installed version.

Suppose package A requires C version 1, while B requires C version 2. One possible installed layout looks like this. These names and versions are hypothetical:

project/node_modules/
+-- a/                     requires c@1.x
+-- b/                     requires c@2.x
|   `-- node_modules/
|       `-- c/             version 2.x
`-- c/                     version 1.x

Hoisting places packages higher in the directory tree so compatible consumers can share them. Deduplication reuses compatible copies where possible. Neither makes incompatible version requirements compatible; nested copies are sometimes necessary.

Why peer dependencies can cause conflicts

A peer dependency expresses compatibility with a host package—for example, a plugin's supported host-library versions. Modern npm installs required peers automatically; peers marked optional aren't automatically installed. Conflicting requirements can produce warnings or installation errors rather than another harmless nested copy.

Fix the incompatible package versions first. --legacy-peer-deps ignores peer-dependency requirements while constructing the tree, so treat it as a temporary workaround, not a compatibility fix.

To inspect an existing project that includes Express:

npm ls --all
npm explain express

npm ls --all lists the logical dependency tree. npm explain shows the dependency chain that caused a package to be installed. The logical tree isn't necessarily the same as the physical directory layout.

npm install with package arguments

Add one package or several packages

With Node.js and npm installed, you can create a small project and add Express:

mkdir npm-install-demo
cd npm-install-demo
npm init -y
npm install express

npm init -y creates a manifest without the interactive questionnaire. The Express install adds Express and its dependencies to the local installation and normally saves Express under dependencies. You don't need to add --save.

You can install multiple packages in one command:

npm install express mongodb

This installs Express and mongodb, the official MongoDB Node.js driver, in one operation. Under normal saving behavior, npm updates package.json and creates or updates package-lock.json.

Versions, ranges, and @latest

For a new registry dependency, a bare package name normally uses the configured tag, which defaults to latest. Current npm also considers the package's declared Node.js compatibility when selecting a version without an explicit version or tag.

npm install express@latest

@latest explicitly requests the version referenced by the publisher's latest tag. It's a movable label, not necessarily the highest version number, and it doesn't establish runtime compatibility.

You can also use name@version for a published exact version or name@range for a version range. For a newly selected registry version, npm normally saves a caret (^) range; configuration can change that prefix.

# Save an exact direct-dependency version instead of a range:
npm install --save-exact express

--save-exact changes the saved direct-dependency specification; it doesn't replace the lockfile's role in recording the whole tree. An ordinary no-argument install isn't an instruction to upgrade everything: see how npm update works for the update workflow.

Install a package from Git

Git dependencies are useful for testing an unpublished change or using a fork. Git must be installed. In npm 12, Git fetching is disabled by default; explicitly allow it when your project needs it.

# npm 12: allow direct Git dependencies for this command.
npm install --allow-git=root "github:npm/node-semver#main"

This selects the main branch of npm's semver package repository. Replace #main with a real tag or full commit hash to select that reference; omitting the reference selects the repository's default branch.

A branch can move. A commit hash makes the intended revision explicit, while the lockfile records the resolved Git commit even when the manifest names a branch. For subsequent npm 12 installs, retain the reviewed allow-git=root setting in the project's .npmrc or supply it again.

For private repositories, configure Git authentication separately, using SSH credentials or an HTTPS credential helper. Follow your provider's instructions, such as GitHub's command-line authentication guidance; don't embed credentials in dependency URLs. Git dependencies can also require preparation scripts and build tools, subject to your npm script policy.

npm install options for saving packages

  • Use -D or --save-dev to save a package under devDependencies.
  • Use -O or --save-optional to save a package under optionalDependencies. Your application must tolerate its absence; an optional dependency's installation failure doesn't necessarily fail the overall install.
  • Use --no-save to install without writing package.json or package-lock.json.

These options control saving behavior, not whether a package's code is suitable for your application.

# Add a development tool:
npm install -D eslint

# Try a package locally without recording it:
npm install --no-save cowsay

The temporary cowsay installation isn't a declared project dependency and won't be reproduced by a clean installation from the committed project files.

dependencies vs devDependencies

Use dependencies for packages your application needs at runtime. Use devDependencies for tools needed only to develop, test, lint, or build it. ESLint, Webpack, and Babel are typical development-tool examples.

Development dependencies may still be essential in a production build environment. If deployment compiles or bundles your application, install its build tools at that stage; the final runtime environment may not need them. Our guide to JavaScript build tools explains that distinction.

Omit development dependencies

npm install --omit=dev

# For a clean installation from an existing, matching lockfile:
npm ci --omit=dev

--omit=dev leaves development-only dependencies out of the installed tree on disk; it doesn't remove their resolutions from the lockfile. It isn't the same as saying that only the dependencies field matters: optional dependencies and peers can still be involved.

When NODE_ENV=production, npm's default omission includes development dependencies. Explicit --include=dev overrides dev omission, including an explicit --omit=dev. When the resulting omit list includes dev, npm sets NODE_ENV=production for lifecycle scripts.

npm install global packages

Global installation is mainly useful for command-line tools. For example, install the cowsay CLI and check that your terminal can execute it:

npm install --global cowsay
cowsay "The global command is available"

-g is shorthand for --global. Global installation uses npm's configured prefix, which may be user-scoped rather than system-wide. The executable directory must be on PATH: typically {prefix}/bin on Unix and {prefix} on Windows.

Global installation doesn't automatically make a package available to application imports. Declare application dependencies locally, and prefer local build-tool installations when teammates and CI need the same versions. Without a package argument, global mode installs the current package context instead of a named registry package.

npm install vs npm ci

Use npm install while changing dependencies. Prefer npm ci when automation should install the committed dependency resolution without rewriting it.

CommandPrimary purposeProject-file behavior
npm installInstall or reconcile the project treeCan create or update the lockfile
npm install expressAdd or change a named dependencyNormally saves the dependency and updates the lockfile
npm ciClean installation from the lockfileRequires a matching lockfile; doesn't write the manifest or lockfile

npm ci requires package-lock.json, removes an existing node_modules before installing, and fails if dependency declarations conflict with the lockfile. It installs a project rather than adding individual packages.

Keep tree-shaping configuration consistent between lockfile creation and CI. For example, a tree generated with --legacy-peer-deps may require that setting during npm ci too. Standardize the runtime and npm version as well; see choosing a Node.js version.

Install scripts and npm 12 security defaults

Installation can involve lifecycle scripts—commands used for tasks such as compiling native components or preparing a Git package. npm 12 blocks dependency install scripts by default unless the project's root allowScripts policy approves them. This is a meaningful change from older npm behavior.

# npm 12: list dependency scripts that still need review.
npm install-scripts ls

After reviewing a dependency, use npm install-scripts approve <package> to record its approval, then npm rebuild to run newly approved scripts. Commit reviewed policy changes instead of blanket-approving everything. The install-script approval documentation describes the workflow.

FAQ

What does npm install do?

npm install installs packages and their dependencies. Without package arguments, it installs the project's declared tree using compatible locked resolutions when available; with package arguments, it installs the specified packages and normally saves them.

What is the difference between npm install and npm ci?

npm install can add dependencies and update the lockfile. npm ci requires a matching package-lock.json, removes existing node_modules, and installs without writing package.json or the lockfile.

Why does npm install change package-lock.json?

npm install can update the lockfile when dependency declarations change, packages are added, or lockfile metadata needs updating. A lockfile change doesn't always mean package versions changed; review the diff before committing it.

How do I install dependencies without devDependencies?

Use npm install --omit=dev, or npm ci --omit=dev for a clean installation from a matching lockfile. Development dependency resolutions remain recorded in the lockfile even when those packages aren't installed on disk.

How do I install a package without saving it?

Use npm install --no-save followed by the package name. It installs the package without writing package.json or package-lock.json, so the temporary addition won't be reproduced by a clean install from those files.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me