Node.js Installation#
What it is#
Node.js is a cross-platform JavaScript runtime built on Chrome’s V8 engine. Installing it gives you the node binary for running scripts and the npm package manager. Most developers use a version manager (nvm, Volta, or fnm) rather than a system-level install so they can switch Node versions per project.
Install#
Windows — winget#
winget install --id OpenJS.NodeJS.LTS
Output:
Found Node.js (LTS) [OpenJS.NodeJS.LTS] Version 22.14.0
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it license, third-party packages.
Downloading https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi
Successfully installed
macOS — Homebrew#
brew install node
Output:
==> Downloading https://ghcr.io/v2/homebrew/core/node/manifests/22.14.0
==> Installing node
==> Summary
🍺 /usr/local/Cellar/node/22.14.0: 2,412 files, 71.5MB
Ubuntu / Debian — apt (via NodeSource)#
# Add the NodeSource v22 repository
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
Output:
Selecting previously unselected package nodejs.
Setting up nodejs (22.14.0-1nodesource1) ...
nvm — Node Version Manager (recommended cross-platform approach)#
nvm is the most widely used approach for managing multiple Node versions. It installs Node entirely in your home directory — no sudo required.
Install nvm#
# macOS / Linux
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
Restart your shell (or source ~/.bashrc / source ~/.zshrc), then verify:
nvm --version
Output:
0.39.7
Install and use a Node version#
# Install the latest LTS release
nvm install --lts
Output:
Downloading and installing node v22.14.0...
Downloading https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-x64.tar.xz...
Now using node v22.14.0 (npm v10.9.2)
Creating default alias: default -> lts/* (-> v22.14.0)
# Install a specific version
nvm install 20.18.0
# Switch to a version
nvm use 20.18.0
# Switch back to latest LTS
nvm use --lts
# Set the default version for new shells
nvm alias default 22
# List installed versions
nvm ls
Output (nvm ls):
-> v22.14.0
v20.18.0
default -> 22 (-> v22.14.0)
lts/* -> lts/jod (-> v22.14.0)
.nvmrc — pin a version per project#
# Create a .nvmrc file in your project root
echo "22" > .nvmrc
# Any developer with nvm can then run:
nvm use
# nvm reads .nvmrc and switches automatically
Output:
Found '/home/user/myproject/.nvmrc' with version <22>
Now using node v22.14.0 (npm v10.9.2)
Volta — toolchain manager#
Volta pins Node versions per project in package.json and automatically switches when you cd into a directory. It works on Windows without WSL.
# Install Volta (macOS / Linux)
curl https://get.volta.sh | bash
# Install Volta (Windows — run in PowerShell as admin)
winget install Volta.Volta
# Install Node LTS and make it the default
volta install node
# Install a specific version
volta install node@20
# Pin a version to the current project (writes to package.json)
volta pin node@22
Output (volta install node):
success: installed and set node@22.14.0 (with npm@10.9.2) as default
Output (volta pin node@22):
success: pinned node@22.14.0 (with npm@10.9.2) in package.json
The package.json entry Volta adds:
{
"volta": {
"node": "22.14.0",
"npm": "10.9.2"
}
}
fnm — Fast Node Manager#
fnm is written in Rust and is significantly faster than nvm at shell startup time. It also reads .nvmrc and .node-version files.
# macOS
brew install fnm
# Linux (curl installer)
curl -fsSL https://fnm.vercel.app/install | bash
# Windows (winget)
winget install Schniz.fnm
Add the shell hook (add to ~/.bashrc, ~/.zshrc, or ~/.config/fish/config.fish):
# bash / zsh
eval "$(fnm env --use-on-cd)"
# fish
fnm env --use-on-cd | source
# Install and use the latest LTS
fnm install --lts
fnm use lts-latest
# Install a specific version
fnm install 20
fnm use 20
# List installed versions
fnm list
Output (fnm install --lts):
Installing Node v22.14.0 (x64)
Output (fnm list):
* v22.14.0 default (lts-latest)
v20.18.0
Verify the installation#
node --version
Output:
v22.14.0
npm --version
Output:
10.9.2
LTS vs Current — when to use each#
| Channel | Example | Stability | Who should use it |
|---|---|---|---|
| LTS (Long-Term Support) | v22.x (Active), v20.x (Maintenance) | High — bug + security fixes for 3 years | Production apps, team projects, CI |
| Current | v23.x | New features land here first; breaks possible | Trying new Node APIs, library authors |
[!TIP] Use LTS for anything that runs in production. LTS versions have even major version numbers (18, 20, 22). Odd numbers (19, 21, 23) are Current releases and have a shorter support window.
Version manager comparison#
| Feature | nvm | Volta | fnm |
|---|---|---|---|
| Shell startup speed | Slow (bash function) | Fast (binary shim) | Fast (Rust binary) |
| Windows support | WSL only | Yes (native) | Yes (native) |
| Per-project pin | .nvmrc | package.json volta field | .nvmrc / .node-version |
Reads .nvmrc | Yes | Yes | Yes |
| Also manages npm/yarn/pnpm | No | Yes | No |
| Install without root | Yes | Yes | Yes |
| Ecosystem maturity | Very mature | Mature | Growing |
[!TIP] Pick nvm if you’re on macOS/Linux and want maximum compatibility. Pick Volta if you’re on Windows or want
package.json-level pinning shared with the whole team. Pick fnm if shell startup time matters (e.g., you open many terminals).
n — minimal version manager#
n is the smallest of the Node version managers — a single bash script with no shell hooks. It installs Node directly into the system Node prefix (default /usr/local), which is the cleanest layout but requires sudo unless you re-point the prefix. Reach for n when you want zero magic and no shell-startup overhead, on a machine where you’re the only Node user.
# Install n itself (one-off bootstrap with curl)
curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o /usr/local/bin/n
chmod +x /usr/local/bin/n
# Or via npm if you already have Node
npm install -g n
Output: (none — exits 0 on success)
# Install the latest LTS
n lts
# Install a specific version
n 22.14.0
# Install latest current release
n latest
# Switch interactively (arrow keys)
n
# Remove a version
n rm 20.18.0
Output (n lts):
installing : node-v22.14.0
mkdir : /usr/local/n/versions/node/22.14.0
fetch : https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-x64.tar.xz
installed : v22.14.0 (with npm 10.9.2)
To avoid sudo, re-point the prefix at a user-owned directory:
# Add to ~/.bashrc or ~/.zshrc
export N_PREFIX="$HOME/.n"
export PATH="$N_PREFIX/bin:$PATH"
echo $N_PREFIX
Output:
/home/alice/.n
asdf — multi-language version manager#
asdf manages versions for many runtimes (Node, Python, Ruby, Go, Terraform…) through a plugin system. One tool, one config file (.tool-versions), one PATH manipulation. Reach for asdf when you switch between multiple languages and want one cohesive setup; the trade-off is slower shell startup than fnm/mise.
# Install asdf via git
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
# Add to ~/.bashrc / ~/.zshrc
. "$HOME/.asdf/asdf.sh"
. "$HOME/.asdf/completions/asdf.bash"
Restart your shell, then install the Node plugin:
asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
# Install a Node version
asdf install nodejs latest
asdf install nodejs 22.14.0
# Set the global default
asdf global nodejs 22.14.0
# Set a project-local version (writes .tool-versions)
asdf local nodejs 20.18.0
# List installed
asdf list nodejs
Output (asdf list nodejs):
20.18.0
*22.14.0
The .tool-versions file is multi-runtime:
nodejs 22.14.0
python 3.12.7
ruby 3.3.5
terraform 1.9.5
mise — modern asdf-compatible manager#
mise (formerly rtx) is a Rust-based, asdf-compatible runtime manager. It reads the same .tool-versions files but has a much faster startup, more features (env management, tasks), and a richer plugin ecosystem. Pick mise over asdf for new projects.
# macOS — Homebrew
brew install mise
# Linux / WSL — curl installer
curl https://mise.run | sh
# Windows — winget
winget install jdx.mise
Activate in your shell (one-time):
# bash / zsh — add to rc file
eval "$(mise activate bash)" # or zsh
# fish
mise activate fish | source
Use it identically to asdf:
mise use --global node@22
mise use node@20.18.0 # project-local (writes .mise.toml or .tool-versions)
mise install # install everything in .mise.toml
mise list # show installed versions
mise current # show what's active in the current dir
Output (mise current):
node@22.14.0
python@3.12.7
mise’s project config:
# .mise.toml
[tools]
node = "22"
python = "3.12"
[env]
NODE_ENV = "development"
DATABASE_URL = "postgres://localhost/dev"
[tasks.dev]
run = "npm run dev"
mise run dev
Output: (none — exits 0 on success)
Manual install (no version manager)#
Sometimes you want a single Node version, system-installed, with no manager overhead. The official downloads are the simplest path — pre-compiled binaries straight from nodejs.org.
# Linux x64 — download + extract to /usr/local
VER=v22.14.0
curl -fsSL "https://nodejs.org/dist/$VER/node-$VER-linux-x64.tar.xz" \
| sudo tar -xJ -C /usr/local --strip-components=1
Output: (none — exits 0 on success)
which node
node --version
Output:
/usr/local/bin/node
v22.14.0
# Windows — MSI installer
winget install OpenJS.NodeJS.LTS
# Or download the .msi from https://nodejs.org and double-click
Output: (none — exits 0 on success)
# macOS — pkg installer (preferred for "system" install)
curl -O "https://nodejs.org/dist/v22.14.0/node-v22.14.0.pkg"
sudo installer -pkg node-v22.14.0.pkg -target /
Output: (none — exits 0 on success)
Linux distribution packages#
Distro packages (apt, dnf, pacman) are convenient but ship older Node versions than nodejs.org. Use them only when you must — for example on a server where you can’t run network installers.
Debian / Ubuntu#
# Default repos (often old — e.g. Ubuntu 24.04 ships Node 18)
sudo apt-get update
sudo apt-get install -y nodejs npm
# NodeSource (current LTS)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
Output:
Setting up nodejs (22.14.0-1nodesource1) ...
Fedora / RHEL / Rocky#
# Default dnf module (older Node)
sudo dnf module install nodejs:22/common
# NodeSource (current LTS)
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install -y nodejs
Output: (none — exits 0 on success)
Arch / Manjaro#
sudo pacman -S nodejs npm
Output: (none — exits 0 on success)
Alpine (for Docker base images)#
apk add --no-cache nodejs npm
Output: (none — exits 0 on success)
Path layout reference#
Where Node lives after each install method — useful when diagnosing PATH conflicts or “wrong version” issues.
| Install method | Node binary | npm prefix | Global modules |
|---|---|---|---|
| Official .pkg (macOS) | /usr/local/bin/node | /usr/local | /usr/local/lib/node_modules/ |
| Homebrew (macOS Apple Silicon) | /opt/homebrew/bin/node | /opt/homebrew | /opt/homebrew/lib/node_modules/ |
| Homebrew (macOS Intel) | /usr/local/bin/node | /usr/local | /usr/local/lib/node_modules/ |
| winget (Windows) | C:\Program Files\nodejs\node.exe | C:\Program Files\nodejs | %APPDATA%\npm\node_modules\ |
| apt / dnf (Linux) | /usr/bin/node | /usr | /usr/lib/node_modules/ |
| nvm | ~/.nvm/versions/node/v<X>/bin/node | ~/.nvm/versions/node/v<X> | ~/.nvm/versions/node/v<X>/lib/node_modules/ |
| fnm | ~/.local/share/fnm/node-versions/v<X>/installation/bin/node | (same prefix) | (same prefix)/lib/node_modules/ |
| Volta | ~/.volta/tools/image/node/<X>/bin/node | (managed by Volta) | (managed by Volta) |
| asdf / mise | ~/.asdf/installs/nodejs/<X>/bin/node | (same prefix) | (same prefix)/lib/node_modules/ |
Inspect what’s currently on PATH:
which -a node
Output:
/home/alice/.nvm/versions/node/v22.14.0/bin/node
/usr/bin/node
The first entry wins. If the wrong version runs, it’s a PATH ordering bug.
Switching package manager via Corepack#
Corepack ships with Node 16.9+. It activates the package manager declared in your package.json packageManager field, so every contributor uses the exact same version.
# One-time enable (idempotent)
corepack enable
# Prepare a specific version
corepack prepare pnpm@9.12.1 --activate
corepack prepare yarn@4.5.1 --activate
corepack prepare npm@10.9.2 --activate
Output: (none — exits 0 on success)
{
"packageManager": "pnpm@9.12.1"
}
Now pnpm install in this repo always uses pnpm 9.12.1 — even if the global pnpm is something else.
pnpm --version
Output:
9.12.1
See pnpm, yarn, and npm for the per-tool deep-dives.
Installing Bun (alternative runtime)#
Bun is a Node-compatible JavaScript runtime, package manager, bundler, and test runner — all in one Zig-written binary. Useful as a faster Node replacement for new projects. Coexists with Node; pick per-project via shebang or per-command.
# Linux / macOS / WSL — official installer
curl -fsSL https://bun.sh/install | bash
# macOS — Homebrew
brew install oven-sh/bun/bun
# Windows — PowerShell
powershell -c "irm bun.sh/install.ps1 | iex"
# Verify
bun --version
Output:
1.2.14
See bun for the full deep-dive.
Installing Deno (secure runtime)#
Deno is a secure-by-default JavaScript and TypeScript runtime by the original creator of Node. Sandboxes scripts behind explicit permission flags. Useful for one-off scripts and security-sensitive workloads.
# Linux / macOS / WSL
curl -fsSL https://deno.land/install.sh | sh
# macOS — Homebrew
brew install deno
# Windows — PowerShell
powershell -c "irm https://deno.land/install.ps1 | iex"
# Cargo (build from source)
cargo install deno --locked
# Verify
deno --version
Output:
deno 2.1.4 (stable, release, x86_64-unknown-linux-gnu)
v8 13.0.245.12
typescript 5.6.2
See deno for the full deep-dive.
Choosing between Node, Bun, and Deno#
A quick decision table for picking a runtime on a new project.
| Project shape | Best pick | Why |
|---|---|---|
| Production server, big ecosystem | Node 22 LTS | Most stable; best library support |
| Greenfield service, max speed | Bun | Faster install + runtime; Node-compat |
One-off script with fetch() | Deno | Zero setup; permission-sandboxed |
| TypeScript-heavy library | Deno (publish to JSR) | Native TS; no build step |
| AWS Lambda / Vercel / Cloudflare | Node 22 or Workerd | Best platform support |
| Electron app | Node | Required by Electron |
| Internal tooling, modern team | Bun | Speed > compat trade-off acceptable |
See bun and deno for the per-runtime details.
Verify the full toolchain#
After installing, sanity-check every tool you’ll use.
node --version
npm --version
npx --version
corepack --version
Output:
v22.14.0
10.9.2
10.9.2
0.31.0
Confirm node_modules/.bin resolution works:
mkdir /tmp/probe && cd /tmp/probe
npm init -y >/dev/null
npm install --save-dev typescript
npx tsc --version
Output:
Version 5.5.4
Uninstalling Node#
When switching from a system Node to a version-managed setup, remove the old install first. Mixing the two leads to “wrong version” bugs that are hard to diagnose.
Windows#
winget uninstall OpenJS.NodeJS.LTS
# Then delete C:\Users\<You>\AppData\Roaming\npm (global modules)
Output: (none — exits 0 on success)
macOS — Homebrew#
brew uninstall node
brew uninstall --ignore-dependencies node
rm -rf /usr/local/lib/node_modules /usr/local/include/node
Output: (none — exits 0 on success)
macOS — official .pkg#
sudo rm /usr/local/bin/node /usr/local/bin/npm /usr/local/bin/npx
sudo rm -rf /usr/local/lib/node_modules /usr/local/include/node
sudo rm /etc/paths.d/40-node
Output: (none — exits 0 on success)
Linux — apt#
sudo apt-get remove --purge nodejs npm
sudo apt-get autoremove
sudo rm -rf /usr/lib/node_modules
Output: (none — exits 0 on success)
Common pitfalls#
- EACCES on
npm install -g— you’re using a system Node and don’t have write access to/usr/lib/node_modules. Switch to nvm/fnm/Volta or setprefix=~/.npm-globalin~/.npmrcand add~/.npm-global/binto PATH. Neversudo npm install -g. nvm: command not foundafter install — the install script edits your shell rc file, but the current shell hasn’t sourced it yet. Restart the terminal orsource ~/.bashrc.- Wrong Node version in
cron/systemd— these don’t read your shell rc. Use a full path:/home/alice/.nvm/versions/node/v22.14.0/bin/node. .nvmrcignored — nvm only switches on demand (nvm use). Addcdhooks to your shell rc to switch automatically, or use fnm/Volta which auto-switch.- Multiple Node installs fighting on PATH —
which -a nodeshows them all. Remove duplicates from~/.bashrc,~/.profile,/etc/paths.d/, and shell completion scripts. - WSL Node slower than Windows Node — running
nodein/mnt/c/...is slow because of the 9P filesystem. Keep your project under the Linux home (/home/...) when working in WSL. corepack enablefailing on shared CI — Corepack writes shims to the Node bin directory. On hosted CI with read-only Node, install pnpm/yarn explicitly:npm install -g pnpm@9.- Stale
nvminstall — oldnvmversions don’t know about newer LTS lines. Update withnvm install-latest-npmand refresh nvm itself by re-running the install script. - Volta and nvm both installed — Volta’s shim and nvm’s PATH manipulation collide. Pick one; uninstall the other.
fnm: command not foundin new shell — you added the eval to~/.bashrc, but your terminal opens login shells, which read~/.profile. Addfnm env --use-on-cdto both orsource ~/.bashrcfrom~/.profile.
Real-world recipes#
Per-project Node version with auto-switch#
The recommended setup: a .nvmrc (or .node-version) at the repo root + a shell hook that switches when you cd in. Below uses fnm’s built-in --use-on-cd.
# Repo root
echo "22.14.0" > .nvmrc
git add .nvmrc
Output: (none — exits 0 on success)
# Shell rc (one-time)
eval "$(fnm env --use-on-cd)"
cd ~/projects/my-app # fnm auto-switches to 22.14.0
node --version
Output:
Using Node v22.14.0
v22.14.0
CI matrix across Node LTS versions#
Test against every supported LTS in parallel. Pin minor versions to keep the matrix stable.
# .github/workflows/test.yml
jobs:
test:
strategy:
matrix:
node: [20.18, 22.14, 24.0]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
node --version
Output:
v22.14.0
Dockerfile with pinned Node + pnpm#
Multi-stage build: deps in a cached layer, app in a thin runtime image.
# syntax=docker/dockerfile:1
FROM node:22.14-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.12.1 --activate
WORKDIR /app
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod
FROM base AS runner
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["node", "dist/server.js"]
docker build -t my-app .
docker run --rm my-app
Output:
Server listening on http://0.0.0.0:3000
Switch between Node 22 and Bun on the same project#
Per-shebang selection — Bun for hot-path scripts, Node for the canonical runtime.
# bun-only entry (note the shebang)
#!/usr/bin/env bun
console.log(Bun.version);
chmod +x ./script.ts
./script.ts
node script.ts # uses Node (errors on Bun-only APIs)
bun script.ts # uses Bun
Output:
1.2.14
See bun for what’s safe to call.
Bootstrap a clean Mac dev machine#
A condensed checklist a teammate can run after a fresh macOS install.
# Xcode CLT (compilers, git)
xcode-select --install
# Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Node via fnm + Corepack-managed pnpm
brew install fnm
echo 'eval "$(fnm env --use-on-cd)"' >> ~/.zshrc
exec zsh
fnm install --lts
corepack enable
corepack prepare pnpm@latest --activate
# Optional: Bun and Deno
brew install oven-sh/bun/bun deno
node --version && pnpm --version && bun --version && deno --version | head -1
Output:
v22.14.0
9.12.1
1.2.14
deno 2.1.4 (stable, release, aarch64-apple-darwin)
Pin everything for a team#
engines + packageManager + .nvmrc covers every developer regardless of which version manager they use.
{
"engines": {
"node": ">=22.14 <23",
"pnpm": ">=9.12"
},
"packageManager": "pnpm@9.12.1"
}
22.14.0
# After cloning, contributors run:
fnm use # or nvm use, or asdf install, or mise install
corepack enable
pnpm install
Output: (none — exits 0 on success)
Roll back a Node upgrade#
A new Node release broke your project. Switch back without reinstalling everything.
fnm install 22.10.0
fnm use 22.10.0
npm ci
Output:
Installed Node v22.10.0
Using Node v22.10.0
added 247 packages in 4s
Pin in .nvmrc so the rollback is shared with the team:
echo "22.10.0" > .nvmrc
git add .nvmrc && git commit -m "pin: roll node back to 22.10.0"
Output: (none — exits 0 on success)