Ditch nvm: Manage Node.js Versions with Nix
- nix
- nvm
- nodejs
- devtools
- developer-experience
Nix is not just a package manager or an OS. It is a better nvm — declarative, deterministic, and per-project Node.js versioning without shell hacks or manual switches.
The nvm Tax
Every JavaScript developer knows the ritual:
nvm use 18
nvm install 20
nvm alias default 20
It works. But over time, friction accumulates:
- Every terminal session needs a shell hook that slows down startup.
- The
nvmrcfile is just a convention — nothing enforces it. - Project A needs Node 18, Project B needs Node 20, and your global default is a guessing game.
- You forget to run
nvm useandnpm installinstalls modules for the wrong Node version. - CI and local environments silently drift apart because
nvmrcis not a build contract.
None of this is catastrophic. But none of it is solved either.
The Nix Approach
Nix is a purely functional package manager and build system. You describe your development environment in a file, and Nix materialises it — isolated, reproducible, and completely independent of whatever Node.js version happens to be installed on your host.
If you already use NixOS or have Nix installed on macOS or Linux, you can replace nvm entirely. No shell hooks, no nvm use, no version drift.
Per-Project Node.js with shell.nix
Create a shell.nix in your project root:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.nodejs_20
];
}
Enter the shell:
nix-shell
That is it. You are now in a subshell with Node 20 on PATH. node --version prints v20.x.x. npm and npx are the ones shipped with that exact Node build.
Exit the shell and you are back to your system Node (or no Node at all). No pollution, no shell hooks, no manual version switching.
Multiple Node Versions
Need to test against multiple versions? Nix makes it trivial:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.nodejs_18
pkgs.nodejs_20
pkgs.nodejs_22
];
shellHook = ''
echo "Available Node.js versions:"
echo " nodejs_18 → node-18"
echo " nodejs_20 → node-20 (default)"
echo " nodejs_22 → node-22"
'';
}
Inside the shell, all three versions are on PATH, ready to be invoked directly.
Using Flakes (Modern Nix)
If you are on a modern Nix setup with flakes enabled, use flake.nix instead:
{
description = "My project development shell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
};
outputs = { nixpkgs, ... }: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.${system}.default = pkgs.mkShell {
buildInputs = [
pkgs.nodejs_20
pkgs.node-gyp
];
};
};
}
Then enter it with:
nix develop
Flakes give you a lockfile (flake.lock) that pins every dependency of your dev shell — including the exact Nixpkgs revision. This means the same Node.js patch version is used across your entire team, CI, and any machine that runs nix develop.
Beyond Node: The Real Upside
Nix manages more than just Node.js. Your shell.nix can include any tool your project needs, and they all come pinned to exact versions:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.nodejs_20
pkgs.yarn
pkgs.pnpm
pkgs.go_1_22
pkgs.just
pkgs.dprint
pkgs.biome
pkgs.dbmate
pkgs.temporal-cli
];
}
Every teammate runs exactly the same versions. No “works on my machine” for tooling. A single nix-shell (or nix develop) provisions your entire local environment.
Compare that to nvm, which only solves Node.js — and only partially at that.
What You Lose
Nix is not a drop-in replacement that costs nothing:
- Learning curve — the Nix language is unlike anything you have used. Expect a week of confusion before it clicks.
- Binary cache misses — if a package is not in the cache, Nix builds it from source. For Node.js itself this is not an issue, but some tools can take minutes to compile.
- macOS compatibility — most Nix tooling targets Linux. macOS works, but some packages need special handling.
But none of these affect the core workflow of “run nix-shell, get Node.js”. That path is well-trodden and fast.
The Bottom Line
If all you need is to switch between Node versions, nvm works fine. Stick with it.
But if you already use Nix, or if you want a unified way to pin every tool in your project (not just Node), nvm becomes redundant. A shell.nix or flake.nix handles Node versioning more reliably, without shell hooks, and with deterministic guarantees that nvm cannot match.
At the end of the day, the question is not “Can Nix replace nvm?” — it is “Why would you keep both?”
Nix replaces nvm, nodenv, direnv, asdf, rtx, mise, Homebrew, and most other language-version managers — all with one file and one command. The fewer tools you maintain, the fewer things break.
Have a project in mind? Let's build something together.
Get in Touch