Nix Language Essentials
- Type: Tutorial
- Status: Current
- Scope: Nix syntax needed to read and make small changes in
nixos-config- Canonical sources:
flake.nix;modules/common/default-config.nix;modules/hardware/bluetooth.nix;modules/services/jellyfin.nix;hosts/electra/default.nix; Nix language basics- Last verified: Source commit
556df88494686003b1c4f20c8e0b99b5afc16a6e(2026-09-12); source HEADb2831d33132967b4c4fb69e6068b2b05fff8fdffchanges onlyflake.lock. Examples checked 2026-09-13; no Nix file changed.- Review triggers: Changes to cited modules, flake builder, commonly used language patterns, or linked upstream tutorial
Learning goal and prerequisites
Read the expressions this repository uses most: attribute sets, lists,
strings, paths, functions, let, conditionals, and library calls. First
follow the Guided Repository Tour. This is a
reading primer, not a Nix language specification; use the official Nix
language basics and language
reference for complete syntax.
Read one module from the outside in
modules/services/jellyfin.nix begins with a function argument pattern:
{ config, inputs, lib, pkgs, ... }:
The braces select named arguments from an attribute set; ... permits
additional arguments supplied by the NixOS module system. The colon starts
the function body. In this repository, config is the merged NixOS
configuration, pkgs the package set, lib the Nixpkgs library, and
inputs the flake inputs passed through specialArgs. A module does not
gain an inputs variable by naming it elsewhere in the file; it must accept
it as an argument if used.
The module then uses a let ... in expression:
let
cfg = config.nixosConfig.jellyfin;
dataDir = config.services.jellyfin.dataDir;
in
{ ... }
cfg and dataDir are names for values, not mutable variables. The final
attribute set is the function result. Dot notation selects a nested
attribute, as in config.services.jellyfin.dataDir; assignment such as
services.jellyfin.enable = true; builds a nested attribute set. An
assignment ends with ;. A list uses square brackets and whitespace, not
commas: [ "video" "render" ].
Trace conditions and composition
modules/services/jellyfin.nix declares an option with
lib.mkEnableOption and contributes its service definition only under
lib.mkIf cfg.enable. Those are library/module-system functions, not
special Nix syntax. lib.mkAfter [ dataDir ] adjusts merge ordering for a
list; it is not a shell append. The NixOS module lesson covers the merge
semantics.
In home/users/nimmo/default.nix, lib.optional (!isServer) ./plasma.nix evaluates to either a one-item list or an empty list. ++
concatenates that result with the fixed imports list. ./plasma.nix is a
relative Nix path, resolved from the containing file, not a quoted string.
When a path becomes a flake source input, Git tracking matters; a newly
created, untracked file is invisible to a Git-backed flake.
In modules/common/default-config.nix, the default flake path is a string
with interpolation:
default = "/home/${config.nixosConfig.primaryUser}/nixos-config";
${...} evaluates the expression inside the string. The same syntax can
appear within attribute selection, as in
inputs.nixpkgs-stable.legacyPackages.${pkgs.stdenv.hostPlatform.system}.jellyfin
in the Jellyfin module. That chooses a package for the current host platform;
it does not mean the host itself uses the stable Nixpkgs input as its main
channel. The host builder choice is in flake.nix.
hosts/electra/default.nix also uses with pkgs; [ coreutils gnused ] for
package names in a runtime-input list. with brings attributes into scope;
it can make origins less obvious, so read the surrounding pkgs binding
before copying a name. inherit inputs; in flake.nix is shorthand for
inputs = inputs;.
Guided check
Open modules/hardware/bluetooth.nix and explain (without deploying) which
parts are Nix syntax and which are NixOS option names. Then open
modules/services/jellyfin.nix and identify: its function arguments, two
let bindings, enable gate, one list, one interpolated attribute path, and
one relative path or explain why that module has none. Compare your result
to Adding a Module or Profile.
Common mistakes: confusing a quoted path with a Nix path, putting commas
between list items, forgetting a semicolon, assuming mkIf is a language
keyword, or treating config as state that changes sequentially. Nix
evaluates an expression; NixOS later merges module definitions and builds
the resulting system. The next lesson, Declarative Configuration, Store, and
Generations, explains that lifecycle.