Muhammad Zulfan Wahyudin
Back to Blog

Ditch nvm: Manage Node.js Versions with Nix

· 4 min read
  • 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 nvmrc file 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 use and npm install installs modules for the wrong Node version.
  • CI and local environments silently drift apart because nvmrc is 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