The modern Windows JavaScript stack is fast, snappy, and entirely scriptable. Here's how to get from clean Windows to a working Node + pnpm + Bun setup in 10 minutes using winget.
TL;DR — install everything
Open Terminal as Administrator:
winget install --id Schniz.fnm -e --accept-package-agreements --accept-source-agreements
winget install --id pnpm.pnpm -e
winget install --id Oven-sh.Bun -e
winget install --id Microsoft.VisualStudioCode -e
winget install --id Git.Git -e
winget install --id Microsoft.WindowsTerminal -e
winget install --id Microsoft.PowerShell -e
winget install --id GitHub.cli -e
Then install Node via fnm:
fnm install --lts
fnm use lts-latest
You're ready to ship.
Step 1 — fnm or direct Node?
Two approaches:
Direct install (simpler, single Node version):
winget install --id OpenJS.NodeJS.LTS -e
Good if you only work on one Node version.
fnm (recommended for working on multiple projects):
winget install --id Schniz.fnm -e
fnm is a fast Node version manager. It reads .nvmrc or .node-version files and auto-switches Node version when you cd into a project.
This guide uses fnm. If you prefer direct install, skip Step 2.
Step 2 — Configure fnm in your shell
After install, add fnm to your PowerShell profile:
notepad $PROFILE
Paste:
# fnm
fnm env --use-on-cd | Out-String | Invoke-Expression
Save, restart Terminal. Now cd into a project with a .nvmrc and fnm switches Node version automatically.
Install Node versions
fnm install --lts
fnm install 22
fnm install 20
fnm list
# * v22.10.0 (default)
# v20.18.0 (lts-latest)
Switch:
fnm use 22
fnm default 22
Step 3 — pnpm
winget install --id pnpm.pnpm -e
pnpm is faster than npm, uses 80% less disk space (symlinked store), and has stricter dependency resolution. Verify:
pnpm --version
Make it the default for new projects:
pnpm config set store-dir ~/.pnpm-store
Step 4 — Bun
winget install --id Oven-sh.Bun -e
Bun is an all-in-one runtime + package manager + bundler + test runner, written in Zig. Replaces Node + npm + webpack + jest for many workflows.
bun --version
bun init # creates a starter project in current dir
bun add react
bun run dev
For Next.js / Vite / SvelteKit etc., Bun works as a drop-in npm replacement:
bun install # like npm install
bun run dev # like npm run dev
For server-side, Bun's runtime is 4× faster than Node for hot paths:
bun server.ts
Step 5 — VS Code + extensions
winget install --id Microsoft.VisualStudioCode -e
Install JS/TS essentials:
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
code --install-extension biomejs.biome
code --install-extension yoavbls.pretty-ts-errors
code --install-extension bradlc.vscode-tailwindcss
Biome is a modern alternative to ESLint + Prettier — a single tool, 10× faster.
VS Code settings for JS/TS
Ctrl+, → Open JSON:
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": { "source.fixAll": "explicit" },
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
"typescript.preferences.preferTypeOnlyAutoImports": true
}
Step 6 — Git + GitHub CLI
winget install --id Git.Git -e
winget install --id GitHub.cli -e
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global pull.rebase true
gh auth login
A real example: Next.js + pnpm
cd ~/projects
mkdir my-app
cd my-app
# Use Node 22
fnm use 22
# Bootstrap Next.js with pnpm
pnpm create next-app@latest . --typescript --tailwind --eslint --app
pnpm dev
Open http://localhost:3000.
A real example: Bun + Hono API
cd ~/projects
mkdir my-api
cd my-api
bun init # accept defaults
bun add hono
# Replace index.ts with:
@"
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.json({ hello: 'world' }))
export default app
"@ | Out-File -Encoding utf8 src/index.ts
bun --hot src/index.ts
Open http://localhost:3000.
Lockfile, ".nvmrc", and team standardisation
If you're on a team, commit a .nvmrc:
22
…and an .npmrc to pin pnpm:
engine-strict=true
In package.json:
{
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.0.0"
},
"packageManager": "pnpm@9.12.0"
}
Now anyone cloning the repo gets the right Node + pnpm via fnm + Corepack automatically.
Pin runtimes with winget
If you've direct-installed Node (not via fnm), pin the LTS line:
winget pin add --id OpenJS.NodeJS.LTS --version "22.*"
Now winget upgrade --all won't accidentally bump you to Node 24. See winget pin guide.
Save your setup
winget export -o nodejs-dev.json --include-versions
Future you, on a new machine:
winget import -i nodejs-dev.json
Common pitfalls
Multiple Node versions confused — use fnm. Stop installing Node directly.
pnpm not found after install — close + reopen Terminal. PATH refresh.
Corepack vs pnpm-installed-via-winget — pick one. Either:
corepack enable
# Now `pnpm` comes from package.json's packageManager field
Or:
winget install --id pnpm.pnpm -e
# Now `pnpm` comes from winget
Both works fine; just don't mix.
Bun on Windows fails to install some packages — Bun on Windows is still maturing. For complex projects with native dependencies, fall back to pnpm. Bun for greenfield, pnpm for legacy.
What's next?
- Python setup with winget → — sibling guide for Python
- Best CLI tools → — modern terminal toolkit
- Fresh Windows 11 setup → — full setup workflow
- Developer bundle → — one-click install
