-----BEGIN YUNFACHI WEBSITE-----

Separate development inputs into their own `dev` flake to avoid redundant dependencies - 2026-08-20
[back to posts] [edit]

-----BEGIN POST CONTENT-----

First of all, this post is aimed at flakes that provide external outputs, like `nixosModules` or `packages` — your private NixOS configuration DOES NOT need this.

For example, you have a project called `niri-nix`, which introduces Niri's NixOS module and Niri package. But besides nixpkgs, that flake's inputs also include `flake-parts`, `treefmt-nix`, `git-hooks.nix`, and so on. Nix flake inputs are NOT lazily fetched. This means that if you only use, for example, `nixosModules.default` from that flake, and `nixosModules.default` does not mention `treefmt-nix` or any of the other inputs at all, they will still be added to the user's flake lock as dependencies of your flake, and they will still be downloaded.

> But how can you prevent some inputs from being redundantly and meaninglessly fetched?
> Implement a separate flake for those inputs — I call it a `dev` flake. The idea is to keep development-only inputs outside of the flake that is consumed by users.

There are 2 ways to set up a `dev` flake:

1. Use the `dev` flake for both inputs and logic, while the main flake only needs to integrate the `dev` flake's outputs into itself.
2. Use the `dev` flake only for inputs, while the logic stays in the main flake.

Both of these methods will be discussed. But to quickly answer which is the better option: if your `dev` logic is huge, then choose the first option; otherwise, you are good to go with the second one.

Let's begin with the first option. First, initialize the `dev` flake in the `dev/` directory:

# dev/flake.nix
{
  inputs = {
    # By this "hack", you can access your root flake and its inputs to
    # deduplicate them in the `dev` flake too. It only works in a git repository.
    # my-project.url = "path:../.";
    # nixpkgs.follows = "my-project/nixpkgs";

    # Nixpkgs is required for flake-parts `perSystem` to work.
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

    # Remove this if flake-parts is already used in your root flake
    # or if you want to implement the mechanism yourself.
    flake-parts.url = "github:hercules-ci/flake-parts";

    # Add any inputs that you want here; `treefmt-nix` is used as an example.
    treefmt-nix = {
      url = "github:numtide/treefmt-nix";
      # inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  # Logic will be written in a separate file.
  outputs = _: { };
}

> Can I do this without flake-parts?
> Yes, but you'll need to implement your own mechanism for separating the `dev` flake's logic into another file. This can be a bit tricky. You may want to consider the second option below instead.

Then we'll create a flake-parts module in it. Don't worry, flake-parts is not required in your root flake:

# dev/config.nix
# The content of this file is just an example. You can write anything you want here:
# https://flake.parts/options/flake-parts.html
{ inputs, ... }: {
  systems = [ "x86_64-linux" ];

  imports = [ inputs.treefmt-nix.flakeModule ];

  perSystem = _: {
    # https://flake.parts/options/treefmt-nix.html
    treefmt = {
      projectRootFile = "flake.nix";

      programs = {
        nixfmt.enable = true;
        deadnix.enable = true;
      };
    };
  };
}

Finally, you have to export the `dev` outputs to your root flake.
If you already use `flake-parts` in your root flake, you should use its partitions feature instead.
Otherwise, to avoid adding flake-parts to the root flake's dependencies, write this:

# flake.nix
{
  inputs = {
    # You may remove this; read the comments below.
    flake-compat.url = "github:NixOS/flake-compat";

    # ...
  };

  outputs = { flake-compat, ... }: let
    # If you have `flake-compat` in your inputs, keep this:
    devFlake = (import flake-compat { src = ./dev; }).defaultNix;

    # But if you don't want `flake-compat` itself to become a dependency of your users, you can fetch the pinned flake-compat implementation directly:
    # devFlake =
    #   (import (builtins.fetchurl {
    #     url = "https://raw.githubusercontent.com/NixOS/flake-compat/f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2/default.nix";
    #     sha256 = "sha256:0nycwx0777d451k63ghp6p5lcv791kziqgkvqzmr1qwzywkdk1cj";
    #   }) { src = ./dev; }).defaultNix;

    devOutputs = devInputs.flake-parts.lib.mkFlake {
      inputs = devFlake.inputs // {
        self = devFlake;
      };
    } ./dev/config.nix;
  in {
    # Explicitly inherit outputs to prevent forcing evaluation of the `dev` flake
    # when accessing root flake outputs.
    inherit (devOutputs) checks formatter;
  } // {
    # Some output that is free from fetching `dev` inputs.
    # nixosModules.default = ...;
  };
}

Harder than just having `treefmt-nix` in the root flake's inputs? Yes. But better for users? Absolutely.

This approach is also used in the following projects:

- ://github.com/NixOS/flake-compat (without flake-parts in the root flake)
- ://github.com/hercules-ci/flake-parts (with flake-parts in the root flake)

---

Now, let's discuss the second option. First, you need to initialize the `dev` flake:

# dev/flake.nix
{
  inputs = {
    # By this "hack", you can access your root flake and its inputs to
    # deduplicate them in the `dev` flake too. It only works in a git repository.
    # my-project.url = "path:../.";
    # nixpkgs.follows = "my-project/nixpkgs";

    # Add any inputs that you want here; `treefmt-nix` is used as an example.
    treefmt-nix = {
      url = "github:numtide/treefmt-nix";
      # inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  # This flake is only for inputs; there is no logic here.
  outputs = _: { };
}

Then generate the lock file by running this command in the `dev/` directory: `nix flake lock`.

And, finally, you have to access the `dev` inputs from the root flake:

# flake.nix
{
  inputs = {
    # You may remove this; read the comments below.
    flake-compat.url = "github:NixOS/flake-compat";

    # ...
  };

  outputs = { flake-compat, ... }: let
    # If you have `flake-compat` in your inputs, keep this:
    devInputs = (import flake-compat { src = ./dev; }).defaultNix.inputs;

    # But if you don't want `flake-compat` itself to become a dependency of your users, you can fetch the pinned flake-compat implementation directly:
    # devInputs =
    #   (import (builtins.fetchurl {
    #     url = "https://raw.githubusercontent.com/NixOS/flake-compat/f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2/default.nix";
    #     sha256 = "sha256:0nycwx0777d451k63ghp6p5lcv791kziqgkvqzmr1qwzywkdk1cj";
    #   }) { src = ./dev; }).defaultNix.inputs;
  in {
    # Some output that is free from fetching `dev` inputs.
    # nixosModules.default = ...;

    # Actually, read the `treefmt-nix` manual for the proper setup of it; this post is not about how to use `treefmt-nix`.
    formatter = devInputs.treefmt-nix.formatter;
  };
}

-----END POST CONTENT-----

-----END YUNFACHI WEBSITE-----

Evaluated with Nix 2.34.7 on x86_64-linux
Revision 663b77f (modified on 2026-08-21)