Server

Semantic Versioning in Practice

By Prabath Thalangama· October 24, 2026· 3 min read
#versioning#release#git

Introduction

Semantic Versioning (MAJOR.MINOR.PATCH) is a promise to consumers of your package or API about what an upgrade will do to them.

The contract

Given a public API:

  • PATCH (1.4.2 → 1.4.3) — backward-compatible bug fix. Consumers upgrade without reading anything.
  • MINOR (1.4.2 → 1.5.0) — backward-compatible new functionality. Additive. Safe to upgrade; may want to read the changelog for new features.
  • MAJOR (1.4.2 → 2.0.0) — a backward-incompatible change. Consumers must read the migration notes.

"Backward compatible" is defined by your documented public API. Renaming an internal function isn't a breaking change; removing a documented CLI flag is.

Pre-release and metadata

1.0.0-alpha
1.0.0-alpha.1
1.0.0-beta.2
1.0.0-rc.1
1.0.0
1.0.0+20260827.sha.5114f85     # build metadata — ignored in precedence

Precedence: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0. Pre-release versions sort before the release. Build metadata (+...) never affects ordering.

The 0.x rule

Before 1.0.0, anything can break at any time. The convention many ecosystems adopt: in 0.y.z, a MINOR bump (0.4.x → 0.5.0) may break, a PATCH (0.4.1 → 0.4.2) shouldn't. Publishing 1.0.0 is a statement that you'll honour the full contract from now on.

Version ranges

npm / semver syntax (others are similar):

Range Matches Notes
1.2.3 exactly that
^1.2.3 >=1.2.3 <2.0.0 "compatible" — the default npm install writes
~1.2.3 >=1.2.3 <1.3.0 patch-level only
>=1.2.3 <1.5.0 explicit range
* / latest anything don't
^0.2.3 >=0.2.3 <0.3.0 caret is special-cased for 0.x

A lockfile (package-lock.json, poetry.lock, Cargo.lock, go.sum) pins the exact resolved versions so builds are reproducible; the range in the manifest just bounds what an update may pick.

Automating it

Deriving the next version by hand is error-prone. Drive it from Conventional Commits:

feat: add --json output flag          -> MINOR
fix: handle empty config file         -> PATCH
refactor!: drop Node 16 support       -> MAJOR  (! or "BREAKING CHANGE:" footer)
docs: fix typo                        -> no release

Tools: semantic-release, release-please, changesets, commitizen. On merge to the main branch they compute the bump, update the changelog, tag, and publish.

# release-please, abridged
- uses: googleapis/release-please-action@v4
  with:
    release-type: node

Verification and troubleshooting

npx semver 1.2.3 -r "^1.0.0"          # does it satisfy the range?
npx semver -i minor 1.4.2             # -> 1.5.0
git tag --sort=-v:refname | head
  • ^ upgrade broke a build — a dependency shipped a breaking change in a MINOR release (SemVer violation on their part). Pin it (~ or exact) and report upstream. This is why lockfiles + tested dependency-update PRs (Renovate/Dependabot) matter.
  • Two ranges can't be satisfied together — A needs lib@^1, B needs lib@^2. npm may install both copies (nested); other ecosystems error. Update or replace one consumer.
  • Pre-release not installing — ranges don't match pre-releases unless the range itself has one (^1.0.0-0 or --include=prerelease).
  • Automated release didn't fire — commit messages don't follow the convention, or the only commits were chore:/docs: (no release by design), or the tool needs a feat/fix since the last tag.
  • Version in code out of sync with the git tag — let the release tool be the single source; read the version from package metadata at runtime, don't hardcode it in two places.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.