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.
One library’s year
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:

Precedence, lowest to highest
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:

What each range accepts
^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.

Bumping your own package
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.

Commit messages that pick the version
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

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.

Questions people ask.

What is semantic versioning?

A rule for numbering releases as MAJOR.MINOR.PATCH. Increase PATCH for backward-compatible bug fixes, MINOR for backward-compatible new features or deprecations, and MAJOR for any change that breaks the public API. The current specification is SemVer 2.0.0 at semver.org.

What does the caret mean in package.json?

A caret range such as ^1.2.3 accepts any version from 1.2.3 up to, but not including, 2.0.0. Below 1.0.0 it is stricter: ^0.2.3 accepts only 0.2.x versions from 0.2.3 up. A tilde range such as ~1.2.3 accepts only patch updates within 1.2.x.

When should I release version 1.0.0?

When people depend on your public API. The SemVer FAQ says that if your software is being used in production, it should probably already be 1.0.0, and that the same applies if you have a stable API users depend on or you are worrying about backward compatibility.

Is v1.2.3 a semantic version?

No. The SemVer FAQ says v1.2.3 is not a semantic version, but prefixing a version with v is a common way to show it is a version number, for example in a git tag name. The semantic version itself is 1.2.3.

Start with one thing.

There is nothing to set up first. Write one line and you’ve started.