Writing Modules
Modules should be self-contained with options and config together, using mkIf and mkMerge for lazy evaluation.
Writing Modules
The Dendritic Principle
We take inspiration from dendritic thinking: modules should be self-contained, with options and config together, dependencies explicit. We take the concept, not the ecosystem—no external libraries required, just the principle of keeping related things together.
A module is a function that declares options and provides configuration. Multiple modules compose, and they can depend on each other’s options. This is the core abstraction of NixOS, home-manager, flake-parts, and many others.
Module Structure
{ config, lib, pkgs, ... }:let inherit (lib) mkOption types mkIf mkMerge; cfg = config.weyl.services.apiServer;in{ _class = "nixos";
options.weyl.services.apiServer = { enable = mkOption { type = types.bool; default = false; description = '' Whether to enable the Weyl API server.
Configures a systemd service with automatic restart and opens the appropriate firewall port. ''; };
package = mkOption { type = types.package; default = pkgs.weyl-api-server; description = "The API server package to use."; };
settings = { port = mkOption { type = types.port; default = 8080; description = "Port the API server listens on."; };
workers = mkOption { type = types.ints.positive; default = 4; description = "Number of worker processes."; }; }; };
config = mkIf cfg.enable { systemd.services.weyl-api-server = { description = "Weyl API Server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ];
serviceConfig = { ExecStart = "${cfg.package}/bin/weyl-api-server --port ${toString cfg.settings.port}"; Restart = "always"; DynamicUser = true; }; };
networking.firewall.allowedTCPPorts = [ cfg.settings.port ]; };}Lazy Evaluation: mkIf and mkMerge
Always use mkIf and mkMerge in module config blocks:
# CORRECT: Lazy, composes properlyconfig = mkMerge [ (mkIf cfg.enable { services.postgresql.enable = true; })
(mkIf cfg.backup.enable { services.postgresqlBackup.enable = true; })];
# BROKEN: Eager evaluation, causes infinite recursionconfig = if cfg.enable then { services.postgresql.enable = true;} else {};The if/then/else evaluates immediately. If cfg.enable references another module’s option, and that module references this one, infinite recursion. mkIf is lazy—it creates a value the module system processes after all modules are loaded.
Option Nesting
Nested structure over flat namespaces:
# Good: Structure reflects the systemoptions.weyl.services.api = { server = { port = mkOption { type = types.port; }; address = mkOption { type = types.str; }; }; tls = { enable = mkOption { type = types.bool; }; certificate = mkOption { type = types.nullOr types.path; }; };};
# Bad: Flat, no structureoptions.weyl-api-server-port = mkOption { ... };options.weyl-api-tls-enable = mkOption { ... };Module Class Markers
When different module types coexist, use _class to catch mistakes at evaluation time (see lib.evalModules):
{ config, lib, pkgs, ... }: { _class = "nixos"; # ...}
# modules/home/editor.nix{ config, lib, pkgs, ... }: { _class = "home-manager"; # ...}Import a home-manager module into NixOS config? Immediate, clear error—not mysterious failure three layers deep.