Node.js Packages
Node.js Packages
Node is the desk frontend. The boring default is: buildNpmPackage + npmDepsHash from package-lock.json, or pnpm.fetchDeps when the lockfile is pnpm — not npm install in buildPhase.
Go moved to its own chapters. This page is Node only.
Mental model
Same two-step FOD as Go:
package-lock.json → npmDepsHash FOD → npm ci --offline → $out
| Lock | Builder |
|---|---|
package-lock.json |
buildNpmPackage |
pnpm-lock.yaml |
pnpm.fetchDeps + pnpm hooks (26.05) |
| Yarn | yarn-specific fetchers; do not mix |
Commit the lockfile. npm install without a lock is not a pin. npm install in buildPhase hits the sandbox (EAI_AGAIN).
Worked examples
Case 1: buildNpmPackage
Save as node_app.nix:
# node_app.nix
{ buildNpmPackage, lib }:
buildNpmPackage {
pname = "desk-web";
version = "1.0.0";
src = lib.cleanSource ./.;
npmDepsHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
installPhase = ''
runHook preInstall
mkdir -p $out/dist
cp -r dist/* $out/dist/
runHook postInstall
'';
}nix-build -E 'with import <nixpkgs> {}; callPackage ./node_app.nix {}'First run prints got: sha256-…. Paste it into npmDepsHash. npm ci uses the lock, not the latest registry. Never leave lib.fakeHash on main.
Case 2: A CLI with a bin
If package.json has "bin": { "desk-web": "bin/cli.js" }, buildNpmPackage often wraps it:
ls result/bin
./result/bin/desk-web --helpIf you only ship static dist/ for nginx, you do not want nodejs in the runtime closure. Check:
nix path-info -Shr ./result | grep nodejs || echo 'node not in runtime (good for static dist)'Case 3: pnpm
Save as pnpm.nix:
# pnpm.nix
{ lib, stdenv, pnpm, nodejs }:
stdenv.mkDerivation (finalAttrs: {
pname = "desk-web";
version = "1.0.0";
src = lib.cleanSource ./.;
pnpmDeps = pnpm.fetchDeps {
inherit (finalAttrs) pname version src;
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
fetcherVersion = 2;
};
nativeBuildInputs = [ nodejs pnpm.configHook ];
buildPhase = ''
pnpm run build
'';
installPhase = ''
mkdir -p $out
cp -r dist $out/
'';
})fetcherVersion and option names move; the 26.05 nixpkgs doc for pnpm.fetchDeps wins if this snippet drifts. Do not mix npm lock + pnpm fetcher.
Case 4: Node in a devShell, not in the output
Save as flake.nix:
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
{
packages.x86_64-linux.desk-web = pkgs.callPackage ./node_app.nix { };
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.nodejs pkgs.pnpm ];
};
};
}nix develop --command node --versionCI builds with Case 1. Humans pnpm test inside nix develop. Do not npm install -g on the laptop.
Case 5: Native addons
node-gyp needs python3, gcc, headers:
# addon.nix fragment
{
nativeBuildInputs = [ pkgs.python3 pkgs.pkg-config pkgs.nodejs ];
buildInputs = [ pkgs.openssl ];
}If the addon can be skipped (optionalDependencies), prefer a pure-JS path for the desk image. Native addons are a CGO-shaped exception — document gcc in the package comment.
# node_app.nix fragment
{
npmBuildScript = "build"; # package.json script name; default is often "build"
makeCacheWritable = true; # some postinstall scripts write into node_modules
forceGitDeps = true; # only if the lock has git: URLs
}NODE_ENV=production in installPhase is how you drop devDependencies from the runtime tree. A static dist/ served by nginx should not contain node_modules at all — cp -r dist only. nodejs in nativeBuildInputs (build) vs not in the runtime closure is the same split as gcc vs glibc.
The trap
The trap is npm install in buildPhase. Sandbox: EAI_AGAIN / network disabled. Hash the lockfile FOD.
The other trap is no lockfile (package-lock.json gitignored). Then npmDepsHash is a lie next week.
The boring rule
- Lockfile in git.
npmDepsHash/pnpm.fetchDepshash. buildNpmPackagefor npm. pnpm fetcher for pnpm. Do not mix.- Probe hashes; never leave
fakeHashon main. - Toolchain in the devShell; artifacts from the derivation.
- Native addons are a CGO-shaped exception — document gcc.
- Static
dist/should not keepnodejsinnix path-info -Shr.npmBuildScript/makeCacheWritableonly when the log says you need them.
Try this
- Tiny
package.json+ lock;fakeHash; pastegot. - Change a dep in
package.jsonwithoutnpm install; watch the FOD mismatch ornpm cifail. nix path-info -Shr resulton a builtdist; confirmnodejsis or is not in the runtime closure (staticdist/should not need node if nginx serves files).git check-ignore -v package-lock.json— it must not be ignored.