package.json VS package-lock.json
When you run:
npm install
npm uses both package.json and package-lock.json, but they have different roles.
package.json — What dependencies does the project need?
It declares the dependencies and acceptable version ranges.
For example:
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
The ^ means npm can potentially use a compatible newer version, such as 19.2.x, rather than being restricted to exactly 19.1.0.
package-lock.json — Which exact versions should be installed?
The lock file records the exact dependency versions and their dependency tree that npm previously resolved.
For example, conceptually:
react
→ 19.1.0
react-dom
→ 19.1.0
some-package
→ 3.2.1
This makes installations reproducible across different machines.
What happens with npm install?
If both files exist:
npm install
│
├── Read package.json
│ ↓
│ What dependencies are required?
│
└── Read package-lock.json
↓
Which resolved versions?
↓
Install dependencies
So the short answer is:
package.jsondefines what you need;package-lock.jsonrecords the exact dependency versions npm resolved.
Important distinction
If you run:
npm install axios
npm updates:
package.json
package-lock.json
If you run:
npm install
in an existing project with a valid lock file, npm generally installs according to the lock file while respecting the dependency requirements in package.json.
For CI/CD or a production deployment, you will often see:
npm ci
instead. npm ci is specifically designed for clean, reproducible installations and requires the lock file.
What is npm ci?
npm ci means Clean Install.
The easiest way to understand it is to compare it with npm install.
npm install
Used mainly during development.
npm install
It looks at:
package.json
+
package-lock.json
↓
Install dependencies
It can also update package-lock.json if the dependency information needs to be changed.
npm ci
Used mainly for CI/CD and clean installations.
npm ci
It uses the existing:
package-lock.json
to install the exact dependency versions recorded in the lock file.
package-lock.json
↓
Exact versions
↓
Clean installation
↓
node_modules/
Before installing, npm removes the existing node_modules folder.
Simple Example
Suppose your package.json says:
"dependencies": {
"react": "^19.0.0"
}
The ^ allows compatible versions.
Your package-lock.json might record:
react → 19.0.2
If you run:
npm install
npm resolves dependencies according to package.json and the lock file, and the lock file can be updated if needed.
If you run:
npm ci
npm says, essentially:
"Don't resolve new dependency versions. Give me exactly what is recorded in
package-lock.json."
So you get:
react → 19.0.2
Why is npm ci useful in CI/CD?
Imagine your team has 5 developers and a build server.
Without a lock file, dependency resolution could potentially produce different versions at different times.
With:
npm ci
the build server installs the exact dependency tree recorded in package-lock.json.
Developer 1 ─┐
Developer 2 ─┤
Developer 3 ─┼── package-lock.json
Developer 4 ─┤ ↓
Developer 5 ─┤ npm ci
│ ↓
└── Same dependency versions
This makes CI/CD builds much more reproducible.
Important Difference
npm install |
npm ci |
|---|---|
| Normal development | CI/CD / clean builds |
Uses package.json + lock file |
Requires lock file |
Can modify package-lock.json |
Does not update lock file |
Doesn't require deleting node_modules first |
Removes node_modules first |
| Can resolve dependency changes | Installs locked versions |
Remember this
npm install
↓
"I am developing the application."
↓
Install/update dependencies
npm ci
↓
"I am building/testing an existing project."
↓
Give me exactly what package-lock.json specifies.
For your React + Vite project, you'll normally use:
npm install
npm run dev
During a CI/CD pipeline, you would commonly use:
npm ci
npm run build
ci = clean install, using the lock file exactly.
When running npm install, what will happen if the lockfile has version 6.0.0 and the package.json file has 7.5.0?
The key is that npm does not blindly take the version from package.json or the lock file. It checks whether the locked version satisfies the version requirement in package.json.
Suppose:
package.json
"dependencies": {
"some-package": "7.5.0"
}
And package-lock.json contains:
some-package → 6.0.0
Now you run:
npm install
What happens?
The lock file says:
6.0.0
but package.json says:
7.5.0
6.0.0 does not satisfy the requirement 7.5.0.
Therefore npm will resolve/install 7.5.0 and update the package-lock.json.
package.json
│
│ requires 7.5.0
▼
package-lock.json
│
│ currently has 6.0.0 ❌
▼
npm install
│
│ resolve dependency
▼
7.5.0
│
▼
package-lock.json updated
Afterward, the lock file will contain the resolved 7.5.0 version.
Important distinction
If your package.json has:
"some-package": "^7.5.0"
then the situation is slightly different.
^7.5.0 means approximately:
7.x.x
So:
6.0.0 ❌
7.5.0 ✅
7.6.0 ✅
7.9.0 ✅
8.0.0 ❌
Therefore npm would need to update the lock file from the 6.x version to a compatible 7.x version.
And what about npm ci?
This is where the difference becomes important.
If:
package.json → 7.5.0
package-lock → 6.0.0
and you run:
npm ci
npm expects the lock file and package.json to be in sync. Because 6.0.0 doesn't satisfy 7.5.0, npm ci will fail rather than update the lock file.
So remember:
npm install
↓
Detect mismatch
↓
Resolve correct version
↓
Update package-lock.json
↓
Continue
Whereas:
npm ci
↓
Detect mismatch
↓
❌ Fail
Simple rule: npm install can repair/update the lock file; npm ci expects the lock file to already be correct.