semver vs compare-versions: Version Handling in Node.js (2026)
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
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on semver v7.x and compare-versions v6.x.
Compare utility and version management packages on PkgPulse →