Do you want setup a reproducible nodejs version for your project? And you want to use nix to execute npm/nodejs directly on your OS (Linux/MacOS/WSL2) -- rather than a heavy-weight devcontainer using Docker?

It is not difficult per-se but it took a while for me to assemble all the bits and pieces. Let's make it easier for you!

Requirements

  1. flake-enabled nix - copy the installation shell script from nix-installer if you haven't installed it already.
  2. direnv - if you haven't installed it already:
    1. Use the direnv package from nixpkgs, e.g. with nix profile install nixpkgs#direnv.
    2. Add eval "$(direnv hook bash)" to your .bashrc or eval "$(direnv hook bash)" to your .zshrc.
  3. I assume an existing git repository. If not at hand, create a new directory and execute git init.

Setup flake.nix with flake-parts

The example code uses flake.parts. You can start with this flake.nix template:

# flake.nix "flake" "imports"
{
  inputs = {
    nixpkgs.url = "nixpkgs";

    flake-parts = {
      url = "github:hercules-ci/flake-parts";
      inputs.nixpkgs-lib.follows = "nixpkgs";
    };

    # Development

    devshell = {
      url = "github:numtide/devshell";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  
  outputs = inputs@{ self, nixpkgs, flake-parts, devshell }: flake-parts.lib.mkFlake { inherit inputs; } {
    systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];

    imports = [];

    flake = {
      # your existing definitions before using flake-parts...
    };
  };
}

If you need to quick&dirty migrate a few existing flake attributes, you can stuff them into "flake" and refactor later.

Quickly check if you made any error:

$ git add flake.nix
$ nix flake check
warning: Git tree '/Users/.../projects/astro-starlight-with-nix' is dirty
warning: The check omitted these incompatible systems: aarch64-linux, x86_64-darwin, x86_64-linux
Use '--all-systems' to check all.

You can ignore the warnings but you need to fix any errors.

If you look at git status, yet another file appeared: flake.lock That's good, it contains the exact versions of our dependencies. It should be committed with the rest.

Choose nodejs version

Alright. For astro/starlight, we are going to need nodejs with npm.

Let's chose a nodejs version and put it into the exported packages of the flake. That way we can already test run it and refer to it from other config files:

# flake-modules/nodejs-packages.nix
{
  perSystem = { pkgs, ... }: {
    packages.nodejs = pkgs.nodejs_21;
  };
}

Now add this file to the imports in flake.nix:

# flake.nix
# ...
  imports = [
    ./flake-modules/nodejs-packages.nix
  ];
# ...

Add the file to git and execute nix flake check to notice any errors.

We can also test the nodejs version that we chose:

❯ nix run .#nodejs
warning: Git tree '/Users/.../projects/astro-starlight-with-nix' is dirty
Welcome to Node.js v21.5.0.
Type ".help" for more information.

Setup devshell

This is not so convenient... yet.

Let's make sure that we and our contributors all use the same version!

# flake-modules/nodejs-devshell.nix
{ inputs, ...}: {
  imports = [
    inputs.devshell.flakeModule
  ];

  perSystem = { config, ... }: {
    devshells.default = {
      commands = [
        { package = config.packages.nodejs; category = "docs"; }
      ];
    };
  };
}

Notice the config.packages.nodejs? Here we are referring to the nodejs package definition that we setup before.

Now add this file to the imports in flake.nix:

# flake.nix
# ...
    imports = [
      ./flake-modules/nodejs-packages.nix
      ./flake-modules/nodejs-devshell.nix
    ];
# ...

And now add the new file to git and run nix flake check. Noticing a pattern? Well, get used to it ;)

Now we can enter a devshell with node and npm provided by nix:

❯ nix develop
warning: Git tree '/Users/.../projects/astro-starlight-with-nix' is dirty
🔨 Welcome to devshell

[[general commands]]

  menu   - prints this menu

[docs]

  nodejs - Event-driven I/O framework for the V8 JavaScript engine

[devshell]$ which npm
/nix/store/3h53w5zkwpyapp8510d6ivmhmf47x0bs-devshell-dir/bin/npm
[devshell]$ which node
/nix/store/3h53w5zkwpyapp8510d6ivmhmf47x0bs-devshell-dir/bin/node
[devshell]$
exit

You can exit the shell on Unix-like OSes with CTRL+D or by typing "exit".

Adding an .envrc for direnv

All this entering and exiting. Here come direnv to automate that step for us!

Create a new file:

# .envrc
#!/usr/bin/env bash
# ^ make editor happy

#
# Use https://direnv.net/ to automatically load the dev shell.
#

# Update this by looking at https://github.com/nix-community/nix-direnv#installation
# under "Direnv source"
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
  source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
fi  

watch_file -- flake-modules/*.nix
use flake . --show-trace

If you press enter on the shell prompt now and you have direnv set up properly, you should get something like:

direnv: error /Users/.../projects/astro-starlight-with-nix/.envrc is blocked. Run `direnv allow` to approve its content

Well, we'll do as we are told:

❯ direnv allow .
direnv: loading ~/projects/astro-starlight-with-nix/.envrc
direnv: loading https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc (sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=)
direnv: using flake . --show-trace
direnv: nix-direnv: using cached dev shell
🔨 Welcome to devshell

[[general commands]]

  menu   - prints this menu

[docs]

  nodejs - Event-driven I/O framework for the V8 JavaScript engine

direnv: export +DEVSHELL_DIR +IN_NIX_SHELL +NIXPKGS_PATH +PRJ_DATA_DIR +PRJ_ROOT +name ~PATH ~XDG_DATA_DIRS

Awesome! Now we have npm and nodejs with the chosen version in our PATH whenever we are in this directory.

It also creates a .direnv folder that you probably want to add to your .gitignore:

echo ".direnv" >>.gitignore

Checking your flake with nix repl

nix repl would deserve its own blog article or book but to get your started:

❯ nix repl
Welcome to Nix 2.18.1. Type :? for help.

nix-repl> :lf .
warning: Git tree '/Users/peterkolloch/projects/astro-starlight-with-nix' is dirty
Added 20 variables.

:lf . stands for "load flake" and has to be followed by a path. Here, the current directory.

Now the outputs of the flake are discoverable in the outputs attribute in the shell. You can use tab completion (i.e. hit TAB after outputs.):

nix-repl> outputs.
outputs.apps                 outputs.checks               outputs.devShells            outputs.formatter            outputs.legacyPackages       outputs.nixosConfigurations  outputs.nixosModules         outputs.overlays             outputs.packages
nix-repl> outputs.devShells
outputs.devShells
nix-repl> outputs.devShells.
outputs.devShells.aarch64-darwin  outputs.devShells.aarch64-linux   outputs.devShells.x86_64-darwin   outputs.devShells.x86_64-linux
nix-repl> outputs.devShells.aarch64-darwin.default

You see that there are attributes for each system (architecture) that we defined our flake for.

Updating all dependencies

If you want to use a nodejs version from a more recent nixpkgs later, you'll have to update your flake dependencies. That is as easy as:

nix flake update

VSCode support

Did you know that you can actually force VSCode and some other IDEs to use exactly the same nodejs version for builds that you specified?

For VSCode you can use the direnv plugin. There are a couple of them. This is the one that I use currently.

To help fellow VSCode users find the extension quickly, you can add the following file to the project:

// .vscode/extensions.json
{
    "recommendations": [
        "rubymaniac.vscode-direnv"
    ]
}

Conclusion

Now we have set up a very simple dev shell with a pinned nodejs version that is convenient to use. That is a big help to onboard anyone onto the project.

We did it step-by step by adding little tiny flake-module.nix files to our flake. This was convenient for this tutorial but actually also works well in practice. If you scope one flake-module.nix to one purpose, it remains easy to understand.

A good basis - let's soon add more on top! For example, a documentation site built with the astro framework:

Tutorial: Building our Astro Starlight page with Nix & flake.parts

Feel free to give me feedback/ask questions at discourse or in a GitHub issue. I want to hear your thoughts so feel free to err on the side of commenting too much.