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 rangessemver.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 — npm's official SemVer implementation:
Basic version operations
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)
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
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
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
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
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
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 — 1KB version comparison:
Basic usage
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
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
// 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 > bora === b - Simple sorting of version strings without complex range logic
- You don't need
^1.0.0style ranges — just>=1.0.0operators
Use Node.js built-ins if:
- Just comparing
process.versionto a fixed string —parseInton 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 →
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
semveras 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-versionsis under 1KB vssemver'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 and h3 vs polka vs koa 2026, better-sqlite3 vs libsql vs sql.js.