npm — Semver & Version Ranges
Semantic Versioning (SemVer) is the versioning scheme used by npm and most of the JavaScript ecosystem. It provides a standard way to communicate the nature of changes between releases, enabling npm to safely resolve dependency ranges.
Understanding SemVer and npm version ranges is essential for managing dependencies, publishing packages, and avoiding unexpected breaking changes in your projects.
Every npm package version follows the MAJOR.MINOR.PATCH format. Each segment has a specific meaning that signals the type of change:
| Component | Example | Meaning |
|---|---|---|
| MAJOR | 3 | Breaking changes — may break existing code that depends on this package |
| MINOR | 2 | New features — backwards compatible, new functionality added |
| PATCH | 1 | Bug fixes — backwards compatible, bugs fixed, no new features |
| 1 | // Example: package.json with version |
| 2 | { |
| 3 | "name": "my-library", |
| 4 | "version": "3.2.1", |
| 5 | "description": "MAJOR 3 . MINOR 2 . PATCH 1" |
| 6 | } |
| 7 | |
| 8 | // Version meaning: |
| 9 | // 3.2.1 -> MAJOR=3, MINOR=2, PATCH=1 |
| 10 | // 0.x.x -> Initial development (anything may break) |
| 11 | // 1.0.0 -> First stable release |
info
The caret (^) is the default range specifier in npm. It allows changes at the leftmost non-zero component. This is the most common range operator.
| 1 | // ^ allows MINOR and PATCH updates |
| 2 | // but not MAJOR updates |
| 3 | |
| 4 | "^1.2.3" // >=1.2.3 and <2.0.0 |
| 5 | "^0.2.3" // >=0.2.3 and <0.3.0 (leftmost non-zero is MINOR) |
| 6 | "^0.0.3" // >=0.0.3 and <0.0.4 (leftmost non-zero is PATCH) |
| 7 | "^1" // >=1.0.0 and <2.0.0 |
| 8 | "^1.2" // >=1.2.0 and <2.0.0 |
| 9 | |
| 10 | // Examples of what gets installed: |
| 11 | "^1.0.0" can install 1.0.0, 1.0.1, 1.1.0, 1.5.3 |
| 12 | but NOT 2.0.0 |
| 13 | |
| 14 | "^0.1.0" can install 0.1.0, 0.1.1, 0.1.5 |
| 15 | but NOT 0.2.0 or 1.0.0 |
best practice
The tilde (~) is more restrictive than caret. It allows only PATCH-level changes by default, or MINOR changes when the minor version is specified.
| 1 | // ~ allows only the most specific level to change |
| 2 | |
| 3 | "~1.2.3" // >=1.2.3 and <1.3.0 (only PATCH) |
| 4 | "~1.2" // >=1.2.0 and <1.3.0 (only PATCH) |
| 5 | "~1" // >=1.0.0 and <2.0.0 (same as ^1) |
| 6 | "~0.2.3" // >=0.2.3 and <0.3.0 |
| 7 | |
| 8 | // Compared with caret: |
| 9 | // ^1.2.3 -> 1.2.3 to 1.999.999 (MINOR + PATCH) |
| 10 | // ~1.2.3 -> 1.2.3 to 1.2.999 (just PATCH) |
| 11 | |
| 12 | // Use tilde when you want maximum stability: |
| 13 | "~4.17.21" // lodash: only patch updates within 4.17.x |
| 14 | "~3.0.0" // bootstrap: only critical fixes |
info
Pinning to an exact version ensures everyone installs the identical dependency. This gives maximum reproducibility but requires manual updates for bug fixes and security patches.
| 1 | // Exact version — no range specifier |
| 2 | "lodash": "4.17.21" |
| 3 | "react": "18.3.1" |
| 4 | "express": "4.21.0" |
| 5 | |
| 6 | // Save with exact version |
| 7 | npm install lodash --save-exact |
| 8 | # or set npm config: |
| 9 | npm config set save-exact true |
| 10 | |
| 11 | // Advantages: |
| 12 | // - Fully reproducible builds |
| 13 | // - No surprises from dependency updates |
| 14 | // - Recommended for production applications |
| 15 | |
| 16 | // Disadvantages: |
| 17 | // - You miss bug fixes and security patches |
| 18 | // - Requires manual effort to stay updated |
| 19 | |
| 20 | // Best practice: pin exact versions in applications, |
| 21 | // use ranges in libraries |
best practice
The npm version command automatically bumps the version in package.json, creates a Git tag, and optionally commits the change. It understands SemVer and increments the correct segment.
| 1 | # Bump each segment: |
| 2 | npm version patch # 1.0.0 -> 1.0.1 |
| 3 | npm version minor # 1.0.1 -> 1.1.0 |
| 4 | npm version major # 1.1.0 -> 2.0.0 |
| 5 | |
| 6 | # Pre-release bumps: |
| 7 | npm version premajor # 1.0.0 -> 2.0.0-0 |
| 8 | npm version preminor # 1.0.0 -> 1.1.0-0 |
| 9 | npm version prepatch # 1.0.0 -> 1.0.1-0 |
| 10 | npm version prerelease # 1.0.1-0 -> 1.0.1-1 |
| 11 | |
| 12 | # Custom pre-release identifier: |
| 13 | npm version premajor --preid=alpha # 1.0.0 -> 2.0.0-alpha.0 |
| 14 | |
| 15 | # Skip Git tag creation: |
| 16 | npm version patch --no-git-tag-version |
| 17 | |
| 18 | # Skip commit: |
| 19 | npm version patch --no-commit-hooks |
| 20 | |
| 21 | # From a specific version string: |
| 22 | npm version "2.0.0-beta" # set exact version |
| 23 | |
| 24 | # View current version: |
| 25 | npm version |
| 26 | # { 'my-package': '1.2.3', npm: '10.5.0', ... } |
Beyond caret and tilde, npm supports a full set of range operators for complex dependency resolution:
| 1 | // Comparison operators: |
| 2 | ">=1.2.3" // Greater than or equal to |
| 3 | "<=1.2.3" // Less than or equal to |
| 4 | ">1.2.3" // Strictly greater than |
| 5 | "<1.2.3" // Strictly less than |
| 6 | |
| 7 | // AND conditions (space separated): |
| 8 | ">=1.2.0 <2.0.0" // Equivalent to ^1.2.0 |
| 9 | |
| 10 | // OR conditions (|| operator): |
| 11 | "1.2.3 || 1.3.0" // Either exact version |
| 12 | "^1.0.0 || ^2.0.0" // Either 1.x or 2.x |
| 13 | |
| 14 | // Hyphen ranges: |
| 15 | "1.2.3 - 2.3.4" // >=1.2.3 <=2.3.4 |
| 16 | |
| 17 | // Wildcard/partial versions: |
| 18 | "1.2.x" // >=1.2.0 <1.3.0 |
| 19 | "1.*" // >=1.0.0 <2.0.0 |
| 20 | "*" // any version |
| 21 | |
| 22 | // X-ranges (same as wildcard): |
| 23 | "1.2.X" // >=1.2.0 <1.3.0 |
| 24 | |
| 25 | // URL dependencies: |
| 26 | "git+ssh://git@github.com:user/repo.git#v1.0.0" |
| 27 | "github:user/repo" |
| 28 | "file:../local-package" |
| 29 | |
| 30 | // Combined example: |
| 31 | { |
| 32 | "dependencies": { |
| 33 | "express": "^4.21.0", |
| 34 | "lodash": "4.17.21", |
| 35 | "debug": ">=2.0.0 <4.0.0", |
| 36 | "my-lib": "file:./packages/my-lib" |
| 37 | } |
| 38 | } |
Use ^ for Library Dependencies
Libraries should use caret ranges to give consumers flexibility. Pinning exact versions in a library forces all downstream projects to use that exact version, causing duplication and bloated node_modules.
Pin Exact Versions in Applications
For production applications, pin exact versions to ensure reproducible builds. Use tools like Renovate or Dependabot to automate security updates.
Use --save-exact for Critical Dependencies
Run npm config set save-exact true if your team policy requires exact versions. Or use npm install --save-exact for individual packages.
Understand Lock Files
Even with ranges in package.json, the lock file (package-lock.json) records the exact resolved versions. This ensures reproducible installs across environments regardless of the range specifiers.