Semantic Versioning (SemVer): Major, Minor and Patch Explained
SemVer turns a version number into a promise about compatibility. The 2.0.0 specification in plain words, what counts as a breaking change, pre-releases, 0.x, how versions sort, npm’s caret and tilde, and automating the bump from commit messages.
7 min read
Semantic versioning, or SemVer, is a rule for numbering releases as MAJOR.MINOR.PATCH so that the number tells users whether an upgrade is safe. You increase PATCH for backward-compatible bug fixes, MINOR for backward-compatible new functionality, and MAJOR for changes that break the public API. A hyphen adds a pre-release label such as 2.0.0-rc.1, a plus sign adds build metadata, and anything below 1.0.0 is initial development where anything may change. The current specification is SemVer 2.0.0.
It starts with a public API
The Semantic Versioning 2.0.0 specification (opens in a new tab) opens with a requirement most people skip: software using SemVer must declare a public API. The numbers only mean something relative to that promise. For a library, the public API is the exported functions and types. For a command-line tool, it is the commands, flags and output other scripts parse. For a web service, it is the endpoints, request fields and response shapes. Write down what is public, and treat everything else as internal.
The specification also says that once a version is released, its contents must not be modified; any change ships as a new version. Retagging 1.4.2 with different code breaks every user who already installed it.
MAJOR.MINOR.PATCH in plain words
- PATCH: backward-compatible bug fixes only. The spec defines a bug fix as an internal change that fixes incorrect behavior.
- MINOR: new backward-compatible functionality, or marking something in the public API as deprecated. Reset PATCH to 0.
- MAJOR: any backward-incompatible change to the public API. Reset MINOR and PATCH to 0.
- A higher level may include lower-level changes: a minor release can carry fixes, and a major release can carry both.
1.4.2 current release 1.4.3 fix: rounding error in sales tax totals PATCH 1.5.0 feat: optional ZIP+4 support in addresses MINOR 1.6.0 deprecate formatAddress() in favor of format() MINOR 2.0.0 remove formatAddress() MAJOR
What counts as a breaking change
A breaking change is anything that can make correct code written against the old version fail against the new one. Most are obvious; a few are not.
- Removing or renaming a function, endpoint, flag, field or config key.
- Adding a required parameter, or making an optional one required.
- Changing a type, a unit, a date format or a default value.
- Rejecting input the old version accepted, such as tightening validation.
- Dropping support for a runtime version. The Conventional Commits specification uses exactly this as an example of a breaking change.
- Usually not breaking: adding an optional parameter, a new endpoint or a new optional response field, as long as existing callers keep working unchanged.
The gray area is a bug fix that someone depended on. If users relied on the wrong behavior, fixing it can break them, and a major version is the honest number. If you ship a breaking change in a minor release by mistake, the specification’s FAQ says to fix it with a new minor release that restores compatibility and to document the offending version. Updating your own dependencies without changing your public API is compatible; whether it is a patch or a minor depends on whether you updated to fix a bug or to add something.
Pre-release versions and build metadata
A pre-release adds a hyphen and dot-separated identifiers after the patch number: 1.0.0-alpha, 1.0.0-alpha.1, 2.0.0-rc.1. Identifiers use letters, digits and hyphens, and numeric identifiers must not have leading zeros. A pre-release signals that the version is unstable and may not meet the compatibility promise its normal version will make.
Build metadata adds a plus sign and identifiers, for example 1.0.0+20261001 or 1.0.0-beta+exp.sha.5114f85. It records how something was built and is ignored when comparing versions, so two builds that differ only in metadata have the same precedence.
Precedence: how versions sort
Versions compare MAJOR, then MINOR, then PATCH, numerically, so 1.10.0 is newer than 1.9.0. A pre-release sorts before its normal version. Between two pre-releases, identifiers are compared one at a time: numeric ones as numbers, alphanumeric ones in ASCII order, numeric lower than alphanumeric, and a longer list wins if everything before it is equal. The specification’s own example:
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
One trap follows from those rules. beta.11 sorts after beta.2 because each is a separate numeric identifier, but rc10 sorts before rc9, because a single alphanumeric identifier is compared character by character. Always put a dot before the number.
0.y.z: initial development
Major version zero is for initial development. The specification says anything may change at any time and the public API should not be considered stable. Its FAQ offers a simple test for leaving it: if your software is being used in production, it should probably already be 1.0.0. Staying on 0.x for years tells users nothing about what is safe to upgrade.
Package managers add their own convention on top. The node-semver README (opens in a new tab), the library npm uses for ranges, notes that many authors treat the second number of a 0.x version as the breaking-change indicator, so npm’s caret range is stricter below 1.0.0, as the next section shows.
npm version ranges: caret and tilde
In package.json, a range says which future versions of a dependency you will accept. npm’s semantic versioning page (opens in a new tab) gives the short form: tilde, as in ~1.0.4, accepts patch releases; caret, as in ^1.0.4, accepts minor releases; * or x accepts major releases. The exact bounds:
^1.2.3 >=1.2.3 <2.0.0-0 minor and patch updates ~1.2.3 >=1.2.3 <1.3.0-0 patch updates only ^0.2.3 >=0.2.3 <0.3.0-0 patch updates only, below 1.0.0 ^0.0.3 >=0.0.3 <0.0.4-0 no updates at all 1.2.3 exactly 1.2.3
By default, npm’s configuration (opens in a new tab) saves new dependencies with a caret, so npm install of version 1.2.3 writes ^1.2.3. Ranges also skip pre-releases unless the range itself names a pre-release of the same MAJOR.MINOR.PATCH, so ^1.2.3 never pulls in 1.3.0-beta.1 by surprise, even though it sorts inside the range. Your lockfile pins exact versions, so ranges matter when the lockfile is updated.
npm version patch # 1.4.2 -> 1.4.3 npm version minor # 1.4.3 -> 1.5.0 npm version premajor --preid=rc # 1.5.0 -> 2.0.0-rc.0 npm version major # 2.0.0-rc.0 -> 2.0.0
The npm version command (opens in a new tab) writes the new number to package.json and the lockfile and, inside a git repository, also creates a version commit and tag.
Automating the bump with Conventional Commits
Choosing the number by hand works until someone forgets. Conventional Commits (opens in a new tab) is a commit message format that maps onto SemVer: a fix commit means a patch, a feat commit means a minor, and a ! after the type or a BREAKING CHANGE: footer means a major, whatever the type. Other types such as docs or refactor do not change the version unless they include a breaking change. Release tools read the commits since the last tag and pick the next number.
fix(tax): round sales tax per line, not per order feat(address): accept ZIP+4 codes feat(address)!: remove formatAddress() BREAKING CHANGE: use format() instead. Refs: FET-014
- Turning those commits into a readable record is covered in how to write a changelog.
- Writing the version up for users is covered in the release notes template.
Where a task board fits
fenbs is a simple task board shared by people and AI assistants. It has no release or version field and does not tag anything; your repository and package manager do that. What it adds is a stable reference for the work behind each version. Task refs such as FET-014 are never reused, even after a delete, so a Refs: FET-014 footer keeps pointing at the same task forever. Every task is a feature, an enhancement or a bug, which is a first hint at the bump, but only a hint: a bug fix that changes a response shape is still a major release.
Your team’s versioning rules, such as “any change to a response field is a major version” or “we stay on 0.x until the first production customer,” belong on the Decisions and rules page. A person decides each rule, and every connected AI assistant reads the rules before it writes a commit message.
Related
From commits to a changelog: how to write a changelog. Where version bumps run automatically: what a CI/CD pipeline is. Checking that a new version still talks to its neighbors: integration testing. How fenbs sorts work: features, enhancements and bugs.