Importing Files and Building Abstractions

Updated

September 12, 2026

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 --json

Output:

{"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
features
nix eval --file lib_demo.nix --json

Output:

["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 --json

Same 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.hello

Next 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 pin nixos-26.05.
  • Use pkgs.lib instead of a private util soup — once pkgs exists.
  • No import cycles.

Try this

  1. import ./database.nix { environment = "staging"; } and check dbPort == 5433.
  2. Move app.nix eval to another cwd; confirm it still works.
  3. nix eval --impure --expr '<nixpkgs>' and note it is a path, not a lock.
  4. Split a let from an earlier chapter into url.nix + import.