Skip to main content
Technology & EngineeringPackage Management211 lines

Semantic Versioning

Semantic versioning (SemVer) conventions, version ranges, and strategies for managing breaking changes

Quick Summary31 lines
You are an expert in semantic versioning (SemVer), including version range syntax, pre-release conventions, and strategies for managing breaking changes in published packages.

## Key Points

- **MAJOR** (e.g., 1.x.x to 2.0.0): Incompatible API changes. Consumers may need to update their code.
- **MINOR** (e.g., 1.1.x to 1.2.0): New functionality added in a backward-compatible manner.
- **PATCH** (e.g., 1.1.1 to 1.1.2): Backward-compatible bug fixes.
- `^0.2.3` resolves to `>=0.2.3 <0.3.0` (MINOR is treated as breaking).
- `^0.0.3` resolves to `>=0.0.3 <0.0.4` (effectively exact).
1. Document all breaking changes in the changelog.
2. Provide a migration guide.
3. Consider a codemod for automated upgrades.
4. Publish release candidates first: `2.0.0-rc.1`, `2.0.0-rc.2`.
5. Maintain the previous major with security patches for a defined period.
- **Fixed/uniform**: All packages share the same version (Lerna fixed mode, Changesets fixed groups).
- **Independent**: Each package has its own version, bumped independently.

## Quick Example

```
2.0.0-alpha.1
2.0.0-beta.3
2.0.0-rc.1
```

```
1.0.0+20240101
1.0.0+build.42
```
skilldb get package-management-skills/Semantic VersioningFull skill: 211 lines
Paste into your CLAUDE.md or agent config

Semantic Versioning — Package Management

You are an expert in semantic versioning (SemVer), including version range syntax, pre-release conventions, and strategies for managing breaking changes in published packages.

Core Philosophy

Overview

Semantic Versioning (SemVer) is the universal versioning contract for npm packages. A version string MAJOR.MINOR.PATCH communicates the nature of changes to consumers: breaking changes bump MAJOR, new features bump MINOR, and bug fixes bump PATCH. Understanding SemVer deeply — including range syntax, pre-release identifiers, and real-world edge cases — is essential for both publishers and consumers.

Core Concepts

The SemVer Contract

Given a version MAJOR.MINOR.PATCH:

  • MAJOR (e.g., 1.x.x to 2.0.0): Incompatible API changes. Consumers may need to update their code.
  • MINOR (e.g., 1.1.x to 1.2.0): New functionality added in a backward-compatible manner.
  • PATCH (e.g., 1.1.1 to 1.1.2): Backward-compatible bug fixes.

Pre-release Versions

Pre-release identifiers are appended with a hyphen:

2.0.0-alpha.1
2.0.0-beta.3
2.0.0-rc.1

Pre-release versions have lower precedence than the release version: 1.0.0-alpha.1 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0

Build Metadata

Build metadata is appended with + and is ignored during version comparison:

1.0.0+20240101
1.0.0+build.42

Version Ranges in npm

SyntaxMeaningExample rangeMatches
^1.2.3Compatible with version (same MAJOR)>=1.2.3 <2.0.01.2.3, 1.9.9
~1.2.3Approximately equivalent (same MAJOR.MINOR)>=1.2.3 <1.3.01.2.3, 1.2.9
1.2.3Exact version1.2.31.2.3 only
*Any version>=0.0.0Everything
>=1.2.3Greater than or equal>=1.2.31.2.3, 2.0.0, etc.
1.2.xAny patch version>=1.2.0 <1.3.01.2.0 through 1.2.x
1.xAny minor/patch>=1.0.0 <2.0.0Same as ^1.0.0
1.2.3 - 2.0.0Inclusive range>=1.2.3 <=2.0.0Between the two
>=1.0.0 <1.5.0Combined rangeIntersectionBoth conditions met
^1.2.3 || ^2.0.0Union of rangesEither rangeMatches if either is true

The 0.x Exception

For versions below 1.0.0, SemVer treats the API as unstable:

  • ^0.2.3 resolves to >=0.2.3 <0.3.0 (MINOR is treated as breaking).
  • ^0.0.3 resolves to >=0.0.3 <0.0.4 (effectively exact).

This is a frequent source of confusion. Packages at 0.x signal that any minor bump may break.

Implementation Patterns

Bumping Versions with npm

# Patch: bug fix
npm version patch        # 1.2.3 -> 1.2.4

# Minor: new feature
npm version minor        # 1.2.3 -> 1.3.0

# Major: breaking change
npm version major        # 1.2.3 -> 2.0.0

# Pre-release
npm version premajor --preid=beta   # 1.2.3 -> 2.0.0-beta.0
npm version prerelease --preid=beta # 2.0.0-beta.0 -> 2.0.0-beta.1

# Skip git tag creation
npm version patch --no-git-tag-version

Automating Version Decisions with Conventional Commits

Conventional Commits map commit types to SemVer bumps:

fix: correct null check in parser     -> PATCH
feat: add streaming API               -> MINOR
feat!: redesign configuration format  -> MAJOR
BREAKING CHANGE: removed legacy API   -> MAJOR

Tools like semantic-release or changesets automate the version bump based on commit history:

# semantic-release (fully automated)
npx semantic-release

# changesets (manual changeset files, automated version + publish)
npx changeset        # developer adds a changeset
npx changeset version # CI bumps versions
npx changeset publish # CI publishes

Changeset File Example

---
"@myorg/parser": minor
"@myorg/cli": patch
---

Added streaming support to the parser. Updated CLI to use the new streaming API.

Communicating Breaking Changes

When publishing a major version:

  1. Document all breaking changes in the changelog.
  2. Provide a migration guide.
  3. Consider a codemod for automated upgrades.
  4. Publish release candidates first: 2.0.0-rc.1, 2.0.0-rc.2.
  5. Maintain the previous major with security patches for a defined period.
# Publish RC
npm version 2.0.0-rc.1
npm publish --tag next

# When stable
npm version 2.0.0
npm publish

Version Resolution in Monorepos

In a monorepo, workspace packages reference each other. Version strategies:

  • Fixed/uniform: All packages share the same version (Lerna fixed mode, Changesets fixed groups).
  • Independent: Each package has its own version, bumped independently.
// .changeset/config.json
{
  "fixed": [["@myorg/core", "@myorg/cli"]],
  "linked": [["@myorg/plugin-*"]]
}

Peer Dependency Ranges

Peer dependencies use ranges to declare compatibility:

{
  "peerDependencies": {
    "react": "^17.0.0 || ^18.0.0"
  }
}

This tells consumers: "I work with React 17 or 18. You provide it."

Best Practices

  • Follow SemVer strictly once you hit 1.0.0. Consumers depend on the contract.
  • Use ^ (caret) ranges for dependencies in libraries. This gives consumers maximum flexibility for deduplication.
  • Use exact versions or ~ (tilde) ranges for application dependencies where you want tighter control.
  • Stay at 0.x only during genuine early development. Reach 1.0.0 as soon as the API stabilizes.
  • Use Conventional Commits to make version bumps deterministic and auditable.
  • Publish pre-release versions (-alpha, -beta, -rc) to a non-latest dist tag.
  • Write changelogs that describe the impact on consumers, not the implementation details.
  • When deprecating an API, mark it as deprecated in one minor version before removing it in the next major.
  • In monorepos, use linked or fixed version groups for packages that must stay in sync.
  • Run npm outdated or npx npm-check-updates regularly to review available updates.

Common Pitfalls

  • Breaking changes in a PATCH or MINOR: The most damaging mistake. Consumers on ^ ranges auto-install it, and their builds break.
  • 0.x confusion: ^0.2.3 does not behave like ^1.2.3. Minor bumps at 0.x are treated as potentially breaking, and the range is much narrower.
  • Pre-release range matching: ^1.0.0 does NOT match 1.0.1-beta.0. Pre-release versions only match ranges that explicitly include a pre-release on the same MAJOR.MINOR.PATCH.
  • Forgetting --tag for pre-releases: Publishing 2.0.0-beta.1 without --tag beta makes it latest, and all npm install users get the pre-release.
  • Lockfile masking range issues: A lockfile pins exact versions, so range bugs only surface when consumers install fresh. Always test your ranges in a clean environment.
  • Over-broad peer dependency ranges: Declaring "react": "*" as a peer dependency promises compatibility with every version, which is almost never true.
  • Skipping the changelog: Users cannot adopt new major versions without understanding what changed. Always document breaking changes.
  • Using >= without an upper bound: ">=1.0.0" will match version 47.0.0. Always bound your ranges.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add package-management-skills

Get CLI access →