| |

npm 06 ๐ŸŒ Understanding Semantic Versioning (SemVer: Major, Minor, Patch)

โฑ๏ธ Estimated read: 11 min | |

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 .

ComponentIncrement WhenReset Behavior
MAJORBackward-incompatible API changesMinor and patch reset to 0
MINORBackward-compatible functionality addedPatch resets to 0
PATCHBackward-compatible bug fixesNone

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

ComponentMeaningIncrement When
MAJORBreaking changesAPI incompatible
MINORNew featuresBackward-compatible
PATCHBug fixesBackward-compatible

npm Range Operators

OperatorExampleMatches
Exact1.2.31.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

VersionPrecedence
1.0.0-alphaLowest
1.0.0-alpha.1Higher
1.0.0-betaHigher
1.0.0-rc.1Higher
1.0.0Highest

Increment Examples

ChangeFromTo
Bug fix1.4.21.4.3
New feature1.4.21.5.0
Breaking change1.4.22.0.0
Pre-release1.5.01.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

PitfallWhy It HappensFix
Breaking change in minorMisclassified changeMajor increment
Caret installs breaking version0.x caret behavior differsUse ~ for 0.x
Lock file ignoredDeleted or not committedCommit package-lock.json
Pre-release installed accidentallyRange includes prereleasePrerelease excluded by default
Build metadata used as versionMisunderstanding precedenceTreat 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

ItemValue
FormatMAJOR.MINOR.PATCH
MAJOR incrementBreaking changes
MINOR incrementBackward-compatible features
PATCH incrementBackward-compatible fixes
Pre-release1.0.0-alpha, lower precedence
Build metadata+build, ignored for precedence
Major zero0.y.z, unstable
Caret (^)Minor + patch
Tilde (~)Patch only
Lock fileRecords 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.z means initial development, and anything may change. The caret operator treats 0.x differently: ^0.2.3 matches <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!