Skip to main content
Dublin Library

The Publishing Project

Clean way to install NPM packages

 

One of my biggest frustrations in Node.js development is when your project works on your machine but breaks somewhere else. A common cause is package inconsistency between environments, even with a `package.json` file. Updating, removing, or installing packages can lead to a `node_modules` directory that doesn't perfectly match what's defined. The standard fix — deleting node_modules and `package-lock.json` before running `npm install` again — is cumbersome. This is the exact problem `npm ci` (for clean install) was designed to solve. This post will explain what npm ci is, how it differs from npm install, and how to use it effectively in your workflow. ## What is npm ci? The `npm ci` command provides a clean, reliable, and fast way to install dependencies. It's designed for automated environments like continuous integration (CI), testing platforms, and deployments, where predictable and repeatable builds are essential. Instead of resolving dependencies based on `package.json`, `npm ci` installs packages directly from the `package-lock.json` file. This guarantees that you get the exact same version of every package, every single time. To ensure a truly clean slate, it automatically deletes any existing `node_modules` directory before starting the installation. While it's perfect for automation, `npm ci` is also incredibly useful during development. A common workflow is: 1. Modify your `package.json` with new dependencies. 2. Run `npm install` to update your `package-lock.json`. 3. Commit both the `package.json` and `package-lock.json` files. 4. Run `npm ci` to ensure your local `node_modules` is a perfect mirror of your `package-lock.json`, just as it will be in production. ## npm ci vs. npm install The key differences between `npm ci` and `npm install` are: * **Source of Truth**: `npm ci` uses only `package-lock.json` or `npm-shrinkwrap.json` to install dependencies. `npm install` uses `package.json` and updates the lock file accordingly. * **Lock File Requirement**: `npm ci` requires a `package-lock.json` to exist. If it's missing, `npm ci` will exit with an error. `npm install` will create one if it's not there. * **Error Handling**: If your `package.json` and `package-lock.json` are out of sync, `npm ci` will exit with an error instead of trying to update the lock file. * **Immutability**: `npm ci` never writes to `package.json` or `package-lock.json`. Your project files are never modified. * **Clean Slate**: `npm ci` always deletes `node_modules` before installing to prevent any inconsistencies. * **Package Management**: You cannot use `npm ci` to add, update, or remove individual packages (e.g., `npm ci express`). It only installs the entire project at once. !!! note Note: If you use special flags like `--legacy-peer-deps` when running `npm install`, you must use the same flags with `npm ci`. An easy way to enforce this is to create a `.npmrc` file in your project with these settings (e.g., `legacy-peer-deps=true`) and commit it to your repository. !!! ## Using npm ci with Overrides The overrides feature in package.json lets you enforce a specific version of a nested dependency, which is great for patching security vulnerabilities. However, `npm ci` introduces an important consideration. Since `npm ci` relies exclusively on `package-lock.json` and never modifies it, the overrides must already be reflected in the lock file for `npm ci` to apply them. If the lock file is out of sync with the overrides in `package.json`, `npm ci` will fail. To use overrides correctly with `npm ci`, follow these steps: **Define Overrides in package.json**: Add or update the overrides field with the dependency versions you need. ```json { "name": "my-project", "version": "1.0.0", "dependencies": { "some-package": "^1.0.0" }, "overrides": { "a-nested-dependency": "1.2.3" } } ``` **Update the Lock File**: Run `npm install`. This command reads the overrides from `package.json` and updates `package-lock.json` to reflect the changes. **Commit the Lock File**: Commit the updated `package-lock.json` to your version control system. It now serves as the single source of truth. **Run npm ci**: Now, you and your CI/CD pipeline can run `npm ci`. It will read the updated lock file and install the exact overridden versions, ensuring a consistent and predictable installation. The extra `npm install` step is necessary because it's the only command that can translate the intent from `package.json` into the concrete dependency tree stored in `package-lock.json`.

Edit on Github