Switching from nvm to fnm

fnm and nvm are tools for managing node installations. This is useful for ensuring the correct version of node is used for each project.

fnm is built in Rust. It’s orders of magnitude faster than nvm, cross-platform, and fully compatible with existing .nvmrc and .node-version files.

Why Switch?

nvm is a massive shell script. It sources itself every time you open a new terminal tab. This overhead becomes noticeable, often adding 300-500ms to shell startup times.

fnm solves this. It runs as a compiled binary. Shell startup impact is virtually zero. It supports Windows, macOS, and Linux out of the box.

Removing nvm

First, tear out nvm. Delete the installation directory:

rm -rf ~/.nvm

Open your shell configuration file (~/.zshrc or ~/.bashrc) and delete the nvm initialization block. It typically looks like this:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

Restart your terminal or run source ~/.zshrc.

Installing fnm

Install fnm via its bash script or Homebrew.

Using the bash script:

curl -fsSL https://fnm.vercel.app/install | bash

Using Homebrew (macOS/Linux):

brew install fnm

Hook fnm into your .zshenv instead of .zshrc. This ensures fnm is available in both interactive and non-interactive shells, which is necessary if you use tools that run background processes or AI agents like Claude Code.

Add the following to your ~/.zshenv:

eval "$(fnm env --use-on-cd --resolve-engines --version-file-strategy=recursive)"
INFO
  • --use-on-cd: Automatically switches Node versions when entering a directory with an .nvmrc or .node-version file.
  • --resolve-engines: Allows reading the Node version from the engines field in package.json.
  • --version-file-strategy=recursive: Searches up the directory tree to find a version file if one isn’t present in the current directory.

Restart your terminal.

.node-version vs .nvmrc

If you are setting up a new project, prefer using a .node-version file instead of .nvmrc.

While fnm supports both, .nvmrc implies the usage of nvm. .node-version is a generic standard supported by most modern version managers (including fnm, nodenv, asdf, and volta). It’s a cleaner approach that signals to other developers that any compliant version manager can be used.

Reading from package.json

Because we enabled the --resolve-engines flag, fnm can automatically detect the required Node version from your package.json if you don’t have a .node-version or .nvmrc file.

To use this, add the engines field to your package.json:

{
  "engines": {
    "node": ">=22.0.0"
  }
}

When you cd into this directory, fnm will parse the engines.node range and automatically switch to an installed version that satisfies the requirement.

Managing Versions

Specifying and switching versions in fnm is nearly identical to nvm:

fnm install 22       # Install a specific version
fnm install --lts    # Install the latest LTS release
fnm use 22           # Switch to a specific installed version
fnm default 22       # Set the default version for new shells
node -v

Same functionality, zero lag.

GitHub Actions CI

When setting up your CI pipeline with GitHub Actions, the official actions/setup-node step seamlessly supports .node-version, .nvmrc, and package.json out of the box.

Just point the node-version-file parameter to your source of truth:

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version-file: '.node-version' # or '.nvmrc' or 'package.json'

This keeps your CI pipeline perfectly synced with your local fnm environment, avoiding the headache of updating Node versions in multiple places.

Resources

If you enjoyed this article, feel free to support my work on Ko-fi!

Support on Ko-fi