Importing Files and Building Abstractions
Importing Files and Building Abstractions
One file does not hold a desk. The boring default is: small files that return functions, import ./x.nix { … } with explicit args, and no <nixpkgs> in anything you ship.
Mental model
import evaluates a path to a value:
import ./constants.nix
import ./service.nix { port = 8080; }The imported file cannot see your let names unless you pass them. There is no implicit global pkgs unless you created one.
<nixpkgs> looks up NIX_PATH. Two laptops, two channels, two worlds. Flakes pin github:NixOS/nixpkgs/nixos-26.05. This chapter still uses relative ./ files so you see the operator.
pkgs.lib (once you have pkgs) is optional, mkIf, recursiveUpdate, toUpper, … — prefer it over reinventing if cond then [ x ] else [ ].
Paths in import ./foo.nix are relative to the file that contains the import, not to the shell cwd.
Worked examples
Case 1: Function file + caller
Save as database.nix:
# database.nix
{ environment }:
let
isProduction = environment == "production";
in
{
dbHost = if isProduction then "db.desk.corp" else "127.0.0.1";
dbPort = if environment == "staging" then 5433 else 5432;
poolSize = if isProduction then 50 else 5;
}Save as app.nix:
# app.nix
let
dbConfig = import ./database.nix { environment = "production"; };
in
{
appName = "desk-billing";
database = dbConfig;
}nix eval --file app.nix --jsonOutput:
{"appName":"desk-billing","database":{"dbHost":"db.desk.corp","dbPort":5432,"poolSize":50}}
Case 2: Staging port
nix eval --expr '(import ./database.nix { environment = "staging"; }).dbPort'Output:
5433
Missing arg:
nix eval --expr 'import ./database.nix {}'error: function 'anonymous lambda' called without required argument 'environment'
That error is the boring default working. Make the arg optional only when you mean it: { environment ? "dev" }:.
Case 3: lib.optional
Save as lib_demo.nix:
# lib_demo.nix
let
optional = cond: x: if cond then [ x ] else [ ];
features = [ "core-api" ]
++ optional true "metrics-exporter"
++ optional false "debug-profiler";
in
featuresnix eval --file lib_demo.nix --jsonOutput:
["core-api","metrics-exporter"]
With nixpkgs 26.05: lib.optional is the same idea. Do not copy half of lib into every repo; pkgs.lib once you import nixpkgs.
Case 4: Relative paths are relative to the file
import ./database.nix in app.nix looks next to app.nix, not next to your shell cwd. nix eval --file ./app.nix from /tmp still finds ./database.nix beside app.nix.
cd /tmp
nix eval --file /path/to/desk/app.nix --jsonSame JSON as Case 1.
Case 5: <nixpkgs> is ambient
nix eval --impure --expr 'builtins.nixPath'
nix eval --impure --expr '<nixpkgs>'Whatever nixpkgs= points at is not in flake.lock. Fine for a 10-second repl. Not fine for the desk flake.
# do not ship
{ pkgs ? import <nixpkgs> { } }: pkgs.helloNext parts: inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";. Circular import (a.nix imports b.nix imports a.nix) is an eval loop — pass data downward.
The trap
The trap is import <nixpkgs> {} in production modules. CI, a coworker, and a 6-month-old laptop disagree. Pin 26.05 in a lockfile.
The other trap is circular import. Eval loop. Pass data downward; do not make a cycle.
The boring rule
- Files return functions. Callers pass args.
import ./relative.nix— path relative to the file, not cwd.- No
<nixpkgs>in shipped config. Flake pinnixos-26.05. - Use
pkgs.libinstead of a private util soup — once pkgs exists. - No import cycles.
Try this
import ./database.nix { environment = "staging"; }and checkdbPort == 5433.- Move
app.nixeval to another cwd; confirm it still works. nix eval --impure --expr '<nixpkgs>'and note it is a path, not a lock.- Split a
letfrom an earlier chapter intourl.nix+import.