Building a Maintainable Nix Configuration: A Modular Approach

Building a Maintainable Nix Configuration: A Modular Approach
How I transformed a 663-line monolithic configuration into a clean, modular system—and why you might want to do the same
TL;DR: This guide shows you how to restructure a monolithic Nix configuration (663+ lines) into focused, maintainable modules. You’ll learn when to modularize, how to do it safely, and patterns for organizing packages, shell configuration, git settings, and more.
Introduction
If you’ve been using Nix for managing your development environment, you’ve probably experienced this progression:
Day 1: “This is amazing! One file to manage everything!”
Week 4: “Hmm, this file is getting long, but it’s still manageable.”
Month 3: “Where did I put that git configuration? Ctrl+F time…”
Month 6: “I’m afraid to change anything because I don’t remember what depends on what.”
Sound familiar?
What is Home Manager?
The Journey: From Simple to Complex to Simplified
The Beginning: Simplicity
Like most people, I started with a simple home.nix
:
# home.nix - Day 1
{ config, pkgs, ... }: {
home.packages = with pkgs; [ git vim curl ];
programs.zsh.enable = true;
home.username = "me";
home.homeDirectory = "/Users/me";
}
Twenty lines. Clean. Understandable. Perfect.
The Middle: Complexity
Fast forward three months. I’ve discovered the power of Nix. I’m managing everything:
- 150+ packages across multiple categories
- Custom shell functions and aliases
- Git configuration with 20+ aliases
- Neovim configuration with plugins
- Tmux setup with custom keybindings
- Custom development scripts
The result? My home.nix
is now 663 lines. Scrolling through it takes work. Finding things requires searching. Making changes feels risky.
Common pitfall: The “just one more thing” syndrome. Each addition seems harmless, but collectively they create an unmaintainable monolith. By line 300, you’ve lost the plot.
The “Aha” Moment
The breaking point came when I wanted to share my git configuration with a colleague. I had to:
- Open the 663-line file
- Scroll to find the git section (around line 345)
- Copy about 80 lines and remove personal details
- Explain context that wasn’t obvious
Then they asked: “What about your shell aliases?”
That was it. Time to restructure.
The End: Structured Simplicity
After the restructure, sharing my git configuration became:
“Check out modules/home/git.nix
”
The file is self-contained, documented, and under 100 lines. Everything became easier:
- Finding settings: “Git stuff? Look in
git.nix
” - Making changes: Edit one focused file
- Understanding history: Git diffs show exactly what changed
- Experimenting: Changes are isolated and safe
Understanding Modular Configuration
What Is Modular Configuration?
Instead of one large file containing everything, you split your configuration into multiple files, each handling a specific concern.
Monolithic: 663 lines, everything mixed
Modular: Focused, self-contained files
Why This Matters
1. Cognitive Load Reduction
Before: Open 663-line file to change git. See packages, shell, editors, everything. Your brain must process and ignore 583 irrelevant lines.
After: Open git.nix
. 80 lines. All git. Nothing else. Your brain focuses entirely on git.
2. Git History Becomes Useful
modified: modules/home/shell.nix
@@ -45,1 +45,1 @@
- gs = "git status";
+ gs = "git status -s";
Perfect clarity. You immediately know it’s shell-related.
3. Fearless Experimentation
Edit shell.nix
. If something breaks, you know it’s shell-related. Rollback: git checkout modules/home/shell.nix
.
Pro tip: Create feature branches for experiments. Try new configurations without fear. Merge what works, discard what doesn’t.
When NOT to Be Modular
Use a monolithic approach when:
- You’re learning: Start simple, modularize later
- It’s tiny: If your entire config is 50 lines, don’t split it
- It’s temporary: Experimental setups don’t need structure
Rule of thumb: Modularize when your configuration exceeds 150 lines and handles multiple concerns (packages + shell + git + etc.).
The Structure: A Deep Dive
Directory Organization
nix-config/
├── flake.nix # Entry point
├── darwin-configuration.nix # System-level config (macOS)
│
├── modules/home/ # Home Manager modules
│ ├── default.nix # Imports all modules
│ ├── packages.nix # Package installations
│ ├── shell.nix # Shell configuration
│ ├── git.nix # Git configuration
│ ├── editors.nix # Editor configurations
│ └── development.nix # Custom scripts
│
└── scripts/
└── rebuild.sh # Rebuild helper
Module Design Principles
Each module follows these principles:
1. Single Responsibility - One concern per file
2. Self-Contained - Understandable independently
3. Well-Documented - Clear comments explaining choices
# modules/home/git.nix
# ============================================================================
# GIT CONFIGURATION
# ============================================================================
# This file configures Git version control settings, including identity,
# preferences, and aliases.
# ============================================================================
{ config, pkgs, ... }:
{
programs.git = {
enable = true;
userName = "Alice Smith";
userEmail = "alice@example.com";
# Credential Management
# Store credentials securely using macOS keychain
credential = {
helper = "osxkeychain"; # macOS
# helper = "libsecret"; # Linux alternative
};
};
}
The Import Pattern
# modules/home/default.nix
{ config, pkgs, ... }:
{
imports = [
./packages.nix # Load package configuration
./shell.nix # Load shell configuration
./git.nix # Load git configuration
./editors.nix # Load editor configuration
./development.nix # Load custom scripts
];
# Basic user information
home.username = "alice";
home.homeDirectory = "/Users/alice";
home.stateVersion = "24.05";
}
Key insight: Add or remove modules by changing the import list. Want to disable editors
temporarily? Comment out ./editors.nix
. Want to add Kubernetes tools? Create ./kubernetes.nix
and add it to imports.
Implementation Patterns
Package Organization Strategy
Organize packages by category, not alphabetically:
# modules/home/packages.nix
{ config, pkgs, ... }:
{
home.packages = with pkgs; [
# =========================================================================
# MODERN CLI TOOLS
# =========================================================================
bat # Better cat with syntax highlighting
eza # Better ls with colors and icons
fd # Better find - faster and easier
ripgrep # Better grep - blazingly fast
# =========================================================================
# PROGRAMMING LANGUAGES
# =========================================================================
nodejs_22 # Node.js LTS version
python3 # Python 3
go # Go language
# =========================================================================
# CONTAINER TOOLS
# =========================================================================
docker # Docker CLI
docker-compose # Multi-container applications
kubectl # Kubernetes CLI
];
}
Why categories work: Easier to find, clearer intent, natural organization.
Shell Configuration Patterns
# modules/home/shell.nix
{ config, pkgs, ... }:
{
programs.zsh = {
enable = true;
enableCompletion = true;
oh-my-zsh = {
enable = true;
theme = "robbyrussell";
plugins = [ "git" "docker" "kubectl" ];
};
# Aliases organized by category
shellAliases = {
# Git shortcuts
gs = "git status";
ga = "git add";
gc = "git commit";
# Docker shortcuts
d = "docker";
dc = "docker-compose";
# Navigation shortcuts
".." = "cd ..";
"..." = "cd ../..";
};
# Custom functions
initExtra = ''
mkcd() {
mkdir -p "$1" && cd "$1"
}
export EDITOR="nvim"
export VISUAL="$EDITOR"
'';
};
}
Custom Script Integration
One of the most powerful features of Nix is creating custom commands:
# modules/home/development.nix
{ config, pkgs, ... }:
{
home.packages = with pkgs; [
# Create custom commands available in PATH
(writeShellScriptBin "sysinfo" ''
#!/usr/bin/env bash
echo "System: $(uname -s)"
echo "Architecture: $(uname -m)"
echo "User: $(whoami)"
echo "Home: $HOME"
'')
(writeShellScriptBin "quick-commit" ''
#!/usr/bin/env bash
git add .
git commit -m "$1"
git push
'')
];
}
After rebuilding, these commands are available:
$ sysinfo
System: Darwin
Architecture: arm64
User: alice
Home: /Users/alice
Practical Migration Guide
Migration Flow
Step-by-Step Migration
Before you begin: Commit your current working configuration to git. This is your safety net!
Step 1: Identify Module Boundaries
Look at your monolithic configuration and mark distinct sections:
- Packages:
home.packages =
- Shell:
programs.zsh =
orprograms.bash =
- Git:
programs.git =
- Editors:
programs.neovim =
,programs.vim =
- Custom scripts:
writeShellScriptBin
Step 2: Extract One Module at a Time
Start with the simplest (usually git):
mkdir -p modules/home
vim modules/home/git.nix
Add boilerplate and copy your git config:
{ config, pkgs, ... }:
{
programs.git = {
# ... your existing git config
};
}
Step 3: Test Immediately
# Create default.nix with import
echo '{ imports = [ ./git.nix ]; }' > modules/home/default.nix
# Test build
home-manager switch --flake .
Crucial: Test after each extraction. Don’t extract everything and then test. If something breaks, you’ll know exactly which module caused it.
Step 4: Repeat for Remaining Modules
Extract one at a time: packages.nix
, shell.nix
, editors.nix
, development.nix
Step 5: Document and Optimize
- Add header comments to each module
- Explain non-obvious choices
- Remove duplication
- Clean up commented code
Rollback Strategies
If something breaks:
Option 1: Git rollback
git checkout HEAD -- modules/home/shell.nix
home-manager switch --flake .
Option 2: Home Manager rollback
home-manager generations # List previous builds
home-manager switch --flake . --rollback
Decision Tree: Should You Modularize?
Lessons Learned
After months of using modular configuration:
What Worked Well
1. Category-based organization: Organizing packages by category (not alphabetically) made finding things intuitive.
2. Extensive commenting: Future me is grateful past me added comments explaining why things are configured a certain way.
3. Helper scripts: The rebuild.sh
script made applying changes effortless.
Unexpected Challenges
1. Over-modularization: Initially, I created too many tiny modules. Balance is key—modules should be substantial enough to justify separate files.
Avoid: Creating a separate module for every single program. Group related configurations (e.g., all editors together).
2. Cross-module dependencies: Sometimes configuration in one module needs to reference another. The config
parameter solves this, but it adds complexity.
Recommendations
Do:
- Start when your config exceeds 150-200 lines
- Migrate incrementally, testing after each step
- Document as you go
- Use Git religiously
- Share your configuration
Don’t:
- Modularize too early (premature optimization)
- Create too many tiny modules (balance is key)
- Forget to test after each change
- Skip documentation (future you will regret it)
Conclusion
Modular Nix configuration isn’t just about organization—it’s about making your development environment manageable, understandable, and maintainable.
Key Takeaways
- Modular configuration reduces cognitive load - Focused files are easier to understand
- Git history becomes useful - Clear diffs show exactly what changed
- Experimentation becomes safe - Isolated changes mean limited blast radius
- Sharing becomes effortless - Self-contained modules are easy to share
- Maintenance becomes sustainable - Well-structured configuration grows with you
When to Modularize
- Configuration exceeds 150-200 lines
- Managing multiple concerns (packages, shell, git, etc.)
- Want to share parts of your configuration
- Need clear change history
- Planning long-term maintenance
Your Turn: Whether you’re just starting with Nix or have a monolithic configuration that needs restructuring, the patterns in this article can help. Start small, migrate incrementally, and most importantly—make your configuration work for you, not the other way around.
Your future self will thank you.
Resources
- Nix Manual: https://nixos.org/manual/nix/stable/
- Home Manager Manual: https://nix-community.github.io/home-manager/
- Nix Community: https://discourse.nixos.org/
- Nix Flakes Guide: https://nixos.wiki/wiki/Flakes
Happy configuring! 🚀
Comments