Managing Multiple Node Versions with nvm
Install nvm, pin a Node version per project with .nvmrc, and stop fighting global-install conflicts—all from the terminal.
What you'll build / learn
By the end of this tutorial you'll have nvm (Node Version Manager) installed, you'll switch between Node.js versions with a single command, pin a per-project version using a .nvmrc file, and understand why globally installed packages cause headaches—and how to avoid them.
Prerequisites
- macOS or Linux. nvm is a shell script and does not run on native Windows. On Windows, either use nvm-windows (a separate project with different commands) or run nvm inside WSL2.
- A bash or zsh shell. On modern macOS the default is zsh; most Linux distros default to bash. Check with
echo $SHELL. curlinstalled (preinstalled on macOS and most Linux).- No existing Node.js installed via a system package manager (Homebrew, apt, the official
.pkg). Mixing those with nvm causes confusingPATHconflicts. If you have one, uninstall it first.
Step 1: Install nvm
nvm is distributed as an install script. Run the official installer (this does not require sudo):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
If you prefer to read the script before running it (a good habit), open the URL in your browser first, or download it and inspect it:
curl -o nvm-install.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh
less nvm-install.sh
bash nvm-install.sh
The installer clones nvm into ~/.nvm and appends a few lines to your shell profile (~/.zshrc, ~/.bashrc, or ~/.bash_profile) that load nvm in new shells.
Step 2: Activate nvm in your current shell
The install script only edits your profile; it doesn't reload it. Either open a new terminal window, or run:
source ~/.zshrc # if you use zsh
# or
source ~/.bashrc # if you use bash
Confirm nvm is available:
command -v nvm
Expected output: nvm. Note that nvm is a shell function, not a binary, so which nvm may return nothing—use command -v nvm instead.
Step 3: Install Node.js versions
Install the latest Long Term Support (LTS) release—the safe default for most projects:
nvm install --lts
Install a specific major version (nvm picks the newest matching release):
nvm install 20
List what you have installed locally:
nvm ls
List everything available to download:
nvm ls-remote
Step 4: Switch versions and set a default
Switch the current shell to a version:
nvm use 20
node --version # -> v20.x.x
nvm use only affects the terminal session you run it in. To choose which version new terminals start with, set a default alias:
nvm alias default 20
Now every new shell will start on Node 20.
Step 5: Pin a version per project with .nvmrc
Instead of remembering which version each project needs, commit a .nvmrc file in the project root. From inside your project folder:
cd ~/code/my-app
node -v > .nvmrc
That writes the exact version (e.g. v20.11.1). For a more flexible file you can pin just the major line—edit .nvmrc to contain a single line:
20
Now anyone (including you on a new machine) can match the project's Node version:
nvm use # reads .nvmrc, switches to that version
nvm install # reads .nvmrc, installs it if missing
Commit .nvmrc to git so the whole team stays in sync:
git add .nvmrc
git commit -m "Pin Node version with .nvmrc"
Optional: auto-switch when you cd
nvm doesn't auto-switch by default. The official README provides a shell hook for zsh and bash that runs nvm use automatically when you enter a directory containing .nvmrc. See the "Deeper Shell Integration" section of the nvm README for the exact snippet to add to your profile.
Step 6: Avoid global-install headaches
Here's the trap: each Node version installed by nvm has its own separate set of global packages. Install a CLI globally under Node 20, switch to Node 18, and that command vanishes from your PATH.
Best practices that sidestep this entirely:
| Instead of… | Do this | Why |
|---|---|---|
npm install -g some-cli |
npx some-cli |
Runs a one-off without installing globally |
| Relying on a global tool in a project | Add it to devDependencies and run via npm scripts |
Version is pinned and reproducible per project |
| Reinstalling globals after each switch | nvm install 22 --reinstall-packages-from=20 |
Copies globals from one version to another during install |
For a project dependency, prefer:
npm install --save-dev eslint
npx eslint .
This keeps tools tied to the project and its Node version, so switching versions never breaks your workflow.
Verify it works
Run this sequence to confirm everything is wired up:
nvm --version # prints e.g. 0.40.1
nvm ls # lists installed versions, '->' marks the active one
node --version # current Node version
nvm install 18 # add another version
nvm use 18 && node -v # -> v18.x.x
nvm use 20 && node -v # -> v20.x.x
If each node -v reflects the version you just selected, nvm is working correctly.
Troubleshooting
nvm: command not found in a new terminal. Your profile isn't loading nvm. Confirm the loader block exists in ~/.zshrc or ~/.bashrc:
grep -n NVM_DIR ~/.zshrc ~/.bashrc 2>/dev/null
If it's missing, reinstall (Step 1) or add it manually. If it's present, run source ~/.zshrc.
node still points to an old Homebrew/system version. Run command -v node. If the path isn't under ~/.nvm/, a system install is shadowing nvm. Uninstall it (brew uninstall node) and reopen your terminal.
nvm use says "No .nvmrc file found." You're not in the project directory, or the file is missing/misnamed. Check with cat .nvmrc and ensure it's named exactly .nvmrc in the current folder.
A global CLI disappeared after switching versions. That's expected behavior—globals are per-version. Reinstall it under the active version, or better, run it with npx or add it as a project devDependency.
Next steps
- Read the nvm README for shell auto-switching and CI usage.
- Explore Corepack to pin
npm,pnpm, oryarnversions per project alongside Node. - For teams that want language-agnostic version pinning, look at asdf or mise as alternatives to nvm.
Discussion 0
Join the discussion
Sign in with GitHub to comment and vote.
No comments yet
Be the first to weigh in.