Declarative_depinject
Declarative Dependency Injection in Nix Flakes

This post explores a method for injecting dependencies into NixOS modules from
a flake in a more declarative way, offering an alternative to specialArgs
.
The Problem with specialArgs
As mentioned in post,
specialArgs
andextraSpecialArgs
can be used to pass dependencies and variables from flakes to modules.However,
specialArgs
injects values directly into every module’s argument list.This approach deviates from NixOS’s typical declarative data flow model. Instead of explicit dependency passing, modules receive extra, unstructured variables that aren’t part of the standard module options.
A Declarative Solution: Injecting via a Custom Option
This post introduces a more declarative and centralized technique to share
dependencies across modules by defining a custom option within your flake.nix
. This method makes dependencies accessible to all importing modules without
relying on explicit specialArgs
in your flake’s outputs
.
Defining the dep-inject Module in flake.nix
Within the outputs
function’s let
block in your flake.nix
, define the
following module:
# flake.nix
let
# Module to inject dependencies
depInject = { pkgs, lib, ... }: {
options.dep-inject = lib.mkOption {
# dep-inject is an attribute set of unspecified values
type = with lib.types; attrsOf unspecified;
default = { };
};
config.dep-inject = {
# 'inputs' comes from the outer environment of flake.nix
# usually contains flake inputs, user-defined vars, system metadata
"flake-inputs" = inputs;
userVars = userVars;
system = system;
host = host;
username = username;
};
};
in {
nixosModules.default = { pkgs, lib, ... }: {
imports = [ depInject ];
};
}
This code defines a reusable NixOS module (
nixosModules.default
).This module creates a
dep-inject
option, which is an attribute set containing your flake’s inputs and other relevant variables.By importing depInject, configurations automatically gain access to these dependencies.
Benefits of this Approach
Declarative Dependency Flow: Encourages a more declarative style by accessing dependencies through a well-defined option (
config.dep-inject
) rather than implicit arguments.Centralized Dependency Management: Defines dependencies in one place (
flake.nix
), making it easier to manage and update them.Automatic Availability: Modules importing the configuration automatically have access to the injected dependencies.
Reduced Boilerplate: Avoids the need to explicitly include dependency arguments (
{ inputs, userVars, ... }
) in every module.
Example Usage
Here’s a practical example of how this dep-inject
module is defined and used
within a flake.nix
:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager/master";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
stylix.url = "github:danth/stylix";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
outputs = { self, nixpkgs, home-manager, stylix, treefmt-nix, ... } @ inputs: let
system = "x86_64-linux";
host = "magic";
username = "jr";
userVars = {
timezone = "America/New_York";
gitUsername = "TSawyer87";
locale = "en_US.UTF-8";
dotfilesDir = "~/.dotfiles";
wm = "hyprland";
browser = "firefox";
term = "ghostty";
editor = "hx";
keyboardLayout = "us";
};
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
# Define dep-inject module
depInject = { pkgs, lib, ... }: {
options.dep-inject = lib.mkOption {
type = with lib.types; attrsOf unspecified;
default = { };
};
config.dep-inject = {
flake-inputs = inputs;
userVars = userVars; # Add userVars for convenience
system = system;
username = username;
host = host;
};
};
in {
# Export dep-inject module
nixosModules.default = { pkgs, lib, ... }: {
imports = [ depInject ];
};
# here we don't need imports = [ depInject { inherit inputs;}]
# because the vars are captured from the surrounding let block
# NixOS configuration
nixosConfigurations = {
${host} = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
# enable dep-inject
self.nixosModules.default
./hosts/${host}/configuration.nix
home-manager.nixosModules.home-manager
stylix.nixosModules.stylix
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.${username} = import ./hosts/${host}/home.nix;
home-manager.backupFileExtension = "backup";
# Still need extraSpecialArgs for Home Manager (see below)
home-manager.extraSpecialArgs = {
inherit username system host userVars;
};
}
];
};
};
# Other outputs
checks.x86_64-linux.style = treefmtEval.config.build.check self;
formatter.x86_64-linux = treefmtEval.config.build.wrapper;
devShells.${system}.default = import ./lib/dev-shell.nix { inherit inputs; };
};
}
Using dep-inject
in Modules
Once the dep-inject
module is imported, you can access the injected
dependencies within any module via config.dep-inject
.
Example: System Configuration Module (configuration.nix
)
# configuration.nix
{ config, pkgs, ... }: {
environment.systemPackages = with config.dep-inject.flake-inputs.nixpkgs.legacyPackages.${pkgs.system}; [
firefox
config.dep-inject.userVars.editor # e.g., helix
];
time.timeZone = config.dep-inject.userVars.timezone;
system.stateVersion = "24.05";
}
config.dep-inject.flake-inputs.nixpkgs
: Accesses thenixpkgs
input.config.dep-inject.userVars
: Accesses youruserVars
.You no longer need to explicitly declare
{ inputs, userVars, ... }
in the module’s arguments.
Applying dep-inject to Home Manager Modules
By default, the dep-inject
module is available to NixOS modules but not
automatically to Home Manager modules. There are two main ways to make it
accessible:
- Using
extraSpecialArgs
(Less Ideal)
home-manager.extraSpecialArgs = {
inherit username system host userVars;
depInject = config.dep-inject; # Pass dep-inject
};
Then, in your Home Manager configuration (./hosts/${host}/home.nix
):
# home.nix
{ depInject, ... }: {
programs.git = {
enable = true;
userName = depInject.userVars.gitUsername;
};
home.packages = with depInject.flake-inputs.nixpkgs.legacyPackages.x86_64-linux; [ firefox ];
}
- Importing
depInject
into Home Manager Configuration (More Idiomatic)
# flake.nix
nixosConfigurations = {
${host} = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
self.nixosModules.default # dep-inject for NixOS
./hosts/${host}/configuration.nix
home-manager.nixosModules.home-manager
stylix.nixosModules.stylix
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.backupFileExtension = "backup";
home-manager.users.${username} = {
imports = [ self.nixosModules.default ]; # dep-inject for Home Manager
# Your Home Manager config
programs.git = {
enable = true;
userName = config.dep-inject.userVars.gitUsername;
};
# note: depending on your setup you may need to tweak this
# `legacyPackages.${pkgs.system}` might be needed
# due to how home-manager handles `pkgs`
home.packages = with config.dep-inject.flake-inputs.nixpkgs.legacyPackages.x86_64-linux; [ firefox ];
};
}
];
};
};
By adding
imports = [ self.nixosModules.default ];
within the Home Manager user configuration, thedep-inject
option becomes available underconfig
.This approach is generally considered more idiomatic and avoids the issues associated with
specialArgs
, as highlighted in resources like “flakes-arent-real”
Conclusion
While specialArgs
offers a seemingly straightforward way to inject
dependencies, this declarative approach using a custom dep-inject
option
promotes a cleaner, more structured, and potentially more robust method for
managing dependencies across your NixOS modules. It aligns better with NixOS’s
declarative principles and can enhance the maintainability and
understandability of your configuration.
Disclaimer
- I don’t currently personally use this technique in my configuration, it adds
complexity that
specialArgs
aimed to solve. However, presenting this alternative enhances understanding of different dependency injection methods in Nix Flakes. This example is inspired by and builds upon concepts discussed in flakes-arent-real