Why this matters
Nix has no standard library in the language itself. There are 116 builtins baked into the evaluator, and a much larger lib that ships inside nixpkgs. Half of reading a flake is recognising lib.genAttrs, lib.optional, mapAttrs and knowing which one needs pkgs in scope.
Concept
Two sources of functions
builtins is a global attrset. Always available, even with no nixpkgs:
builtins.length (builtins.attrNames builtins)116A few are also exposed as bare globals: map, toString, throw, import, derivation, baseNameOf, dirOf, removeAttrs, abort. Everything else is builtins.x.
lib is Nix code, in nixpkgs/lib/. You get it from pkgs.lib or nixpkgs.lib in a flake. It wraps builtins with nicer names and adds hundreds more. Check the version to see it is just data from nixpkgs:
nix eval nixpkgs#lib.version"26.11.20260929.b6c8664"Rule: if you are in a plain nix eval --expr with no nixpkgs, you have builtins only. Inside a flake output, use lib; it reads better and has the functions people expect.
Lists: map, filter, fold
[
(builtins.map (x: x + 1) [ 1 2 3 ])
(builtins.filter (x: x > 1) [ 1 2 3 ])
(builtins.foldl' (acc: x: acc + x) 0 [ 1 2 3 4 ])
][ [ 2 3 4 ] [ 2 3 ] 10 ]foldl' is strict in the accumulator; the apostrophe is part of the name. Also: builtins.elem, builtins.head, builtins.tail, builtins.concatLists, builtins.genList, builtins.sort. lib adds lib.flatten, lib.unique, lib.last, lib.range.
Attrsets: mapAttrs, genAttrs, filterAttrs
mapAttrs transforms values, keeping keys. It is a builtin:
builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }{ a = 2; b = 4; }genAttrs builds a set from a list of names. This one is lib only:
nix eval --expr 'let lib = (builtins.getFlake "nixpkgs").lib; in lib.genAttrs [ "nodejs" "pnpm" ] (name: "pkg:${name}")' --impure{ nodejs = "pkg:nodejs"; pnpm = "pkg:pnpm"; }This is how forAllSystems works in flakes: lib.genAttrs [ "x86_64-linux" "aarch64-darwin" ] (system: ...).
Also from lib.attrsets: filterAttrs (n: v: v > 1), mapAttrsToList (n: v: "${n}=${toString v}") giving [ "a=1" "b=2" ], recursiveUpdate for the deep merge that // is not, attrByPath [ "a" "b" ] fallback set. Builtins: attrNames, attrValues, hasAttr, getAttr, removeAttrs, intersectAttrs, listToAttrs.
Strings
[
(builtins.concatStringsSep ", " [ "a" "b" "c" ])
(builtins.replaceStrings [ "a" ] [ "o" ] "banana")
(builtins.substring 0 3 "hello")
(builtins.stringLength "hello")
][ "a, b, c" "bonono" "hel" 5 ]lib.strings adds the ones you reach for daily: toUpper, hasPrefix, splitString "." "1.2.3" giving [ "1" "2" "3" ], removeSuffix, concatMapStringsSep, optionalString cond "text". Note builtins.split is a regex splitter that returns captures interleaved: builtins.split "," "a,b,c" is [ "a" [ ] "b" [ ] "c" ]. Use lib.splitString for the obvious behaviour.
Conditionals as data: optional, optionals
The most common lib idiom in package lists:
nix eval nixpkgs#lib.optional --apply 'f: [ (f true "yes") (f false "yes") ]'[ [ "yes" ] [ ] ]packages = [ pkgs.nodejs ] ++ lib.optional withTurso pkgs.turso-cli; adds the item only when the flag is true. lib.optionals takes a list instead of one item.
JSON
builtins.toJSON and builtins.fromJSON convert both ways:
builtins.fromJSON (builtins.toJSON { a = 1; b = [ true null ]; }){ a = 1; b = [ true null ]; }nix eval --json uses the same conversion. Handy for feeding Nix data to Node.
Finding functions
Three routes, in order:
- noogle.dev. Search by name or by type signature. Type
genAttrs, read the signature and example. Covers builtins and lib. :docin the repl for builtins::doc builtins.map. See The Nix REPL.- Read the source. Every lib function has a position:
nix eval nixpkgs#lib.attrsets --apply 'a: builtins.unsafeGetAttrPos "genAttrs" a'{ column = 3; file = "/nix/store/pzwzs371qflfnkdg37gisaz1pl7qa2jg-source/lib/attrsets.nix"; line = 1351; }Open that file at that line. lib source has doc comments with a # Type section above every function, for example genAttrs :: [String] -> (String -> a) -> { [String] :: a }, followed by an example. It is the best documentation Nix has.
Try it
nix eval nixpkgs#lib.strings --apply 's: s.splitString "." "1.2.3"'[ "1" "2" "3" ]nix eval --expr 'builtins.split "," "a,b,c"'[ "a" [ ] "b" [ ] "c" ]Same intent, different output. --apply lets you call a function from a flake attribute without --impure.
Exercise
Using only builtins, turn { nodejs = "22.23.3"; pnpm = "12.3.4"; } into the single string "nodejs@22.23.3 pnpm@12.3.4".
Solution
let
versions = { nodejs = "22.23.3"; pnpm = "12.3.4"; };
pairs = builtins.attrValues (builtins.mapAttrs (n: v: "${n}@${v}") versions);
in
builtins.concatStringsSep " " pairs"nodejs@22.23.3 pnpm@12.3.4"mapAttrs keeps the keys, so attrValues is needed to get a list. With lib this is one call: lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}@${v}") versions).
Trap
builtins.currentSystem in a flake. It is not there:
error: attribute 'currentSystem' missingFlakes evaluate in pure mode, and currentSystem is impure. That is why every flake hard-codes system = "x86_64-linux" or uses genAttrs over a list of systems. If a snippet from a blog uses currentSystem, it predates flakes or needs --impure.