Semantic Versioning

Difficulty: Beginner

Semantic Versioning (semver) is a versioning convention that gives meaning to version numbers. It is the standard used by virtually all npm packages and is essential to understand for managing dependencies safely. A semver version consists of three numbers separated by dots: MAJOR.MINOR.PATCH (e.g., 2.4.1). Each number has a specific meaning that communicates the nature of changes to consumers of the package.

The PATCH number (rightmost) is incremented for backward-compatible bug fixes that do not add new functionality. The MINOR number (middle) is incremented when you add new functionality in a backward-compatible manner - existing code that depends on your package will continue to work. The MAJOR number (leftmost) is incremented when you make incompatible API changes, also known as breaking changes. A major version bump signals to consumers that they may need to modify their code to upgrade. Version 0.x.y is special: it indicates initial development where the API is not yet stable, and even minor version bumps may contain breaking changes.

In package.json, dependency versions often include a prefix character that defines what range of versions is acceptable. The caret (^) is the default when you run npm install and is the most common. ^2.4.1 means 'compatible with 2.4.1': it allows updates that do not change the leftmost non-zero number. So ^2.4.1 matches >=2.4.1 and <3.0.0. The tilde (~) is more restrictive: ~2.4.1 allows only patch updates, matching >=2.4.1 and <2.5.0. An exact version (no prefix, e.g., 2.4.1) pins to that specific version.

Other version range syntax includes >= and < for explicit ranges, || for combining ranges (e.g., '>=1.0.0 <2.0.0 || >=3.0.0'), * or latest for the latest available version, and x or empty positions as wildcards (2.x matches any 2.minor.patch). Understanding these ranges is crucial because they determine which version npm installs when you run 'npm install' and whether 'npm update' will pull in new versions.

npm provides several commands for managing versions. 'npm outdated' shows which installed packages have newer versions available, distinguishing between current, wanted (latest matching your range), and latest (absolute latest). 'npm update' updates packages to the latest version matching the range in package.json. 'npm audit' checks for known security vulnerabilities in your dependency tree. 'npm version patch/minor/major' bumps the version in your own package.json, creates a git commit, and tags it. For libraries, always follow semver strictly - consumers depend on it to safely update.

Code examples

Semver Ranges Explained

// Demonstrating version range matching
const ranges = {
  // Caret (^): allows minor and patch updates
  '^2.4.1': 'matches >=2.4.1 <3.0.0 (e.g., 2.5.0, 2.99.0)',
  '^0.3.1': 'matches >=0.3.1 <0.4.0 (special: 0.x treats minor as major)',
  '^0.0.3': 'matches >=0.0.3 <0.0.4 (only exact patch for 0.0.x)',

  // Tilde (~): allows only patch updates
  '~2.4.1': 'matches >=2.4.1 <2.5.0',
  '~2.4':   'matches >=2.4.0 <2.5.0',
  '~2':     'matches >=2.0.0 <3.0.0',

  // Exact and other ranges
  '2.4.1':    'matches only 2.4.1 exactly',
  '>=2.0.0':  'matches 2.0.0 and above',
  '>=1.0 <3': 'matches 1.x and 2.x only',
  '*':        'matches any version'
};

Object.entries(ranges).forEach(([range, desc]) => {
  console.log(`${range.padEnd(15)} -> ${desc}`);
});

The caret (^) is the most common range specifier and npm's default. It allows updates that don't change the leftmost non-zero number. Be careful with 0.x packages: ^0.3.1 only allows patch updates (0.3.x) because the API is considered unstable.

Version Bumping Workflow

// Starting version: 1.2.3

// Bug fix that doesn't change the API
// npm version patch -> 1.2.4
console.log('patch: 1.2.3 -> 1.2.4 (bug fix)');

// New feature, backward compatible
// npm version minor -> 1.3.0 (patch resets to 0)
console.log('minor: 1.2.3 -> 1.3.0 (new feature)');

// Breaking change
// npm version major -> 2.0.0 (minor and patch reset to 0)
console.log('major: 1.2.3 -> 2.0.0 (breaking change)');

// Pre-release versions
console.log('pre-release: 2.0.0-alpha.1, 2.0.0-beta.1, 2.0.0-rc.1');
console.log('pre-release < release: 2.0.0-alpha.1 < 2.0.0');

// npm version creates a git commit and tag automatically
console.log('\nnpm version patch creates:');
console.log('  - Updates package.json version');
console.log('  - Creates git commit: "1.2.4"');
console.log('  - Creates git tag: v1.2.4');

Semantic versioning communicates intent. Patch = safe to update automatically. Minor = safe, has new features. Major = may break your code. Pre-release versions (alpha, beta, rc) sort before the release version.

Checking for Updates with npm outdated

// npm outdated output shows:
// Package    Current  Wanted  Latest  Location
// express    4.18.2   4.19.0  5.0.0   my-app
// lodash     4.17.20  4.17.21 4.17.21 my-app
// react      18.2.0   18.3.1  19.0.0  my-app

const outdated = [
  { pkg: 'express', current: '4.18.2', wanted: '4.19.0', latest: '5.0.0' },
  { pkg: 'lodash',  current: '4.17.20', wanted: '4.17.21', latest: '4.17.21' },
  { pkg: 'react',   current: '18.2.0', wanted: '18.3.1', latest: '19.0.0' }
];

outdated.forEach(({ pkg, current, wanted, latest }) => {
  const safe = wanted !== current ? `safe update to ${wanted}` : 'up to date';
  const breaking = latest !== wanted ? ` (major: ${latest})` : '';
  console.log(`${pkg}: ${current} -> ${safe}${breaking}`);
});

console.log('\nnpm update  -> installs "wanted" versions (within range)');
console.log('npm install express@latest -> installs latest (may break!)');

'Wanted' is the latest version matching your package.json range (safe to install). 'Latest' is the absolute latest version. If Latest differs from Wanted, there's a major version available that may contain breaking changes. Run 'npm update' for safe updates within your ranges.

Key points

Concepts covered

semver, Caret ^, Tilde ~, Version Ranges, npm outdated, Breaking Changes