Setting Up a Machine for JavaScript Development Again
It had been a while since I set up a machine for JavaScript. Here is the toolchain I chose (fnm, pnpm and Vitest) and how I used the Fibonacci kata to test it.
I haven’t set up a machine for JavaScript development in a while, and the tools have changed. Before I build anything real, I wanted a clean, current setup that I understand piece by piece.
That raises a question: how do you know a setup works? Seeing a version number after node -v isn’t enough. You find out by writing some code, testing it, changing it, and watching the whole loop run. For that I needed a small problem I already know well, so that any failure points at the setup and not at my code. The Fibonacci kata fits: I know the solution, it’s small, and it has an obvious test.
So this post is about the machine setup. The kata is the smoke test at the end.
The toolchain
- fnm manages Node versions. It’s fast, and it switches versions automatically per project.
- pnpm installs packages. Corepack, which ships with Node, manages the pnpm version for each project.
- Vitest runs the tests. It understands modern JavaScript out of the box, so there is no Babel and no config file.
1. Install fnm
fnm is a Node version manager written in Rust. I’m replacing nvm with it, which is slower to start and has to be loaded into every shell.
curl -fsSL https://fnm.vercel.app/install | bash
The script installs fnm and adds a loader block to your ~/.zshrc. On a Mac it uses Homebrew behind the scenes, so Homebrew needs to be installed. If you don’t have it, add -s -- --force-install after bash to download the binary directly.
The loader the script adds does not include --use-on-cd, so open ~/.zshrc, find the line that runs fnm env, and change it to:
eval "$(fnm env --use-on-cd --shell zsh)"
--use-on-cd makes fnm switch Node versions automatically when you enter a folder containing a .node-version or .nvmrc file. Open a new terminal (or run source ~/.zshrc) so the change takes effect, then check it with fnm --version.
If you’re coming from nvm, remove its lines from your shell config and delete the ~/.nvm folder. Global npm packages don’t carry over, so run npm ls -g --depth=0 first if you want a list of what to reinstall.
2. Install the latest Node LTS
fnm install --lts
fnm default lts-latest
node -v
The first command installs the newest long-term-support release, the second makes it the default for new shells, and the third confirms which version is active. I chose LTS over the newest release because it has longer support and fewer rough edges.
3. Get pnpm through Corepack
Corepack ships with Node and manages package managers per project. It creates a small pnpm shim next to your Node binary. The first time you run pnpm, it downloads the right version and caches it.
corepack enable pnpm
pnpm -v
Two things to know:
- The shim lives next to the active Node version, so after installing a different Node with fnm you may need to run
corepack enable pnpmagain. - Starting with Node 25, Corepack is no longer bundled. If
corepackisn’t found, runnpm install -g corepackfirst.
4. Create a project
mkdir fib-kata && cd fib-kata
node -v > .node-version
git init
pnpm init
The .node-version file pins the Node version for this folder, and fnm picks it up whenever you cd into it. pnpm init creates package.json and records the pnpm version in it.
5. Add Vitest
pnpm add -D vitest
Then edit package.json so it contains these fields, keeping whatever else pnpm generated:
{
"name": "fib-kata",
"type": "module",
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
"type": "module" lets you use import and export directly. pnpm test runs the suite once, which is handy for a final check. pnpm test:watch reruns only the affected tests each time you save, and it’s the one you’ll use while working.
Finally, keep installed packages out of git:
printf "node_modules\n" > .gitignore
6. The smoke test: the Fibonacci kata
Now for the test of the setup itself. Create the two files:
mkdir src
touch src/fib.js src/fib.test.js
In src/fib.test.js, write a first test that will fail:
import { describe, it, expect } from 'vitest'
import { fib } from './fib.js'
describe('Fibonacci Sequence', () => {
it('returns 0 for 0', () => {
expect(fib(0)).toBe(0)
})
})
And a stub in src/fib.js, so the import resolves and the test fails for the right reason (a wrong answer, not a missing file):
export function fib(n) {
return undefined
}
Start the watcher:
pnpm test:watch
You should see the test fail. Make it pass with the simplest thing that could work:
export function fib(n) {
return 0
}
Add the next test, fib(1) is 1, watch it fail, and change the code just enough to pass. Keep going through 2, 3, 5 and 10. Once you have several input and output pairs, a table keeps the tests compact:
it.each([
[0, 0],
[1, 1],
[2, 1],
[3, 2],
[5, 5],
[10, 55],
])('fib(%i) is %i', (n, expected) => {
expect(fib(n)).toBe(expected)
})
You may end up with something like this:
export function fib(n) {
let [current, next] = [0, 1]
for (let i = 0; i < n; i++) {
[current, next] = [next, current + next]
}
return current
}
What the kata proves
Each part of the exercise exercises a different part of the setup:
- Node and fnm:
node -vin the project folder shows the version in.node-version, so automatic switching works. - pnpm and Corepack:
pnpm initandpnpm add -D vitestran without errors, and the pnpm version is pinned inpackage.json. - ES modules: the
importandexportlines run with no build step. - Vitest: a failing test fails, a passing test passes, and the report is readable.
- The feedback loop: saving a file reruns the tests within a second or so.
If all of those hold, the machine is ready.
7. Commit the starting point
git add .
git commit -m "Set up JavaScript dev environment with the Fibonacci kata"
This includes pnpm-lock.yaml, which records the exact version of every installed package so a fresh clone installs the same tree. Commit it.
What this setup doesn’t cover yet
I’ve deliberately kept this minimal. It doesn’t include editor integration, linting, formatting or TypeScript. Those are the next layers, and it’s easier to add them to a setup I know works.
If you want to keep using the kata, there is plenty of room to grow it: reject invalid input with Number.isInteger(n) and a RangeError, compare recursive, memoised and iterative versions, or switch to BigInt for results beyond Number.MAX_SAFE_INTEGER, which is where fib(79) lands.