Choosing between npm ci and npm install comes down to whether you want to make dependency changes or reproduce the dependencies already recorded for a project. The distinction matters when a lockfile is out of sync, and it can determine whether a CI build installs the same dependency tree as a developer’s machine.
Key Takeaways
- Use
npm installfor everyday development, especially when adding or updating dependencies. - Use
npm ciin CI to install from the committed lockfile without changing it. npm cifails ifpackage.jsonand the lockfile disagree;npm installcan resolve the mismatch and update the lockfile.- Commit
package-lock.jsonand use compatible npm configuration across local development and CI.
What each command does
Both commands install project dependencies, but they treat the lockfile differently.
| Behavior | npm install | npm ci |
|---|---|---|
| Main purpose | Install dependencies and support dependency changes | Clean, lockfile-based installation for automated or clean environments |
| Lockfile required | No | Yes: a compatible package-lock.json or npm-shrinkwrap.json must exist |
| If manifest and lockfile disagree | Resolves dependencies and may update the lockfile | Exits with an error |
Existing node_modules | Reuses or adjusts the directory as needed | Removes it before installing |
| Writes the lockfile | May write or update it | Does not write package manifests or lockfiles |
The lockfile records the resolved dependency versions and related information used for an installation. Committing it lets npm use those recorded resolutions instead of resolving the dependency ranges afresh each time.
When to use npm install
Use npm install during development when you need to install the project’s dependencies or change them. For example, to add a package:
npm install lodash
To add a development dependency:
npm install --save-dev eslint
These commands update the project’s dependency declarations and, when applicable, its lockfile. Review and commit both package.json and package-lock.json so the change can be reproduced by teammates and CI.
Running npm install in a project that already has a lockfile is also a normal way to install dependencies locally. If the lockfile is consistent with package.json, npm uses the locked resolutions. If it is missing or no longer satisfies the manifest, npm resolves dependencies and can update the lockfile.
When to use npm ci
Use npm ci when you want a clean install based on the project’s committed lockfile, particularly in a CI job:
npm ci
npm test
Unlike npm install, npm ci removes an existing node_modules directory and installs the project dependencies anew. It does not update package.json or the lockfile. If the lockfile is missing or does not match the manifest, the command fails rather than repairing the files.
This makes npm ci useful for checking the dependency state represented by a commit. It is not a guarantee that every environment behaves identically: npm version, operating system, architecture, configuration, and dependency install scripts can affect the result. Use a consistent Node.js/npm setup where possible, and don’t treat a lockfile as a substitute for testing across your supported environments.
What happens when package.json and package-lock.json disagree?
Suppose package.json allows a package version range that the lockfile does not record, or a dependency declaration was changed without regenerating the lockfile.
With npm install, npm resolves dependencies to satisfy the manifest and can update package-lock.json. Review the resulting diff: a change to one dependency can affect other resolved versions too.
With npm ci, npm stops and reports that the manifest and lockfile are not in sync. Fix the mismatch in a development environment, then commit the updated files:
npm install
# Review package.json and package-lock.json
npm test
git diff -- package.json package-lock.json
Do not make CI run npm install just to hide this error. A CI failure is a useful signal that the committed dependency declarations and lockfile need attention.
A practical local-to-CI workflow
For a dependency change, use npm install, test the change, and commit the manifest and lockfile together. For a clean local check or an automated build, run npm ci from a checkout that contains those committed files.
A basic CI job can run:
npm ci
npm test
npm run build
Make sure the job uses the expected Node.js and npm versions. If the lockfile was created using settings that affect dependency resolution—such as legacy-peer-deps or install-links—use the same settings for npm ci. One way to share project-level npm settings is to commit an appropriate .npmrc; avoid putting credentials or secrets in a committed file.
Common Mistakes
- Using
npm cito add a dependency. It installs the existing locked project; usenpm install <package>to change dependencies. - Treating
npm cias a lockfile repair command. It deliberately fails on a mismatch. Update the lockfile withnpm installand commit the result. - Leaving the lockfile out of version control. Without a committed lockfile, CI cannot reproduce the dependency resolutions recorded by the project.
- Using different dependency-resolution options locally and in CI. Configuration differences can make the lockfile unusable with the CI command. Keep relevant settings consistent.
- Assuming a successful install proves the build is portable. Install scripts and platform-specific dependencies can vary; run the tests and build in the environments you support.
Best Practices
- Commit
package-lock.jsonfor applications and other projects where repeatable installs matter. - Use
npm installto add or change dependencies, and review its lockfile diff before committing. - Use
npm ciin CI and other clean-install checks so lockfile inconsistencies are surfaced. - Keep Node.js, npm, and project npm configuration consistent between development and automation.
- When
npm cifails, fix and commit the dependency mismatch rather than bypassing the check.
Conclusion
Use npm install to work on dependency changes; use npm ci to install the committed dependency tree cleanly. The lockfile is central to that workflow: keep it in sync with package.json, commit both when dependencies change, and let CI report mismatches instead of silently rewriting them.
