Skip to main content
{/}codefunc
WorkflowJuly 9, 2026 · 5 min read

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges, and Bump Workflows

Master SemVer for libraries and apps — pre-releases, caret vs tilde ranges, breaking-change discipline, and a release workflow that keeps dependents sane.

By codefunc

semverversioningnpmreleasespackagingworkflow

Version numbers are a contract. When you publish 2.4.1, consumers infer compatibility from three integers — not from your changelog prose. Get SemVer wrong and lockfiles drift, CI breaks on "minor" upgrades, and teams stop trusting ^ ranges. This guide covers the rules that matter for JavaScript packages and any API that claims semantic versions.

Bottom line: MAJOR.MINOR.PATCH means breaking / feature / fix. Pre-releases sort before the stable tag. Prefer caret ranges for apps you control; pin or review majors carefully. Use a SemVer Calculator when bumping or checking range membership instead of guessing.

The three numbers

Given version MAJOR.MINOR.PATCH:

Part Bump when Example
MAJOR Incompatible API change Rename export, remove endpoint, change default behavior
MINOR Backward-compatible feature New optional param, new endpoint, new export
PATCH Backward-compatible bug fix Crash fix, typo in docs bundled with package, perf with same API
1.0.0   →   1.0.1   patch (bugfix)
1.0.1   →   1.1.0   minor (new feature)
1.1.0   →   2.0.0   major (breaking)

Try these transitions in the SemVer Calculator to confirm the next version before you tag.

Precedence (highest → lowest impact)

  1. Major — consumers must read the migration guide
  2. Minor — safe to pull if they follow SemVer
  3. Patch — usually silent upgrades

If your "minor" removes a public function, you lied. Dependents who trusted ^1.0.0 will break. That erodes trust faster than a honest major bump.

Pre-release and build metadata

SemVer allows labels after the patch number:

1.2.3-alpha.1
1.2.3-beta.2
1.2.3-rc.1
1.2.3+build.45
Form Meaning
1.2.3-alpha.1 Pre-release; sorts before 1.2.3
1.2.3+build.45 Build metadata; ignored for precedence

Ordering examples:

1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0 < 1.0.1

npm and most registries treat pre-releases as opt-in. npm install pkg will not grab 2.0.0-beta when 1.9.0 is the latest stable unless the user asks for a tag or exact version.

When to ship pre-releases

  • Large majors with migration risk
  • Public betas for early adopters
  • Release candidates before cutting stable

Do not leave production apps on *-alpha without a deliberate decision.

Caret, tilde, and exact pins

npm-style ranges appear in package.json:

Range Allows Example matches for ^1.2.3
^1.2.3 (caret) Compatible with 1.x ≥ 1.2.3 1.2.3, 1.9.0 — not 2.0.0
~1.2.3 (tilde) Patch-level on 1.2.x 1.2.3, 1.2.9 — not 1.3.0
1.2.3 Exact Only 1.2.3
>=1.2.3 <2.0.0 Explicit range Same idea as caret for 1.x

Special case: ^0.2.3 only allows changes that do not modify the leftmost non-zero digit — so 0.y.z is treated as unstable. Many teams pin 0.x packages more tightly.

Verify membership with a SemVer Calculator before you widen a range in a shared library.

App vs library policy

Project type Suggested default
Application Caret on most deps; lockfile committed
Internal library Caret on deps; peerDeps for frameworks
Security-critical Exact pins + automated bump PRs
0.x dependencies Tilde or exact until 1.0

Always commit package-lock.json / pnpm-lock.yaml / yarn.lock for apps. Ranges declare intent; lockfiles declare reality.

What counts as breaking?

Public API includes anything consumers reasonably depend on:

  • Exported functions, types, and CLI flags
  • Default configuration values
  • Documented HTTP status codes and response shapes
  • Minimum supported Node / browser engines (raising them is often major)

Usually not breaking if undocumented:

  • Private _internal modules (if truly unexported)
  • Pure implementation refactors with identical observable behavior
  • Adding optional fields to JSON responses (still document; some strict clients break)

When unsure, bump major. A clean major beats a silent landmine.

Diff-driven bump decisions

Before choosing the bump:

  1. Diff against the last tag (git diff v1.4.2...HEAD).
  2. List public surface changes — exports, CLI, schema.
  3. Classify: fix / feature / break.
  4. Run the bump through a SemVer Calculator.
  5. For config or schema packages, a JSON Diff of package.json, OpenAPI, or JSON Schema files makes the contract change obvious.

Example changelog-driven mapping:

Change Bump
Fix null crash in parse() PATCH
Add optional timeout option MINOR
Rename parseparseInput MAJOR
Drop Node 18 support MAJOR

Release workflow that scales

Minimal team flow

  1. Conventional commits or PR labels: fix:, feat:, feat!: / BREAKING CHANGE:
  2. CI runs tests + lint on every PR
  3. Release PR or changeset / release-please computes the next version
  4. Changelog generated from commits
  5. Tag vX.Y.Z and publish
  6. Dependents get Dependabot/Renovate PRs against the range

Checklist before npm publish

  • Version in package.json matches intended SemVer bump
  • Changelog entry exists for the bump class
  • Types / docs updated for public API changes
  • Pre-release tag used if this is not yet stable
  • Lockfile and peer dependency ranges reviewed

Monorepos

Bump packages independently. A patch in @org/utils should not force a major of @org/cli unless the CLI's public behavior changed. Tools like Changesets track per-package bumps so you do not version the whole repo as one blob.

Common SemVer mistakes

Mistake Fix
Marketing majors ("2.0 because redesign") with no API break Use minor or keep major honest
Shipping breaks as minor "because migration is small" Still major
Forgetting 0.x caret rules Pin or read npm range docs
Publishing without a changelog Consumers cannot plan upgrades
Yanking versions from the registry Prefer deprecate + patch

Practical takeaway

Treat SemVer as a compatibility contract, not a marketing dial. Bump MAJOR for breaks, MINOR for features, PATCH for fixes; use pre-releases for risky majors. Prefer caret ranges with a committed lockfile, verify bumps and range matches with a SemVer Calculator, and inspect config or schema deltas with JSON Diff before you publish.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool