<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/semver-vs-compare-versions-vs-semver-satisfies-version-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/semver-vs-compare-versions-vs-semver-satisfies-version-2026/raw.md -->
<!-- Source path: content/guides/semver-vs-compare-versions-vs-semver-satisfies-version-2026.mdx -->

---
og_image: "/images/guides/semver-vs-compare-versions-vs-semver-satisfies-version-2026.webp"
title: "semver vs compare-versions (2026)"
description: "Compare semver and compare-versions for semantic versioning in Node.js. npm range resolution, satisfies(), coerce(), and when to use each library in 2026."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["nodejs", "typescript", "developer-tools", "api"]
tier: 1
---

## TL;DR

**semver** is npm's official semantic version library — the same one the npm CLI uses. It handles everything: version comparison, range checking (`^1.0.0`, `>=2.0.0`), coercion, and validity. **compare-versions** is the lightweight alternative — just 1KB, handles comparison and sorting with no overhead, perfect for simple `a > b` checks without the full SemVer range syntax. For anything touching npm, package managers, or version ranges: `semver`. For simple version comparison in browser or size-critical code: `compare-versions`.

## Key Takeaways

- **semver**: ~120M weekly downloads — npm's official library, full SemVer 2.0, ranges, coerce
- **compare-versions**: ~15M weekly downloads — 1KB, just comparison, no ranges
- Node.js has `process.version` — use semver to check if running Node meets requirements
- `semver.satisfies("18.3.0", ">=18.0.0")` — how npm resolves peer dependency ranges
- `semver.coerce("v1.2.3-beta")` — clean dirty version strings from user input or package metadata
- Both are TypeScript-native in 2026

---

## Download Trends

| Package | Weekly Downloads | Bundle Size | Range Checking | Coerce | Sorting |
|---------|-----------------|-------------|---------------|--------|---------|
| `semver` | ~120M | ~25KB | ✅ | ✅ | ✅ |
| `compare-versions` | ~15M | ~1KB | ❌ | ❌ | ✅ |

---

## semver

[semver](https://github.com/npm/node-semver) — npm's official SemVer implementation:

### Basic version operations

```typescript
import semver from "semver"

// Validate:
semver.valid("1.2.3")          // "1.2.3"
semver.valid("1.2.3-beta.1")   // "1.2.3-beta.1"
semver.valid("not-a-version")  // null

// Compare (returns -1, 0, or 1):
semver.compare("1.2.3", "1.2.4")  // -1 (1.2.3 < 1.2.4)
semver.compare("2.0.0", "1.9.9")  // 1  (2.0.0 > 1.9.9)
semver.compare("1.0.0", "1.0.0")  // 0  (equal)

// Boolean comparisons:
semver.gt("2.0.0", "1.9.9")   // true
semver.lt("1.0.0", "2.0.0")   // true
semver.gte("1.0.0", "1.0.0")  // true
semver.lte("1.0.0", "1.0.0")  // true
semver.eq("1.0.0", "1.0.0")   // true
semver.neq("1.0.0", "2.0.0")  // true
```

### Range checking (npm-style)

```typescript
import semver from "semver"

// Caret range (compatible with):
semver.satisfies("1.2.3", "^1.0.0")   // true  (>=1.0.0 <2.0.0)
semver.satisfies("2.0.0", "^1.0.0")   // false (breaks major)

// Tilde range (patch-level changes):
semver.satisfies("1.2.3", "~1.2.0")   // true  (>=1.2.0 <1.3.0)
semver.satisfies("1.3.0", "~1.2.0")   // false (breaks minor)

// Comparison operators:
semver.satisfies("2.0.0", ">=2.0.0")  // true
semver.satisfies("1.9.9", ">=2.0.0")  // false

// Hyphen range:
semver.satisfies("1.5.0", "1.0.0 - 2.0.0")  // true (inclusive range)

// Wildcard:
semver.satisfies("1.5.3", "1.5.x")  // true
semver.satisfies("1.6.0", "1.5.x")  // false

// OR ranges:
semver.satisfies("3.0.0", "^1.0.0 || ^2.0.0 || ^3.0.0")  // true

// Star (any version):
semver.satisfies("99.0.0", "*")  // true
```

### Checking Node.js/runtime version requirements

```typescript
import semver from "semver"

// Check if current Node.js meets requirements:
function checkNodeVersion(required: string) {
  const current = process.version  // "v22.13.0"

  if (!semver.satisfies(process.version, required)) {
    throw new Error(
      `Requires Node.js ${required}, but found ${current}. Please upgrade.`
    )
  }
}

checkNodeVersion(">=18.0.0")  // OK on Node.js 22
checkNodeVersion(">=22.0.0")  // Throw on Node.js 18

// Check engine requirements from package.json:
async function validateEngines() {
  const pkg = JSON.parse(await fs.readFile("package.json", "utf8"))
  const engines = pkg.engines

  if (engines?.node && !semver.satisfies(process.version, engines.node)) {
    console.warn(
      `Warning: This package requires Node.js ${engines.node}. ` +
      `You have ${process.version}.`
    )
  }
}
```

### Coerce — clean dirty version strings

```typescript
import semver from "semver"

// Handle user-provided or registry versions that aren't clean:
semver.coerce("v1.2.3")          // SemVer { major: 1, minor: 2, patch: 3 }
semver.coerce("v1.2.3-beta")     // { major: 1, minor: 2, patch: 3 } (pre-release stripped)
semver.coerce("1.2")             // { major: 1, minor: 2, patch: 0 } (fills patch)
semver.coerce("3")               // { major: 3, minor: 0, patch: 0 } (fills minor + patch)
semver.coerce("  v1.2.3  ")      // Trims whitespace
semver.coerce("not a version")   // null

// Use for package metadata that may have non-standard version strings:
function parsePackageVersion(raw: string): string | null {
  const coerced = semver.coerce(raw)
  return coerced ? coerced.version : null
}
```

### Increment versions

```typescript
import semver from "semver"

const current = "1.2.3"

semver.inc(current, "patch")   // "1.2.4"
semver.inc(current, "minor")   // "1.3.0"
semver.inc(current, "major")   // "2.0.0"

// Pre-release versions:
semver.inc(current, "prerelease", "beta")  // "1.2.4-beta.0"
semver.inc("1.2.4-beta.0", "prerelease")   // "1.2.4-beta.1"
semver.inc("1.2.4-beta.1", "patch")        // "1.2.4" (promotes to stable)
```

### Sorting versions

```typescript
import semver from "semver"

const versions = ["1.10.0", "1.9.0", "2.0.0", "1.1.0", "0.5.0"]

// Sort ascending:
const sorted = versions.sort(semver.compare)
// ["0.5.0", "1.1.0", "1.9.0", "1.10.0", "2.0.0"]
// Note: 1.10 > 1.9 (numeric, not lexicographic — semver handles this correctly)

// Sort descending:
const sortedDesc = versions.sort(semver.rcompare)
// ["2.0.0", "1.10.0", "1.9.0", "1.1.0", "0.5.0"]

// Max/min:
semver.maxSatisfying(["1.0.0", "1.5.0", "2.0.0"], "^1.0.0")  // "1.5.0"
semver.minSatisfying(["1.0.0", "1.5.0", "2.0.0"], "^1.0.0")  // "1.0.0"
```

### Range analysis

```typescript
import semver from "semver"

const range = new semver.Range("^1.0.0")

// Check range properties:
range.test("1.5.0")   // true
range.test("2.0.0")   // false

// Intersect ranges:
const a = new semver.Range(">=1.0.0")
const b = new semver.Range("<2.0.0")
semver.intersects(a, b)  // true (ranges overlap)

// Outside check:
semver.outside("3.0.0", "^1.0.0", ">")  // true (3.0.0 is > the max of ^1.0.0)
```

---

## compare-versions

[compare-versions](https://github.com/omichelsen/compare-versions) — 1KB version comparison:

### Basic usage

```typescript
import { compareVersions, compare, satisfies, validate } from "compare-versions"

// Compare:
compareVersions("1.2.3", "1.2.4")   // -1
compareVersions("2.0.0", "1.9.9")   // 1
compareVersions("1.0.0", "1.0.0")   // 0

// Boolean helpers:
compare("1.0.0", "2.0.0", "<")   // true
compare("2.0.0", "1.0.0", ">")   // true
compare("1.0.0", "1.0.0", ">=")  // true
compare("1.0.0", "1.0.0", "<=")  // true
compare("1.0.0", "1.0.0", "=")   // true
compare("1.0.0", "2.0.0", "!=")  // true

// Validate:
validate("1.2.3")          // true
validate("not-a-version")  // false

// Range satisfies (simple operators only — no ^ or ~ ranges):
satisfies("1.2.3", ">=1.0.0")   // true
satisfies("1.2.3", ">=1.0.0 <2.0.0")  // true (multiple conditions)
satisfies("2.0.0", ">=1.0.0 <2.0.0")  // false
// Note: No caret (^) or tilde (~) support — use semver for those
```

### Sorting

```typescript
import { compareVersions } from "compare-versions"

const versions = ["1.10.0", "1.9.0", "2.0.0", "1.1.0"]

const sorted = versions.sort(compareVersions)
// ["1.1.0", "1.9.0", "1.10.0", "2.0.0"]
// Handles numeric comparison correctly (1.10 > 1.9)
```

### When to use compare-versions

```typescript
// compare-versions excels for:
// 1. Browser environments where bundle size matters
// 2. Simple comparison/sorting without npm-style ranges
// 3. Zero-dependency scripts

// Example: version badge in a browser widget
import { compare } from "compare-versions"

function isOutdated(current: string, latest: string) {
  return compare(current, latest, "<")
}

// Example: sort user-inputted version list
import { compareVersions } from "compare-versions"

const userVersions = userInput.split(",").sort(compareVersions)
```

---

## Feature Comparison

| Feature | semver | compare-versions |
|---------|--------|-----------------|
| Bundle size | ~25KB | ~1KB |
| npm ranges (^ ~) | ✅ | ❌ |
| Simple operators (> < =) | ✅ | ✅ |
| Coerce dirty versions | ✅ | ❌ |
| Increment (major/minor/patch) | ✅ | ❌ |
| Max/min satisfying | ✅ | ❌ |
| TypeScript | ✅ | ✅ |
| Browser-safe | ✅ | ✅ |
| Pre-release handling | ✅ Full | ✅ Basic |

---

## When to Use Each

**Choose semver if:**
- Working with npm packages or package.json version ranges
- You need `satisfies()` with caret (`^`) or tilde (`~`) ranges
- Building package managers, version resolvers, or update checkers
- You need `coerce()` to clean up dirty version strings
- Any serious version manipulation work

**Choose compare-versions if:**
- Bundle size is critical (1KB vs 25KB)
- Browser environment where you just need `a > b` or `a === b`
- Simple sorting of version strings without complex range logic
- You don't need `^1.0.0` style ranges — just `>=1.0.0` operators

**Use Node.js built-ins if:**
- Just comparing `process.version` to a fixed string — `parseInt` on the major is often enough
- Simple scripts without complex version logic

---

## compare-versions for the Browser: When Bundle Size Wins

The `compare-versions` library exists because `semver` carries 25KB of code to solve the general case, and many applications only need to answer simple questions: is version A newer than version B? Sort these version strings. Validate that this string looks like a version. For browser-side code — widgets, developer dashboards, version badge components — including 25KB of npm's version resolution engine to answer "is 18.3.0 newer than 18.2.1?" is excessive.

`compare-versions` solves this with approximately 1KB of code and zero dependencies. It handles numeric comparison correctly — something naive string comparison fails at, since `"1.10.0" > "1.9.0"` is false in lexicographic string comparison but true numerically. It handles the basic comparison operators (`>`, `<`, `>=`, `<=`, `=`, `!=`). It handles pre-release tags at a basic level. And it provides `compareVersions` as a comparator function compatible with `Array.prototype.sort`, making version list sorting a one-liner.

What it deliberately omits is npm-style range syntax. There is no support for caret ranges (`^1.0.0`), tilde ranges (`~1.2.0`), hyphen ranges, OR ranges, or wildcard versions. If you need to check whether a version satisfies a complex range, you need `semver`. But if your use case is "the user has version 2.3.1 installed and the latest is 2.4.0, should I show an update badge?" — `compare-versions` is the right tool. It answers that question with a fraction of the code and zero risk of pulling in a Node.js-focused dependency that may have compatibility issues in browser environments.

## How npm Uses semver: Version Resolution in Practice

The `semver` package is not a utility you call manually to compare version strings — it is the core of npm's dependency resolution algorithm. Every time you run `npm install`, the CLI is using `semver.satisfies()` to determine which versions of each dependency are compatible with the range you've declared. When you write `"react": "^18.0.0"` in your package.json, npm interprets the caret range as `>=18.0.0 <19.0.0` and considers any React version in that range as satisfying your requirement. The `semver` package defines exactly what those ranges mean.

During `npm install`, npm builds a dependency tree by fetching the list of available versions for each package from the registry and checking them against declared ranges. When two packages in your tree require different versions of a shared dependency, npm uses `semver.intersects()` to determine if the two ranges overlap — if they do, a single version can satisfy both. If they don't, npm installs separate copies at different levels of `node_modules`, which is why deeply nested projects sometimes have multiple versions of the same library.

The range syntax the semver package implements is richer than most developers realize. The caret (`^`) and tilde (`~`) operators are the most common, but the specification also includes hyphen ranges (`1.0.0 - 2.0.0` for inclusive ranges), `x` wildcards (`1.5.x` for any patch of 1.5), OR ranges (`^1.0.0 || ^2.0.0` for multi-major support), and the `*` wildcard. All of these are interpreted by `semver.satisfies()`. Understanding this range syntax is essential when debugging "Cannot find peer dependency" errors or writing automation that needs to check version compatibility — both extremely common tasks in monorepo and CI environments.

## Building a Dependency Update Tool with semver

Beyond simple comparison, the `semver` package is the right foundation for any tooling that needs to reason about package versions programmatically. The most common pattern in production codebases is using it for engine version checks in CLI tools: read the `engines.node` field from package.json, call `semver.satisfies(process.version, engines.node)`, and throw a clear error at startup if the running Node.js version doesn't meet the requirement. This is far more robust than string comparison — it handles the `v` prefix in `process.version`, compares numerically rather than lexicographically, and handles pre-release tags correctly.

For dependency update automation — whether you're building a custom update script, a CI check, or integrating with tools like Renovate or Dependabot — `semver.maxSatisfying(availableVersions, range)` is the key function. Given the list of all published versions and your current range, it returns the latest version that still satisfies the range. `semver.minSatisfying()` does the reverse. For release tooling and changelog generation, `semver.inc(current, releaseType)` calculates the next version: `semver.inc("1.2.3", "patch")` returns `"1.2.4"`, `"minor"` returns `"1.3.0"`, and `"major"` returns `"2.0.0"`. For pre-release workflows, it handles the full progression from `1.0.0-beta.0` through `1.0.0-beta.1` to `1.0.0` correctly.

The `semver.coerce()` function deserves special mention for any tool that processes version strings from external sources. User input, legacy metadata, and third-party registries often produce non-standard version strings: `"v1.2.3"`, `"1.2"`, `"3"`, `"  v1.2.3-build.456  "`. semver coerce handles all of these by extracting the meaningful numeric parts, stripping prefixes and build metadata, and returning a clean SemVer object. This makes it a reliable pre-processing step before any version comparison or range check that might otherwise throw on malformed input.

## Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on semver v7.x and compare-versions v6.x.

## Building Update Checkers and Version Resolvers

The most common real-world use of `semver` beyond basic comparison is building update-checker functionality — determining whether an installed package version satisfies a newer requirement, or surfacing outdated dependencies to users. The `semver.maxSatisfying(versions, range)` function is the core primitive here: given an array of available versions and a range constraint, it returns the highest version that satisfies the range. This is exactly how `npm install` resolves peer dependencies.

A practical update checker for a CLI tool typically fetches the latest version from the npm registry (`https://registry.npmjs.org/{packageName}/latest`), compares it with `process.env.npm_package_version` (or a hardcoded `version` from `package.json`), and uses `semver.gt(latest, current)` to decide whether to show an update notice. The `semver.diff("1.2.3", "1.3.0")` method returns the release type (`"minor"` in this case), letting you communicate whether the update is a patch, minor, or major change — a useful signal for users deciding whether to update immediately.

For monorepo tooling and package managers, `semver.intersects(range1, range2)` and `semver.subset(range1, range2)` are the critical functions for dependency deduplication. When two packages both depend on `lodash` with ranges `^4.17.0` and `>=4.15.0`, checking whether the ranges overlap determines if a single installed version can satisfy both — avoiding duplicate packages in `node_modules`. These range intersection functions are part of what makes `semver` the appropriate choice for package-management-adjacent tooling, while `compare-versions` is only suitable for simpler use cases.

## Pre-release Version Handling

Pre-release versions (`1.0.0-alpha.1`, `2.0.0-beta.3`, `1.5.0-rc.2`) follow specific ordering rules in semantic versioning that differ from stable version sorting, and the two libraries handle them differently.

In semver, a pre-release version is lower than the stable release it precedes: `1.0.0-alpha.1 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0`. The `semver` library implements this fully. Critically, pre-release versions are excluded from range matching by default unless the range itself contains a pre-release identifier. `semver.satisfies("1.0.0-alpha.1", ">=1.0.0")` returns `false` — the alpha is not considered to satisfy a range that only mentions stable versions. This is the correct npm behavior: `npm install package@^1.0.0` does not install `1.1.0-beta.0`. To include pre-releases in range matching, you must use the `{ includePrerelease: true }` option.

`compare-versions` sorts pre-release versions correctly in the same SemVer ordering, but its range-checking (`satisfies()`) does not implement the npm-style pre-release exclusion behavior. If you need the npm-compatible pre-release exclusion logic, `semver` is required. For tools that display changelogs, sort release history, or present pre-release warnings to users, both libraries handle the ordering correctly, and `compare-versions` is sufficient if you just need to sort or compare without range resolution.

*[Compare utility and version management packages on PkgPulse →](https://www.pkgpulse.com)*

## When to Use Each

**Use the `semver` package if:**
- You are building a package manager, CLI tool, or dependency resolver that needs full semver spec support
- You need range parsing: `^1.2.0`, `~1.2.0`, `>=1.2.0 <2.0.0`, `1.x`, `*`
- You need pre-release version handling: `1.0.0-alpha.1`, `1.0.0-beta.2`
- You need version coercion from loose strings like `"1"` or `"1.0"`
- You are already using npm or pnpm tooling that bundles `semver` as a dependency anyway

**Use `compare-versions` if:**
- You only need to compare two version strings (`1.2.3 < 1.3.0`)
- Bundle size matters — `compare-versions` is under 1KB vs `semver`'s ~20KB
- You do not need range satisfaction, pre-release handling, or version coercion
- You are building a frontend component that displays version badges or changelogs

**Use `semver-satisfies` (or `semver.satisfies()`) if:**
- You need to check whether a version meets a semver range constraint
- You are building tooling that validates package engine compatibility (`"engines": { "node": ">=18" }`)

In 2026, for any application that just needs to compare two version strings, `compare-versions` is the minimal choice. For anything involving range notation or ecosystem tooling, `semver` is the correct dependency despite its larger size.

A practical note for package authors: if you are adding version comparison to a library, prefer `compare-versions` for simple comparisons to avoid pulling the full `semver` package (and its test suite and locale handling) into your dependency tree. Your library's consumers may already have `semver` in their tree, but keeping your published package lean is good practice.

## Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on `semver` v7.x, `compare-versions` v6.x. The `semver` package is maintained by the npm team and is a direct dependency of npm itself. Its large bundle size (~20KB) reflects its comprehensive spec coverage including pre-release version ordering, build metadata handling, and the full range syntax (caret, tilde, hyphen ranges, X-ranges). `compare-versions` intentionally omits range syntax to stay lean — its single purpose is comparing two version strings and returning -1, 0, or 1.


*See also: [pm2 vs node:cluster vs tsx watch](/guides/pm2-vs-node-cluster-vs-tsx-watch-process-2026) and [h3 vs polka vs koa 2026](/guides/h3-vs-polka-vs-koa-lightweight-http-frameworks-nodejs-2026), [better-sqlite3 vs libsql vs sql.js](/guides/better-sqlite3-vs-libsql-vs-sql-js-sqlite-nodejs-2026).*
