npm’s SemVer (Semantic Versioning) allows packages to declare their compatibility with other packages, but most developers treat it as a suggestion rather than a strict rule.

Let’s see SemVer in action with a simple dependency:

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

Here, ^4.17.21 means "allow updates to lodash that are not breaking changes." Specifically, it allows versions 4.17.21 up to, but not including, 5.0.0. This is because 4 is the major version, 17 is the minor, and 21 is the patch. SemVer dictates that only changes to the major version are considered breaking.

The problem SemVer solves is managing the inherent complexity of software dependencies. When you npm install, the package manager uses SemVer to decide which versions of your dependencies to download. Without it, every update to a dependency would be a gamble, potentially breaking your application in subtle and unpredictable ways.

Internally, npm parses these version ranges. When you run npm install lodash@^4.17.21, npm checks the lodash registry. It finds the latest version that satisfies the ^4.17.21 range. If the latest lodash is 4.18.0, it installs that. If it’s 5.0.0, it won’t install it because the caret (^) signifies compatibility with minor and patch versions, but not major.

The exact levers you control are the version range specifiers in your package.json.

  • 1.2.3: Exact version. Only 1.2.3 will be installed.
  • ~1.2.3: Tilde. Allows patch updates. Installs 1.2.3 up to, but not including, 1.3.0.
  • ^1.2.3: Caret. Allows minor and patch updates. Installs 1.2.3 up to, but not including, 2.0.0. This is the default for npm install <package> --save.
  • >1.2.3, <1.2.3, >=1.2.3, <=1.2.3: Standard comparison operators.
  • 1.2.3 - 2.3.4: Range.
  • * or x: Wildcard. Allows any version. Use with extreme caution.

When you publish your own package, you also declare its version according to SemVer rules. If you release 1.0.0 and later release 1.1.0, any package depending on your ^1.0.0 can safely upgrade to 1.1.0. If you release 2.0.0, packages depending on your ^1.0.0 will not automatically upgrade to 2.0.0 because that’s a major version bump, implying potential breaking changes.

The most surprising thing about SemVer is how often the "breaking change" rule is misunderstood or ignored, especially by developers of popular libraries. A package that increments its major version but makes no API changes is technically violating SemVer, but often does so to signal internal refactors or to prepare for future breaking changes. Conversely, a package that makes an API-incompatible change while only incrementing the minor or patch version is also violating SemVer, leading to the very dependency hell it’s designed to prevent.

Understanding the nuances of these range specifiers is key to maintaining stable, predictable, and up-to-date dependencies in your projects.

The next concept to grapple with is how npm handles conflicting version requirements across multiple dependencies.

Want structured learning?

Take the full Npm course →