What the three numbers mean
A semantic version number has exactly three parts separated by dots, for example 2.4.1. Each part is a non-negative integer and each carries a specific promise about backward compatibility.
MAJOR(X.0.0)
Increment when you make a breaking change.
Existing code written against the old major version may no longer work. Consumers must actively upgrade and check the migration guide.
MINOR(1.X.0)
Increment when you add new functionality in a backward-compatible way.
Existing code will continue to work without changes. Consumers can upgrade safely to get new features.
PATCH(1.0.X)
Increment when you make backward-compatible bug fixes.
No API change. Consumers should almost always take patch updates immediately.
Pre-release versions and build metadata
You can append a pre-release label with a hyphen. Pre-release versions have lower precedence than the release they precede: 1.0.0-rc.1 is considered older than 1.0.0.
| Version | Meaning |
|---|---|
| 1.0.0-alpha | Very early; API likely to change |
| 1.0.0-beta.1 | Feature-complete but may have bugs |
| 1.0.0-rc.2 | Release candidate; barring critical bugs, this ships |
Build metadata (1.0.0+build.42) is appended with a +. It is ignored for version precedence comparisons and is typically used by build systems for traceability.
Version ranges in package.json
When you add a dependency, npm writes a version range into package.json, not an exact version. The range tells npm the set of versions it is allowed to install.
| Range | Name | Resolves to | Notes |
|---|---|---|---|
| 1.2.3 | Exact | Exactly 1.2.3 | Only this specific version. Rarely used directly; npm uses the lock file to pin versions. |
| ^1.2.3 | Caret | >=1.2.3 <2.0.0 | Allows minor and patch updates within the same major. The default when you run npm install. |
| ~1.2.3 | Tilde | >=1.2.3 <1.3.0 | Allows patch updates only. Stricter than caret; useful when a minor bump has historically caused issues. |
| * | Wildcard | Latest of anything | Dangerous in production. Never pin a dependency to * in a real project. |
| >=1.2.0 <2.0.0 | Range | Any version in the range | Explicit bounds. Rare in package.json but common in peer dependency declarations. |
The special case of 0.x versions
Versions below 1.0.0 are considered unstable. The caret operator behaves more conservatively for them to reflect that a minor bump may contain breaking changes.
| Range | Resolves to | Note |
|---|---|---|
| ^0.1.2 | >=0.1.2 <0.2.0 | Behaves like tilde for 0.x : only patch updates |
| ^0.0.3 | >=0.0.3 <0.0.4 | Locks to exact version for 0.0.x |
| ~0.1.2 | >=0.1.2 <0.2.0 | Same as caret in this case |
Git tags and releases
The convention is to prefix git tags with a lowercase "v": v1.2.3. This is not required by semver (which defines only the number format) but is universal in practice.
Creating a release tag
git tag -a v1.2.3 -m "Release 1.2.3" git push origin v1.2.3
GitHub Releases are built on top of git tags. When you create a release in the GitHub UI, it creates a tag, generates a zip of that commit, and publishes release notes. Many CI pipelines watch for tag pushes matching v* to trigger a publish to npm.
Common mistakes
- Not bumping MAJOR on breaking changes. The most damaging mistake. If you rename a function, remove a field, or change a behavior that consumers rely on, you must bump MAJOR. Failing to do so silently breaks downstream projects when they upgrade.
- Using * or latest in production dependencies. * installs whatever is newest at the moment npm runs, which could be a version published an hour ago. Always pin to a range (^ or ~) and commit your lock file.
- Not committing the lock file. package-lock.json (npm) or yarn.lock records the exact resolved versions of every transitive dependency. Without it, two developers running npm install on the same package.json can get different dependency trees.
- Treating 0.x as stable. By semver convention, any version below 1.0.0 makes no stability guarantees. Breaking changes are allowed on any minor bump. Upgrade 0.x dependencies cautiously and read changelogs.
Frequently asked questions
Should I use ^ or ~ in package.json?
Use ^ (caret) as the default. It lets npm take minor and patch updates automatically, which is usually what you want. Tighten to ~ when a dependency has a history of introducing regressions in minor releases, or when a peer dependency spec requires it.
What does it mean when a package is at 0.x?
It means the author considers the API unstable. Breaking changes may happen on any minor bump. Projects like webpack and many CLI tools stayed at 0.x for years while their API evolved. Treat 0.x dependencies like pre-releases: upgrade manually and check the changelog.
How do I see what version will actually be installed?
Run npm install --dry-run to preview what would be installed. You can also run npm outdated to see the current, wanted, and latest versions of each package. The "wanted" column is what your version range resolves to right now.
Why does npm use a lock file if package.json has ranges?
Version ranges mean npm can resolve to different versions at different points in time as packages release updates. The lock file records the exact resolved version tree at one point, making installs reproducible. Always commit your lock file to version control.
What happens when I run npm update?
npm update resolves every dependency to the highest version allowed by the range in package.json, updates the lock file, and installs the new versions. It respects your ranges: if you have ^1.2.3, it will not install 2.0.0 even if it exists. To upgrade across major versions, use npm install package@latest.