Nix flake guidelines
2 May 2024 - 3 min readThe Nix expression language is very simple and its module system does not prescribe much regarding how to structure your code. This leaves much of the burden of managing the complexity and choosing the organisation of the code down to the user. With many options at every step, building a framework that answers as many questions as possible regarding code structure has proven essential for me.
Here are some guidelines I try to abide to keep my code understandable for myself. They have been written in the context of my configurations for NixOS and Home Manager (which you can find here) but most points should apply to any sufficiently large flake.
Consume your own outputs as inputs
This is NOT irl advice
When defining a flake, the outputs
field is a function parameterised by a set containing self
(along with your regular inputs of course). Use it to create a clean flow of custom modules and libraries being surfaced as outputs then consumed like an input.
You will make it simpler to see where modules are defined and used and make it possible to use the nix repl
subcommand (then run :lf .
to load the content of the flake inside the repl) to quickly explore the contents of your outputs.
Added benefit: it makes your modules easily reusable by other flakes.
Pass your inputs around as unmodified as possible
Transformed food is bad for your health
I pass around most inputs as a flake-inputs
variable which I avoid destructuring. This means that even deep in the file hierarchy, I have the same inputs available to me in the same way. No need to follow the whole path taken by the data to see which transformations might have been applied to it. I can just figure out which input I need and grab it on the spot.
Also, passing this single set everywhere makes it easy to pull in new dependencies, I do not have to add them to the whole branch (e.g. to add a new dependency to a/b/c.nix
, I do not have to pass it in a/default.nix
and a/b/default.nix
after adding it to the inputs as flake-inputs
is already there).
Make your code grep
-friendly
Despite being pretty much just fancy JSON, the lack of static typing makes Nix rather unfriendly to language servers. When making tree-wide refactors, you will generally rely on your favorite variant of grep
, so keep it in mind when writing your code. This is typically why I picked the name flake-inputs
, there is nothing it can conflict with and I can just run rg flake-inputs.some.input
to find all uses of this specific input.
Simple imports
I only consume locally-sourced modules
When you need to access a module, you can do so in a couple of ways: either importing it directly using the Nix built-in function import
or relying on a file that already did so and exposed the results.
Importing modules from any point of your module tree to any point of your module tree can quickly become very messy, and it is much worse if you have two ways to do it.
For this reason the only files that are allowed to use import
are files called default.nix
(and flake.nix
for obvious reasons) and they should only import files or directories (thus importing another default.nix
file) in the directory where they are located. Every default.nix
then exposes whatever might be needed out of its own tree.
This ties with the previous points regarding data flow: always keep it as one-way as possible, with inputs flowing down the tree and outputs being passed up towards flake.nix
in a nice tree structure.