npm 06 ๐ Understanding Semantic Versioning (SemVer: Major, Minor, Patch)
Semantic Versioning is a formal specification for how version numbers are assigned and incremented. It exists because version numbers without a shared meaning are useless for dependency management: a consumer cannot look at 2.4.1 and know whether upgrading to 2.5.0 is safe, or whether 3.0.0 will break their code. SemVer solves this by tying each of the three numbers to a specific category of change, so the version number itself communicates the compatibility contract .
The specification defines a version as MAJOR.MINOR.PATCH. The major version increments when the public API changes in a backward-incompatible way. The minor version increments when functionality is added in a backward-compatible way. The patch version increments when backward-compatible bug fixes are made . npm adopted SemVer as its versioning scheme, and the symbols in package.jsonโthe caret (^) and tilde (~)โare range operators built on top of the SemVer contract. Understanding SemVer is therefore not academic; it determines what npm install will actually install.
This chapter covers the three version components and their increment rules, the pre-release and build metadata extensions, the precedence rules that determine which version is “greater,” the 0.y.z initial development convention, and the npm range operators that translate SemVer into dependency resolution behavior. It assumes familiarity with the package.json file and the npm install command.
Key point: SemVer is a contract between a package maintainer and its consumers. The major number is a promise about compatibility; the minor and patch numbers are promises about what kind of change was made. Breaking the contractโshipping a breaking change in a minor versionโis worse than shipping no versioning scheme at all, because consumers trust the number and stop reading the changelog.
Why SemVer exists as a formal specification
Dependency hell is the problem SemVer was designed to solve. In a system with many dependencies, releasing a new version of a package becomes a nightmare if dependency specifications are too tight (version lock: you cannot upgrade without releasing new versions of every dependent package) or too loose (version promiscuity: you assume compatibility with future versions more than is reasonable) . SemVer provides the rules that let a consumer specify “any compatible version” and trust that the resolver will find one.
The version number is a communication channel. Without a formal specification, version numbers are essentially useless for dependency management because different maintainers increment them differently. One maintainer bumps the major for a new feature; another bumps the minor for a breaking change. SemVer makes the increment rules explicit, so a consumer can write ^1.2.3 and know that the resolver will not install 2.0.0, because 2.0.0 by definition contains a breaking change .
The specification is small and unambiguous. The SemVer 2.0.0 specification has a set of numbered rules and a BackusโNaur Form grammar for valid versions. The rules cover the version core, pre-release identifiers, and build metadata. The grammar means a version string can be validated mechanically, which is what tools like npm’s semver parser do .
The trade-off. SemVer requires maintainers to declare a public API and to classify every change against it. That classification is a judgment call: is renaming a parameter a breaking change? Is adding a new required field to a response a minor or a major? The specification cannot answer these questions; it only says that whatever the maintainer decides, the version number must reflect it. A maintainer who misclassifies changes breaks the contract, and consumers who trusted the number are burned.
a. The three components and their increment rules
The version core is X.Y.Z, where X is the major version, Y is the minor version, and Z is the patch version. Each element is a non-negative integer with no leading zeroes. The elements increase numerically, so 1.9.0 precedes 1.10.0, which precedes 1.11.0 .
| Component | Increment When | Reset Behavior |
|---|---|---|
| MAJOR | Backward-incompatible API changes | Minor and patch reset to 0 |
| MINOR | Backward-compatible functionality added | Patch resets to 0 |
| PATCH | Backward-compatible bug fixes | None |
The npm documentation recommends starting at 1.0.0 for the first stable release. A bug fix produces 1.0.1. A backward-compatible new feature produces 1.1.0 (the patch resets to zero). A breaking change produces 2.0.0 (both minor and patch reset to zero) .
// package.json version field follows SemVer.
{
"name": "my-package",
"version": "1.4.2"
}
// 1 = major (breaking changes)
// 4 = minor (backward-compatible features)
// 2 = patch (backward-compatible fixes)
The rule that minor and patch reset when major increments, and that patch resets when minor increments, is what keeps the numbers meaningful. A version 2.0.0 does not mean “the second major release with zero minor changes and zero patches”; it means “the first release of the second major API.” The resets discard the history of the previous major line, which is correct because the previous line is no longer compatible .
b. Pre-release versions and build metadata
The version core can be extended with a pre-release tag and build metadata. A pre-release version appends a hyphen and a series of dot-separated identifiers: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-rc.1. A pre-release version has lower precedence than the associated normal version: 1.0.0-alpha < 1.0.0 .
// Pre-release progression for the 1.0.0 line.
"version": "1.0.0-alpha" // Very early, unstable.
"version": "1.0.0-alpha.1" // First alpha increment.
"version": "1.0.0-beta" // Features complete, bugs possible.
"version": "1.0.0-rc.1" // Release candidate.
"version": "1.0.0" // Stable release.
Pre-release identifiers have their own precedence rules. Identifiers consisting only of digits are compared numerically. Identifiers with letters or hyphens are compared lexically in ASCII sort order. Numeric identifiers always have lower precedence than non-numeric identifiers. A larger set of pre-release fields has higher precedence than a smaller set if all preceding identifiers are equal. The specification’s canonical example is 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 .
Build metadata is appended with a plus sign: 1.0.0+build.1, 1.0.0-beta+exp.sha.5114f85. Build metadata is ignored when determining precedence. Two versions that differ only in build metadata have the same precedence . This means build metadata is informational, not a version selector; a resolver treats 1.0.0+build.1 and 1.0.0+build.2 as the same version.
c. Major version zero and the npm range operators
The specification defines major version zero (0.y.z) as initial development. Anything may change at any time, and the public API should not be considered stable . A package at 0.4.1 has no compatibility guarantees, even across patch versions in the strict SemVer reading. The npm ecosystem treats 0.x versions with more caution, and the caret operator behaves differently for them: ^0.1.2 matches >=0.1.2 <0.2.0, not <1.0.0, because the minor version in the 0.x line is effectively the major version .
The npm range operators are the practical interface between SemVer and package.json. The caret (^) allows minor and patch updates: ^1.2.3 matches >=1.2.3 <2.0.0. The tilde (~) allows only patch updates: ~1.2.3 matches >=1.2.3 <1.3.0. An exact version (1.2.3) matches only that version. A wildcard (* or x) matches any version, which is dangerous for production because it accepts breaking changes .
{
"dependencies": {
"exact": "1.2.3", // Only 1.2.3
"patch": "~1.2.3", // >=1.2.3 <1.3.0
"minor": "^1.2.3", // >=1.2.3 <2.0.0
"caret-zero": "^0.2.3", // >=0.2.3 <0.3.0
"any": "*" // Any version (avoid in production)
}
}
When npm install runs, it resolves each range to the latest version that satisfies it and records the exact version in package-lock.json. Future installs use the locked version, even if a newer version satisfying the range has been published, unless the lock file is deleted or updated . The lock file is what makes reproducible builds possible; the range in package.json describes intent, and the lock file records the resolution.
Complete Example Session
// ============================================
// PART 1: THE VERSION CORE
// ============================================
{
"version": "1.4.2"
}
// 1 = major: breaking changes
// 4 = minor: backward-compatible features
// 2 = patch: backward-compatible fixes
// ============================================
// PART 2: INCREMENT RULES
// ============================================
// Bug fix: 1.4.2 โ 1.4.3
// New feature: 1.4.2 โ 1.5.0 (patch resets)
// Breaking change: 1.4.2 โ 2.0.0 (minor+patch reset)
// ============================================
// PART 3: PRE-RELEASE VERSIONS
// ============================================
// 1.5.0-alpha โ early development
// 1.5.0-beta.1 โ feature complete, testing
// 1.5.0-rc.1 โ release candidate
// 1.5.0 โ stable release
// Precedence:
// 1.5.0-alpha < 1.5.0-beta.1 < 1.5.0-rc.1 < 1.5.0
// ============================================
// PART 4: BUILD METADATA
// ============================================
// 1.5.0+build.123
// Build metadata ignored for precedence.
// 1.5.0+build.123 == 1.5.0+build.456
// ============================================
// PART 5: MAJOR VERSION ZERO
// ============================================
// 0.4.1 โ initial development.
// No stability guarantees.
// ^0.4.1 matches >=0.4.1 <0.5.0 (not <1.0.0).
// ============================================
// PART 6: NPM RANGE OPERATORS
// ============================================
{
"dependencies": {
"exact": "1.2.3", // 1.2.3 only
"tilde": "~1.2.3", // >=1.2.3 <1.3.0
"caret": "^1.2.3", // >=1.2.3 <2.0.0
"caret-zero": "^0.2.3", // >=0.2.3 <0.3.0
"wildcard": "*" // Any version
}
}
// ============================================
// PART 7: LOCK FILE RESOLUTION
// ============================================
// package.json: "foo": "^1.2.3"
// npm install resolves to 1.2.3 (or latest 1.x).
// package-lock.json records exact version.
// Future installs use locked version.
// Delete lock file โ re-resolve to latest 1.x.
// ============================================
// PART 8: BREAKING THE CONTRACT
// ============================================
// Shipping a breaking change in 1.5.0 (minor).
// Consumers with ^1.2.3 install 1.5.0.
// Their code breaks. The number lied.
// The changelog was ignored because the number
// promised compatibility.
// ============================================
// PART 9: CHOOSING A RANGE
// ============================================
// Library: ^1.2.3 (allow minor+patch).
// Application: ~1.2.3 (allow patch only).
// Critical dependency: 1.2.3 (exact).
// Production: avoid * (accepts breaking changes).
// ============================================
// PART 10: THE CONTRACT
// ============================================
// MAJOR: "This will break your code."
// MINOR: "This adds functionality, your code works."
// PATCH: "This fixes a bug, your code works."
// The number is a promise. Keep it.
Quick Reference
Version Components
| Component | Meaning | Increment When |
|---|---|---|
| MAJOR | Breaking changes | API incompatible |
| MINOR | New features | Backward-compatible |
| PATCH | Bug fixes | Backward-compatible |
npm Range Operators
| Operator | Example | Matches |
|---|---|---|
| Exact | 1.2.3 | 1.2.3 only |
| Tilde | ~1.2.3 | >=1.2.3 <1.3.0 |
| Caret | ^1.2.3 | >=1.2.3 <2.0.0 |
| Caret zero | ^0.2.3 | >=0.2.3 <0.3.0 |
| Wildcard | * | Any version |
Pre-release Precedence
| Version | Precedence |
|---|---|
1.0.0-alpha | Lowest |
1.0.0-alpha.1 | Higher |
1.0.0-beta | Higher |
1.0.0-rc.1 | Higher |
1.0.0 | Highest |
Increment Examples
| Change | From | To |
|---|---|---|
| Bug fix | 1.4.2 | 1.4.3 |
| New feature | 1.4.2 | 1.5.0 |
| Breaking change | 1.4.2 | 2.0.0 |
| Pre-release | 1.5.0 | 1.5.0-rc.1 |
Best Practices
โ Do This:
// Use caret for libraries. // โ
"library": "^1.2.3"
// Use tilde for applications (conservative). // โ
"app": "~1.2.3"
// Lock exact versions for critical dependencies. // โ
"critical": "1.2.3"
// Start stable releases at 1.0.0. // โ
"version": "1.0.0"
โ Don’t Do This:
// Use wildcard in production. // โ
"dependency": "*" // Accepts breaking changes.
// Ship a breaking change in a minor version. // โ
// 1.5.0 breaks consumers with ^1.2.3.
// Reuse a version number after publishing. // โ
// 1.2.3 must never change contents. Release 1.2.4.
// Use build metadata for precedence. // โ
// 1.0.0+build.2 == 1.0.0+build.1
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Breaking change in minor | Misclassified change | Major increment |
| Caret installs breaking version | 0.x caret behavior differs | Use ~ for 0.x |
| Lock file ignored | Deleted or not committed | Commit package-lock.json |
| Pre-release installed accidentally | Range includes prerelease | Prerelease excluded by default |
| Build metadata used as version | Misunderstanding precedence | Treat as informational |
Real-World Examples
1. Stable Version
"version": "2.1.3"
2. Pre-release
"version": "3.0.0-beta.1"
3. Caret Range
"dependency": "^2.1.3"
4. Tilde Range
"dependency": "~2.1.3"
5. Exact Version
"dependency": "2.1.3"
6. Caret Zero
"dependency": "^0.4.2"
7. Build Metadata
"version": "2.1.3+build.456"
8. Lock File Entry
"node_modules/foo": { "version": "2.1.3" }
9. Precedence Check
# 1.0.0-alpha < 1.0.0
10. Increment Bug Fix
// Before: 1.4.2
// After: 1.4.3
Visual
The SemVer Contract
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ VERSION = CONTRACT โ
โ โ
โ MAJOR.MINOR.PATCH โ
โ โ โ โ โ
โ โ โ โโโ "Bug fix. Safe." โ
โ โ โโโโโโโโโ "New feature. Safe." โ
โ โโโโโโโโโโโโโโโ "Breaking change." โ
โ โ
โ Consumer reads the number: โ
โ ^1.2.3 means "any 1.x.x, no 2.0.0." โ
โ โ
โ Maintainer increments: โ
โ Breaking โ 2.0.0 โ
โ Feature โ 1.3.0 โ
โ Fix โ 1.2.4 โ
โ โ
โ The number is a promise. โ
โ Breaking it breaks trust. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Increment Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INCREMENT RULES โ
โ โ
โ Breaking change: โ
โ 1.4.2 โ 2.0.0 โ
โ (minor+patch reset) โ
โ โ
โ New feature: โ
โ 1.4.2 โ 1.5.0 โ
โ (patch resets) โ
โ โ
โ Bug fix: โ
โ 1.4.2 โ 1.4.3 โ
โ โ
โ Resets discard history of the previous โ
โ line, which is correct: the previous line โ
โ is no longer compatible. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Pre-release Precedence
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PRE-RELEASE ORDER โ
โ โ
โ 1.0.0-alpha โ
โ < 1.0.0-alpha.1 โ
โ < 1.0.0-alpha.beta โ
โ < 1.0.0-beta โ
โ < 1.0.0-beta.2 โ
โ < 1.0.0-beta.11 โ
โ < 1.0.0-rc.1 โ
โ < 1.0.0 โ
โ โ
โ Numeric identifiers compared numerically. โ
โ Alphanumeric compared lexically. โ
โ Numeric < non-numeric. โ
โ More fields > fewer fields if equal prefix. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
npm Range Resolution
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ package.json โ package-lock.json โ
โ โ
โ "foo": "^1.2.3" โ
โ โ โ
โ โผ โ
โ npm install resolves to latest 1.x.x โ
โ โ โ
โ โผ โ
โ package-lock.json records 1.5.0 โ
โ โ โ
โ โผ โ
โ Future installs use 1.5.0 โ
โ (even if 1.6.0 published) โ
โ โ
โ Delete lock โ re-resolve to 1.6.0 โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Format | MAJOR.MINOR.PATCH |
| MAJOR increment | Breaking changes |
| MINOR increment | Backward-compatible features |
| PATCH increment | Backward-compatible fixes |
| Pre-release | 1.0.0-alpha, lower precedence |
| Build metadata | +build, ignored for precedence |
| Major zero | 0.y.z, unstable |
Caret (^) | Minor + patch |
Tilde (~) | Patch only |
| Lock file | Records resolved version |
Key takeaways:
- SemVer is a contract, not a numbering scheme. The major number promises compatibility; the minor and patch numbers promise the kind of change. Breaking the contractโshipping a breaking change in a minor versionโis worse than no versioning, because consumers trusted the number and skipped the changelog .
- The increment rules are mechanical. Breaking change increments major and resets minor and patch. New feature increments minor and resets patch. Bug fix increments patch only .
- Pre-release versions have lower precedence than the stable version.
1.0.0-rc.1 < 1.0.0. Build metadata is ignored for precedence; two versions differing only in build metadata are equal . - Major version zero is not stable.
0.y.zmeans initial development, and anything may change. The caret operator treats0.xdifferently:^0.2.3matches<0.3.0, not<1.0.0. - npm range operators are built on SemVer. Caret allows minor and patch. Tilde allows patch only. Exact pins one version. The lock file records the resolution, and future installs use it unless the lock file is deleted .
- Wildcards are dangerous in production.
*accepts any version, including breaking changes. Use caret for libraries, tilde for applications, and exact versions for critical dependencies .
Remember: Semantic Versioning is a promise encoded in three numbers. The major number says “this will break your code.” The minor number says “this adds functionality, your code still works.” The patch number says “this fixes a bug, your code still works.” npm’s caret and tilde operators are range selectors that trust the promise: ^1.2.3 installs any 1.x.x because the major number promises compatibility, and refuses 2.0.0 because the major number changed. When a maintainer breaks the promiseโshipping a breaking change in a minor versionโthe range selector installs the breaking version, and the consumer’s code fails. The number is small; the trust it carries is the entire dependency ecosystem.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!