Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Overview

npm (Node Package Manager) is a popular dependency manager in the JavaScript ecosystem. The two commands commonly used for installing and resolving project dependencies are npm install and npm ci. Although similar, there are some notable differences in their purpose and operation.

In this tutorial, we’ll explore the differences between these two commands and how well they fit into development and automated workflows.

2. The npm install Command

In a JavaScript or Node.js project, the npm install command installs the dependencies required to run the project. We define the project’s direct dependencies in the package.json file:

{
  "name": "some-project",
  "version": "1.0.0",
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "eslint": "^8.1.0"
  }
}

In this example, we use a package.json with caret (^) version ranges to specify the versions of the lodash and eslint packages while allowing npm to accept new minor versions of these dependencies automatically.

2.1. npm install and package-lock.json

When no lock file exists, npm install installs dependencies according to package.json and semantic versioning (SemVer). In addition, the command automatically generates a package-lock.json file, which locks a snapshot of installed versions.

In package-lock.json, we can see the resolved package versions installed within the specified ranges:

    "node_modules/lodash": {
      "version": "4.17.21",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
      "license": "MIT"
    },    
    "node_modules/eslint": {
      "version": "8.57.1",
      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
      "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
      "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
      "dev": true,
      "license": "MIT",
      "dependencies": {
      ...
      }

Notice that the resolved version is the latest version that satisfies the SemVer range. This means that if we run npm install later, newer compatible versions will be installed as they become available.

The lock file acts as a snapshot of dependencies. It serves as a reference that npm install uses when possible to ensure consistency, without enforcing strict version locking.

2.2. Updating the package-lock.json File

npm install installs the versions listed in package-lock.json as long as they satisfy the version ranges defined in package.json. If the locked version is no longer compatible with the updated range, npm install resolves a new version accordingly and updates the lock file.

Let’s update the eslint package version to 9.0.0 in package.json:

  "devDependencies": {
    "eslint": "^9.0.0"
  }

When running npm install, npm detects that the locked version (8.57.1) no longer satisfies the new version range (^9.0.0). Therefore, it resolves a compatible version from the registry, installs it, and updates package-lock.json accordingly.

    "node_modules/eslint": {
      "version": "9.24.0",
      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz",
      "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
       ...
      }

However, if we use ^8.56.0, npm install will install version 8.57.1 from the lock file, as it satisfies the constraint.

3. The npm ci Command

Although the install command uses the lock file when it satisfies requirements, it doesn’t guarantee reproducible builds in all cases. If there’s a mismatch between package.json and package-lock.json, the latter will be updated with versions compatible with package.json.

Starting with version 5.7.0, npm introduced the ci (clean install) command to create reproducible builds. npm ci performs a clean read-only install from package-lock.json or npm-shrinkwrap.json:

  • It deletes the node_modules directory if it exists and recreates it from scratch
  • Unlike npm install, this command generates an error when the package.json file and lock file are desynchronized

Let’s take the example of a manual edit to the package.json file. We downgrade lodash to version 4.16.0, while package-lock.json still references version 4.17.21:

  "dependencies": {
    "lodash": "^4.16.0"
  }

Running npm ci will create an error:

$ npm ci
npm ERR! code EUSAGE
npm ERR! `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm ERR! Invalid: lock file's [email protected] does not satisfy [email protected]

This error signals a drift between package.json and the lock file. If this is intentional, npm ci recommends updating the lock file via npm install.

Therefore, the npm ci command guarantees a clean, reproducible build that strictly conforms to the lock file.

Using npm ci is recommended in automated workflows, such as continuous integration and deployment pipelines. It’s particularly suitable for the first installation after a clone or pull in development environments.

4. Key Differences

The npm install and ci commands differ notably in terms of operation and usage.

Let’s take a closer look at the main differences between these two commands, as illustrated in the table below:

npm install npm ci
Usage Interactive development Continuous Integration (CI) and clean installations
package-lock.json required No Yes
Handling mismatches Updates package-lock.json if necessary Fails if package.json and package-lock.json don’t match
Partial installation Allows adding individual dependencies Only installs the entire project
Handling node_modules Keeps the existing folder Deletes and recreates node_modules
lock file modifications May modify package-lock.json Never modifies package-lock.json
Dependencies check Checks and updates as necessary Skip dependency check

These key differences highlight the core nuance between these two commands. We should use npm install in our development flows, where we’re iterating on our project. When we’re happy with our changes, we should use npm ci for build environments for consistent and repeatable builds for our production artifacts.

5. Conclusion

In this article, we’ve covered two essential npm commands for managing dependencies in a JavaScript project. We’ve looked at the behavior of both commands, highlighted what differs between them, and pointed out which things are similar.

Understanding the differences between npm install and npm ci ensures consistency and stability across projects, regardless of the environment.