package.json Explained: Dependencies, Scripts and Semver
Basics4 minInnovialabs editorial team
In short
package.json is the passport of a JavaScript project: it lists the name, scripts and dependencies with version ranges, while the lock file pins exact versions so every install builds the same thing.
What package.json is
package.json is a file in the root of a JavaScript project that describes the project and everything it needs to run. npm, pnpm or yarn read it to know which packages to install, and you read it to know which commands build, test and start the app.
You can create one with npm init -y, but it usually comes with a framework template.
Key fields
{
"name": "my-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"test": "vitest"
},
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"typescript": "~5.4.0"
},
"engines": {
"node": ">=20"
}
}
- name and version — the package identity. Required if you publish a library.
- private: true — prevents accidental publishing to npm. Use it for applications.
- type — which module system Node.js uses for
.jsfiles:"module"(ES Modules) or"commonjs"(the default). - scripts — named commands. Run them with
npm run dev;testandstartalso work asnpm testandnpm start. - main / exports — the entry point for anyone importing your package.
- engines — supported Node.js versions. Helps catch incompatibilities early.
dependencies, devDependencies and peerDependencies
| Field | What goes there | Example |
|---|---|---|
| dependencies | code needed at runtime | react, express, axios |
| devDependencies | development and build tools | typescript, eslint, vitest |
| peerDependencies | packages the consuming project must provide | react for a component library |
For a bundled frontend app the line between the first two is blurry, but for server code and libraries it matters: a production install with --omit=dev skips devDependencies.
peerDependencies are for library authors. If your component library installed its own copy of React, the project would end up with two copies, which causes errors. So the library declares React as a peer and uses the version the project already has.
How to read versions: semver
A version follows MAJOR.MINOR.PATCH, for example 4.17.2:
- MAJOR — breaking API changes.
- MINOR — new features without breaking anything.
- PATCH — bug fixes.
Ranges in package.json:
| Notation | What is allowed | Example for 1.4.2 |
|---|---|---|
1.4.2 | this exact version | 1.4.2 |
~1.4.2 | newer patches | 1.4.2 up to <1.5.0 |
^1.4.2 | newer minors and patches | 1.4.2 up to <2.0.0 |
* or latest | anything | avoid |
One detail: for 0.x versions the caret is stricter. ^0.4.2 allows only 0.4.x, because before 1.0 any minor release may break the API.
Semver is a convention, not a guarantee. A maintainer can ship a breaking change in a patch by mistake. The lock file protects you from that.
Why the lock file matters
package-lock.json (or pnpm-lock.yaml, yarn.lock) records exact versions of every package, including nested dependencies. Without it, two developers installing on different days may get different code.
Rules:
- Always commit the lock file.
- On CI and servers use
npm ci— it installs exactly what the lock file says and fails if the lock file and package.json disagree. - Do not mix package managers: one project, one lock file.
How to update packages safely
- See what is outdated:
npm outdated. - Update within the allowed ranges:
npm update. - Upgrade major versions one at a time, after reading the changelog and migration notes.
- Run the build and tests after each update.
- Check for vulnerabilities with
npm audit. Do not runnpm audit fix --forceblindly — it can jump major versions. - Commit package.json and the lock file together.
Bots like Dependabot or Renovate automate this: they open a separate pull request per update, and CI shows right away what broke.
Common mistakes
- Lock file in .gitignore — builds become unpredictable.
npm installinstead ofnpm cion the server — the lock file can be rewritten.- Build tools in dependencies — the production image gets bloated.
- Editing the lock file by hand — change it only through the package manager.