What is so special about package-lock.json?

iamprovidence
7 min readDec 17, 2023

--

In the realm of JavaScript development, npm has gained tremendous popularity by giving developers access to a vast ecosystem of open-source packages and libraries that enable rapid development and increased productivity.

However, managing dependencies can sometimes become challenging, especially when working on collaborative projects or deploying applications across different environments. This is where the package.json and package-lock.json files come into play. The fun stuff is, not many developers understand the need for the second one.

Today we will see how packages are versioned. What the hack is package-lock.json. How it is different from package.json. And most importantly, you finally would understand its necessity in your project.

So, buckle up and get ready for a new journey into the world of packages📦

NPM

So what is package-lock.json? 🤔

Do not rush it, buddy 😊. Before diving into package-lock.json let’s make sure we are in a loop and know enough about npm.

In a frontend world, npm is the most popular package manager. Basically, it allows developers to reuse other people’s code.

Imagine there is a js-library that can detect whether a number is even or not. That library could be called "is-even". To use it, you need to create a file called package.json and list all required dependencies there:

{
"dependencies": {
"is-even": "1.0.0"
}
}

We did it manually but, you can also use npm CLI for it:

  • npm init — creates a package.json file
  • npm install <package-name> or npm i <package-name> — install a package

Usually, you would join a project where package.json is already defined and to restore all dependencies you would just go with npm i. It will do the following:

  • skate through the list of dependencies you have
  • find them in npm registry
  • download them to the node_modules folder
  • list all downloaded dependencies in package-lock.json

Semantic Versioning

Got you!😏 So what is package-lock.json?🤔

Too soon, buddy, too soon 😁. First, you need to understand how the package’s version works.

Imagine the developer of the "is-even" package, decided to enhance that library. He could just remove the old code and publish a new one, meaning breaking other people’s projects 😬.

Surely, that just would not be great. Similarly to git, npm tracks a version of each package release, and it is up to us, lazy developers, to decide whenever we want to migrate to a newer version or stay with the current one.

Back to our package.json:

{
"dependencies": {
"is-even": "1.0.0"
}
}

Here 1.0.0 represents a version of that package. However, those are not just random numbers a developer pulled from his ass. It should follow Semantic Versioning:

  • The first number is a MAJOR version. It changes only when a newer version of the package has incompatible API changes
  • The second number is a MINOR version. It changes only when a newer version of the package has new functionality that is backward compatible
  • The third number is a PATCH version. It changes only when a newer version does not bring any new functionality, but only backward compatible bug fixes

Version ranges

Oh, I see 😌 So what is package-lock.json? 🤔

False start again, buddy, false start 😤. Let’s first see how npm can simplify our work with managing packages’ version.

So we have defined our package.json:

{
"dependencies": {
"is-even": "1.0.0"
}
}

The question here is follow: how do we know a new version on stock and can be used?

Checking it every day in the hope of success? That would be a nightmare. This is why you are not obligated to specify a concrete version and be more agile with it. Check how versions can be ranged:

  • version — match version exactly
  • 1.2.x — matches 1.2.0, 1.2.1, etc., but not 1.3.0
  • >version, >=version, <version, <=version, =version — must be greater (greater equal etc) than version
  • ~version — matches current and all future patch versions
  • ^version — matches current and all future compatible version, meaning patch and minor
  • *, "" (just an empty string) — matches any version
  • version1 - version2 — same as >=version1 <=version2
  • range1 || range2 — passes if either range1 or range2 are satisfied

With all those configurations, your package.json can look insane:

{
"dependencies": {
"foo": "1.0.0 - 2.9999.9999",
"bar": ">=1.0.2 <2.1.2",
"baz": ">1.0.2 <=2.3.4",
"boo": "2.0.1",
"qux": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0",
"til": "~1.2",
"elf": "~1.2.3",
"two": "2.x",
"thr": "3.3.x",
"lat": "latest"
}
}

It is not only that. Instead of a version, you can also specify a url or path to a folder. But those are something for another story 😁.

package-lock.json

Can we finally talk about package-lock.json? 😫

Oh, wait a minute… 😯 Actually it is about time 😊.

So, package-lock.json you say, huh 😏. Alright, let’s break it through.

The first thing you need to know about this file, it is auto-generated. Developers have a spoiled opinion about auto-generated files up to the point, that they would not commit it to source control. This one is exceptional, you should commit it.

The second thing is about how package-lock.json is generated. Imagine you have a next file:

{
"dependencies": {
"is-even": ">=1.0.0"
}
}

Then you run your npm i command. What will happen, is that npm will try to search for that package version 1.0.0 or higher if there is available. When it finds it, it will install it and list all installed packages in package-lock.json.

So:

  • package.json — lists packages you would like to install
  • package-lock.json — lists actually installed packages

Why would you need two files to manage dependencies? 🤔

package-lock.json helps ensure that all developers working on the project are using the same versions of dependencies, reducing the likelihood of bugs or inconsistencies between environments.

Imagine a scenario. You share a code, you finished a few months ago, with a friend. He tried to restore packages but got a different version and the code no longer compiles.

Scenario number two. Imagine, your regular working day. All of a sudden you see that your PR has not passed the CI on the building fronend step. That happens because version 2.0.0 of package appears. CI is trying to install a newer version which has breaking changes causing your PR to stop compiling.

I can continue, but I think you can imagine yourself multiple cases where different versions of the same package are used in different environments. That is exactly where package-lock.json is useful.

Wait a second. In all those cases I had package-lock.json. Why it did not help?🤔

Because you were using npm install 😏. It will install newer versions of packages when available and break your code.

A big downside of npm install is that it is not deterministic and its unexpected behavior may mutate the package-lock.json.

Wait? Are you telling me it is bad? I have always been using it! 🤔

I know, right? Everybody has been using npm i from the ancient ages. It seems harmless and the only way to install packages, doesn’t it?

However, in practice, it should be avoided.

So far you have been only criticizing! Is there anything better to use? 🤔

Turned out, there is a better command — npm clean-install.

It will install packages with exact versions listed in package-lock.json if such a file exists. If not, an error will occur.

Can I just list the exact version of packages in package.json and use npm i all the time? 🤔

You think it could help, but it would not. The packages you have installed rely on other packages too, and they use version ranges, meaning npm i still can install newer available packages and mutate packge-lock.json.

My brain not braining .😫 Can you just sum it up for me? 🤔

That is why I am here 😏

Here is a rule set to follow:

  • if there is no package-lock.json use npm i only to load packages initially and generate a lock file
  • use npm ci instead of npm i when building your applications both on your CI and your local development machine. It guarantees the exact same version of every package between your dev and prod environments
  • if you want to add a new dependency, you still run npm install {dependency}
  • if you want to upgrade a package, use either npm update {dependency} or npm install ${dependendency}@{version} and commit the changed package-lock.json

Conclusion

I hope now you understand why package-lock.json is an essential tool for maintaining consistency, reproducibility, and stability in your projects. It enables deterministic builds, ensures consistency across environments, improves collaboration, and reduces the risk of unexpected changes.

Having said that, not many developers understand its importance. If you think that everything I have described is some kind of nonsense, just continue using npm i. However, now, you at least know what to discuss with other developers in the kitchen during the launch break.😏

If you’re happy and you learned something new clap your hands👏👏
If you’re happy and you want to support me, use the link below to buy me a coffee☕️
If you’re happy and you know it and you really want to show it, follow me to get those even more ✅✅

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence