Multi-Language Development Environments
Multi-Language Development Environments
The desk API is Go, the admin UI is Node, a report is Python, and one crate talks to OpenSSL. The boring default is: one mkShell listing every compiler and C library the repo actually uses — not asdf + nvm + pyenv + libssl-dev.
Mental model
Version managers fight over PATH. Nix does not: each package is a store path. Two Pythons can exist; the shell only prepends the ones you listed.
| Need | Nix |
|---|---|
| Go / Node / Python interpreters | pkgs.go, pkgs.nodejs, python3.withPackages |
C headers / .so |
buildInputs (openssl, zlib, postgresql) |
pkg-config, cmake |
nativeBuildInputs |
| Per-subdir tools | Named devShells.backend / frontend (direnv chapter) |
Do not dump gcc+node+python into every shell “just in case.” Closures grow; eval slows. Split named shells when the frontend never needs go.
In mkShell, packages is the PATH you type (go, node). nativeBuildInputs is setup-hook tools (pkg-config, cmake). buildInputs is libraries those hooks see (openssl). Mixing “everything in packages” skips setup hooks — pkg-config --libs openssl then fails even though openssl is “installed.”
Worked examples
Case 1: One polyglot shell
Save as polyglot.nix:
# polyglot.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = [
pkgs.go
pkgs.nodejs
(pkgs.python3.withPackages (ps: [ ps.requests ps.pydantic ]))
pkgs.sqlite
];
shellHook = ''
echo "Polyglot: $(go env GOVERSION) node $(node -v) $(python3 --version)"
'';
}nix-shell polyglot.nix --run 'go env GOVERSION; node -v; python3 --version'Output (shape — versions follow 26.05):
go1.24.x
v22.x.x
Python 3.12.x
Use pkgs.nodejs (channel default on 26.05), not a random nodejs_22 unless you have a reason to pin a major. pkgs.go is the channel Go — buildGoModule in CI must use the same major or go.mod’s toolchain line fights GOTOOLCHAIN=local.
nix-shell polyglot.nix --run 'which go; which node; which python3'All three must be /nix/store/…. A mix of /usr/bin/python3 and store go is two version managers again. PATH order in the shell is Nix first.
Case 2: OpenSSL via pkg-config
Save as native_c.nix:
# native_c.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl pkgs.zlib ];
shellHook = ''
pkg-config --cflags openssl
'';
}nix-shell native_c.nix --run 'pkg-config --libs openssl'Output (shape):
-L/nix/store/…-openssl-…/lib -lssl -lcrypto
Headers are not /usr/include/openssl. PKG_CONFIG_PATH is set by the setup hook because pkg-config is in nativeBuildInputs. Put openssl only in packages and the hook never runs.
Case 3: Named shells in a flake
# flake.nix fragment
devShells.x86_64-linux = {
default = self.devShells.x86_64-linux.backend;
backend = pkgs.mkShell { packages = [ pkgs.go pkgs.gopls ]; };
frontend = pkgs.mkShell { packages = [ pkgs.nodejs pkgs.pnpm ]; };
};nix develop .#frontend --command which node
nix develop .#backend --command which godirenv: use flake .#frontend in web/.envrc.
Case 4: Python packages vs a second venv
packages = [
(pkgs.python3.withPackages (ps: [ ps.requests ]))
];python3 -c "import requests" works. pip install requests inside this shell writes to a user venv and fights Nix. If you need a lockfile-heavy app, package it (buildPythonApplication) instead of pip in the shell.
Case 5: LD_LIBRARY_PATH is a last resort
Nix wrappers set RPATH. If a vendor binary still cannot find libssl.so, use nix-ld / FHS (later chapter), not:
export LD_LIBRARY_PATH=$(nix eval --raw nixpkgs#openssl.out)/libThat leaks into every child process.
The trap
The trap is buildInputs = [ pkgs.go pkgs.nodejs pkgs.python3 pkgs.gcc pkgs.cmake pkgs.openssl … ] copied from another team. The shell takes a minute to eval and nobody uses cmake. List what README says you run.
The other trap is host /usr/include for openssl while Node’s node-gyp uses Nix python. Mix = “works on Ubuntu, dies on macOS.”
A third: packages = [ pkgs.openssl pkgs.pkg-config ] and wondering why pkg-config sees nothing — use nativeBuildInputs / buildInputs. A fourth: pip install / npm i -g inside nix develop “just this once.”
The boring rule
- One shell per job (backend / frontend), not one kitchen sink.
- Interpreters in
packages. C libs inbuildInputs. cmake/pkg-config innativeBuildInputs. python3.withPackagesinstead of pip. Same Go major asbuildGoModule.- Pin majors (
nodejs_22) only when the default is wrong. - No host headers. No global
LD_LIBRARY_PATH.
Try this
- Add
pkgs.cargoto Case 1;nix-shell --run 'cargo --version'. pkg-config --cflags opensslin Case 2; confirm/nix/store.- Split Case 3;
which nodemust fail in.#backendif node is not listed. nix-shell --pure --run 'python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"'with openssl inbuildInputs.- Move
pkg-configfrompackagestonativeBuildInputsand confirmpkg-config --libs opensslstarts working.