npm 02 š Installing, Updating, and Versioning Node.js & npm
Node.js and npm are installed together, but they are versioned separately. Node.js ships with a specific npm version bundled into each releaseāNode.js 24.19.0 includes npm 11.17.0, for exampleāand npm releases new versions more frequently than Node.js does . This means the npm you have is not necessarily the latest npm, even if your Node.js is current. The official guidance is to use a Node version manager rather than a system installer, because the installer places npm in a directory with local permissions that cause errors when installing global packages . The version manager installs Node.js and npm in the user’s home directory, sidesteps the permission problem, and makes switching between versions trivial.
The versioning story is the other half of the topic. Node.js follows a release schedule that determines which versions are stable enough for production, and npm follows semantic versioning, which determines what a version range like ^4.18.0 actually means. A production application should run on an Active LTS or Maintenance LTS release, not on the Current line . The version range in package.json is a contract about which updates are acceptable, and understanding the caret and tilde is the difference between a predictable dependency tree and one that breaks on a Tuesday because a transitive dependency published a major version.
Key point: Use a Node version manager (nvm on macOS/Linux, nvm-windows on Windows) to install and switch Node.js versions. Node.js 27+ moves to one major release per year, with every release becoming LTS after six months of Current and thirty months of LTS support. npm install -g npm@latest updates npm independently of Node.js. The caret (^) allows minor and patch updates, the tilde (~) allows only patch updates, and exact versions pin the dependency.
Why the version manager is the recommended install method
The permission problem. A system installer puts Node.js and npm in a directory like /usr/local/lib/node_modules, which is owned by root. A global install (npm install -g) then tries to write to that directory as the unprivileged user, and it fails with EACCES . The workaround is sudo npm install -g, which is dangerous because the package’s install scripts run as root. The version manager avoids the problem entirely by installing into ~/.nvm/versions/node/<version>/, which the user owns.
The multi-version problem. Different projects require different Node.js versions. One application might be pinned to Node.js 18 because a dependency has not been updated, and another might require Node.js 22 for a newer API . A system installer installs exactly one version. The version manager installs as many as needed and switches between them per shell, per project, or per directory.
The reproducibility problem. A project can record its required Node.js version in a .nvmrc file. nvm use reads the file and switches to that version, and nvm install installs it if it is missing . This is the Node.js equivalent of the lockfile: the project declares its runtime, and the tooling enforces it.
a. Installing and switching Node.js with nvm
The installation is a script that appends initialization code to the shell profile. On macOS and Linux, the script is fetched from the nvm repository and run with bash . The command -v nvm check verifies that the shell has reloaded the profile and that nvm is available.
# Download and run the nvm install script
# The script writes to ~/.nvm and adds initialization to the shell profile
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
# Reload the shell profile so nvm is available
# Use ~/.zshrc for zsh, ~/.bashrc for bash
source ~/.bashrc
# Verify nvm is installed
nvm --version
# Install the latest LTS version of Node.js
nvm install --lts
# Install a specific version
nvm install 20.11.0
# List installed versions
nvm ls
# List available remote versions
nvm ls-remote
# Switch to a specific version
nvm use 20
# Set the default version for new shells
nvm alias default 20
The nvm install --lts command installs the latest Long-Term Support release. The nvm use command switches the current shell to the specified version by rewriting the node symlink in the nvm directory . The nvm alias default command sets the version that new shells start with, which is the one to set after installing the version the project should use by default.
The .nvmrc file is the per-project mechanism. Create it in the project root with the version number, and nvm use in that directory switches automatically.
# Write the required version to .nvmrc
echo "20.11.0" > .nvmrc
# In the project directory, switch to the version
nvm use
# Or install it if it is not present, then use it
nvm install
The nvm use command with no argument reads the .nvmrc in the current directory. The shell can be configured to run this automatically on directory change, which is the pattern for a project that must run on a specific version .
On Windows, nvm does not support the native shell. The nvm-windows package is the equivalent, and it is installed from its GitHub releases rather than the shell script .
b. Updating npm and the Node.js release schedule
npm is bundled with Node.js, but it can be updated independently. The command is npm install -g npm@latest, and it installs the latest npm into the Node.js version’s node_modules directory . The npm -v command verifies the result.
# Update npm to the latest version
# The -g flag installs it into the active Node.js version's global directory
npm install -g npm@latest
# Verify the version
npm -v
The update does not affect the Node.js version. It replaces the npm that ships with that Node.js version with the latest npm, and the next nvm use to a different Node.js version reverts to that version’s bundled npm until it is updated separately. The nvm install node --reinstall-packages-from=node command is the nvm feature that installs a new Node.js version and copies the global packages from the previous version in one step .
The Node.js release schedule determines which versions are safe for production. Until Node.js 26, odd-numbered releases were Current for six months and then unsupported, and even-numbered releases moved to Active LTS . Starting with Node.js 27, the schedule changes: one major release per year in April, an LTS promotion in October, and every release becomes LTS . The LTS phase lasts thirty months, and production applications should use only Active LTS or Maintenance LTS releases . The Alpha channel replaces the early-testing role that odd-numbered releases served, and it allows semver-major changes during the Alpha phase .
| Phase | Duration | Meaning |
|---|---|---|
| Alpha | 6 months | Early testing, breaking changes allowed |
| Current | 6 months | Stabilization, not for production |
| LTS | 30 months | Production-ready, critical fixes |
| EOL | ā | No support |
c. Semantic versioning and the range syntax
npm uses semantic versioning, which divides version numbers into three parts: major, minor, and patch. The major number changes when a release breaks backward compatibility, the minor number changes when a release adds a backward-compatible feature, and the patch number changes when a release fixes a bug without changing the API . The version range in package.json declares which of those changes the project accepts.
The caret (^) is the default when npm install --save is used. It allows minor and patch updates but not major updates. ^1.2.3 matches 1.2.3 and every version up to but not including 2.0.0 . The one exception is the 0.x range: ^0.1.2 matches 0.1.2 and every version up to but not including 0.2.0, because the 0.x major number is treated as unstable .
The tilde (~) is more restrictive. It allows patch updates but not minor updates. ~1.2.3 matches 1.2.3 and every version up to but not including 1.3.0 .
// The version ranges in package.json
{
"dependencies": {
// Caret: minor and patch updates allowed
"express": "^4.18.0",
// Tilde: patch updates only
"lodash": "~4.17.21",
// Exact: no updates allowed
"react": "18.2.0",
// Wildcard: any version
"some-lib": "*"
}
}
The exact version has no prefix. "react": "18.2.0" installs exactly that version and nothing else, which is the most predictable but requires manual updates. The wildcard * accepts any version, which is the least predictable and should be avoided in application code.
The lockfile is the other half of the versioning story. package-lock.json records the exact version and resolved location of every package in the tree, and npm install uses it to recreate the identical tree. The ranges in package.json are the constraints; the lockfile is the resolution. npm ci installs exactly what the lockfile specifies, fails if the two disagree, and is the command for CI pipelines .
Complete Example Session
The following session installs nvm, installs two Node.js versions, switches between them, updates npm, and demonstrates the range syntax.
# ============================================
# PART 1: INSTALL NVM
# ============================================
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
source ~/.bashrc
nvm --version
# ============================================
# PART 2: INSTALL NODE.JS VERSIONS
# ============================================
# Install the latest LTS version
nvm install --lts
# Install a specific older version
nvm install 18.20.0
# List installed versions
nvm ls
# ============================================
# PART 3: SWITCH BETWEEN VERSIONS
# ============================================
# Switch to Node.js 18
nvm use 18.20.0
node -v
# Output: v18.20.0
# Switch back to the LTS version
nvm use --lts
node -v
# Output: v22.x.x (or whatever the LTS is)
# Set the default for new shells
nvm alias default 22
# ============================================
# PART 4: UPDATE NPM
# ============================================
npm install -g npm@latest
npm -v
# ============================================
# PART 5: VERSION RANGES
# ============================================
# Create a package.json with different ranges
cat > package.json << 'EOF'
{
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.21",
"react": "18.2.0"
}
}
EOF
# Install the dependencies
npm install
# Check the resolved versions in the lockfile
grep -A2 '"express"' package-lock.json | head -5
The five parts cover the nvm installation, the two Node.js versions, the switch, the npm update, and the version range demonstration. What the session shows is that the version manager makes the Node.js version a per-shell choice, and the range syntax determines what npm install will accept.
Quick Reference
Version Managers
| Tool | Platform | Install |
|---|---|---|
nvm | macOS, Linux, WSL | Shell script from GitHub |
nvm-windows | Windows | GitHub releases |
fnm | Cross-platform | Package manager |
nvm Commands
| Command | Effect |
|---|---|
nvm install --lts | Install latest LTS |
nvm install 20 | Install major version 20 |
nvm use 20 | Switch to 20 |
nvm alias default 20 | Set default |
nvm ls | List installed |
nvm ls-remote | List available |
npm Update
| Command | Effect |
|---|---|
npm install -g npm@latest | Latest npm |
npm install -g npm@10 | Specific major |
npm -v | Verify |
Version Ranges
| Range | Matches |
|---|---|
^1.2.3 | >=1.2.3 <2.0.0 |
~1.2.3 | >=1.2.3 <1.3.0 |
1.2.3 | Exactly 1.2.3 |
* | Any version |
Best Practices
ā Do This:
# Use nvm, not the system installer
nvm install --lts
# Pin the project version in .nvmrc
echo "20.11.0" > .nvmrc
# Use the caret for application dependencies
"express": "^4.18.0"
# Use npm ci in CI
npm ci
ā Don’t Do This:
# Use sudo with npm install -g
sudo npm install -g some-package # root-owned files
# Use * for application dependencies
"express": "*" # unpredictable
# Ignore the lockfile
rm package-lock.json # breaks reproducibility
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
EACCES on global install | System installer put npm in root-owned dir | Use nvm |
| Wrong Node.js version | Forgot nvm use | .nvmrc + nvm use |
| npm version reverts | Switched Node.js version | Update npm per version |
| Range resolves to unexpected version | Caret semantics | Pin exact or use ~ |
| CI install differs | No npm ci | Use npm ci |
Real-World Examples
1. Install LTS
nvm install --lts
2. Switch version
nvm use 20
3. Pin with .nvmrc
echo "20.11.0" > .nvmrc
4. Update npm
npm install -g npm@latest
5. Caret range
"express": "^4.18.0"
6. Tilde range
"lodash": "~4.17.21"
7. Exact version
"react": "18.2.0"
8. Reinstall global packages
nvm install node --reinstall-packages-from=node
9. CI install
npm ci
10. Check versions
node -v && npm -v
Visual
nvm Version Switching
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ~/.nvm/versions/node/ ā
ā āāā v18.20.0/ ā
ā āāā v20.11.0/ ā
ā āāā v22.x.x/ ā
ā ā ā
ā ā¼ ā
ā nvm use 20 ā
ā ā symlink node ā v20.11.0/bin/node ā
ā ā npm ā v20.11.0/bin/npm ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Release Schedule
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Node.js 27+ ā
ā Alpha: OctāMar (6 months) ā
ā Current: AprāOct (6 months) ā
ā LTS: Oct + 30 months ā
ā EOL: no support ā
ā ā
ā Production: Active LTS or Maintenance LTS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Version Range
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ^1.2.3 ā >=1.2.3 <2.0.0 ā
ā ~1.2.3 ā >=1.2.3 <1.3.0 ā
ā 1.2.3 ā exactly 1.2.3 ā
ā * ā any version ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Summary
| Item | Value |
|---|---|
| Recommended installer | Node version manager |
| macOS/Linux manager | nvm |
| Windows manager | nvm-windows |
| Default version file | .nvmrc |
| npm update | npm install -g npm@latest |
| Caret range | ^1.2.3 ā minor + patch |
| Tilde range | ~1.2.3 ā patch only |
| LTS duration | 30 months |
| Production versions | Active LTS or Maintenance LTS |
| CI install | npm ci |
Key takeaways:
- Use a version manager, not a system installer. The installer puts npm in a root-owned directory, and global installs fail with
EACCES. The version manager installs into the user’s home directory and makes switching versions trivial . - Node.js 27+ moves to one major release per year. Every release becomes LTS after six months of Current and thirty months of LTS support. Production should use Active LTS or Maintenance LTS .
- npm is updated independently of Node.js.
npm install -g npm@latestinstalls the latest npm into the active Node.js version’s directory . - The caret allows minor and patch updates; the tilde allows only patch updates. The caret is the default for
npm install --save, and the0.xrange has a special behavior where the caret only allows patch updates . - The lockfile is the resolution of the ranges.
package.jsondeclares the constraints,package-lock.jsonrecords the exact tree, andnpm ciinstalls the lockfile exactly .
Remember: The Node.js and npm versions are two separate decisions. The Node.js version is the runtime, and it is chosen by the version manager and pinned in .nvmrc. The npm version is the package manager, and it can be updated independently. The version ranges in package.json declare which package updates the project accepts, and the lockfile records the exact versions that were installed. Use nvm, pin the runtime, update npm deliberately, and let the lockfile handle the dependency tree.
Stop using slow, ad-bloated tool sites! š¤®
š Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
ā
Finance (Mortgage, Interest, Inflation)
ā
Tech (Base64, JSON, Dev Suite, IP)
ā
Health (BMI, BMR, TDEE)
ā
Productivity (Timer, Workspace, QR)
ā”ļø Fast & Private
š No data leaves your device
š 100% Free
š Use it now: https://tools.kandz.me
š Bookmark itāyouāll need it later!