Semantic Versioning and Dependencies
When you install a package, you are depending on a specific version of someone else’s code.
Understanding Semantic Versioning
Most packages follow semantic versioning (often called semver), which uses the format MAJOR.MINOR.PATCH:
- MAJOR – incremented for breaking changes (incompatible API changes)
- MINOR – incremented for new features that are backwards compatible
- PATCH – incremented for backwards-compatible bug fixes
For example, a version bump from 2.3.1 to 2.4.0 means new features are added but nothing should break. A jump from 2.4.0 to 3.0.0 means something changes in a way that could break your code.
Version Ranges in package.json
When you install a package, npm records it with a version range rather than a fixed version. This range controls what updates are allowed:
| Symbol | Meaning | Example | Matches |
|---|---|---|---|
| (none) | Exact version only | 1.2.3 |
Only 1.2.3 |
^ |
Compatible with version | ^1.2.3 |
>=1.2.3 and <2.0.0 |
~ |
Approximately equivalent | ~1.2.3 |
>=1.2.3 and <1.3.0 |
> |
Greater than | >1.2.3 |
Any version above 1.2.3 |
>= |
Greater than or equal | >=1.2.3 |
1.2.3 or higher |
* |
Any version | * |
Anything at all |
The ^ (caret) is the default when you run npm install. It allows minor and patch updates but blocks major version bumps, so you get bug fixes without breaking changes.
The ~ (tilde) is more conservative. It only allows patch-level updates.
Lock Files and Reproducible Builds
The problem is that version ranges like ^1.2.3 can resolve to different actual versions depending on when you run npm install. Your machine might get 1.2.3 today, and a teammate might get 1.2.7 next week.
That is what package-lock.json is for. It records the exact version of every package (and every transitive dependency) that was installed. When someone else runs npm install, npm reads the lock file and installs those exact versions.
Always commit your lock file to version control. It ensures everyone on the team – and your CI/CD pipeline – gets identical dependencies.
Common package.json Fields
Beyond dependencies and scripts, package.json has several other useful fields:
{
"name": "my-package",
"version": "1.0.0",
"description": "A brief description of the project",
"main": "dist/index.cjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"types": "dist/index.d.ts",
"files": ["dist"],
"engines": {
"node": ">=18.0.0"
},
"keywords": ["utility"],
"author": "Your Name",
"license": "MIT"
}
main– legacy/default CommonJS entry point (used byrequire())exports– modern, explicit package entry points (supportsimport/requireconditions)types– points to TypeScript type declarationsfiles– which files to include when publishing the packageengines– specifies required Node.js version
Running Packages with npx
Sometimes you want to run a CLI tool without installing it globally. That is what npx (an alias for npm exec) is for – it runs a local binary if available, or fetches one to npm’s cache and executes it:
# Scaffold a new Vite app
npx create-vite@latest my-app
# Run a specific version of a tool
npx eslint@latest --init
# Run a locally installed binary
npx jest
npx is useful for one-off tools like project scaffolding or migration utilities.