Use this when setting up Nix for a development project (devShell + package build) and you care about `nix develop` being fast. Covers the zero-inputs flake.nix + npins + default.nix/shell.nix layout, sub-flakes for non-user-facing Nix, and language-specific recommendations.
Installs into .claude/skills of the current project.
Are you the author of Nix For Dev?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/juspay-nix-for-dev)
---
name: nix-for-dev
description: Use this when setting up Nix for a development project (devShell + package build) and you care about `nix develop` being fast. Covers the zero-inputs flake.nix + npins + default.nix/shell.nix layout, sub-flakes for non-user-facing Nix, and language-specific recommendations.
---
# Nix for development
A low-overhead Nix setup for dev projects: ~1s cold `nix develop`, ~0.1s warm, with a clean separation between the user-facing flake and internal Nix. Reference implementation: [juspay/kolu](https://github.com/juspay/kolu).
## Core principle: zero flake inputs
The top-level `flake.nix` declares **no `inputs`** at all. Each flake input adds ~1.5s of fetcher-cache verification on cold eval; a single `nixpkgs` input costs ~7s. With zero inputs, cold `nix develop` is ~1.0s, warm ~0.1s.
Instead, pin sources with [npins](https://github.com/andir/npins) and import them via `fetchTarball` / `callPackage` from files under `nix/`.
```nix
# flake.nix — slim, zero inputs
{
outputs = { self, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
eachSystem = f: builtins.listToAttrs (map
(system: {
name = system;
value = f (import ./nix/nixpkgs.nix { inherit system; });
})
systems);
in
{
packages = eachSystem (pkgs: {
default = import ./default.nix { inherit pkgs; };
});
devShells = eachSystem (pkgs: {
default = import ./shell.nix { inherit pkgs; };
});
};
}
```
**Do not** add `nixpkgs`, `flake-parts`, `git-hooks`, etc. as flake inputs. If a downstream consumer needs to override the pinned nixpkgs, they can override the npins source (`NPINS_OVERRIDE_nixpkgs=/path`).
## File layout
```
flake.nix # slim wrapper, zero inputs
default.nix # main package(s), pkgs ? import ./nix/nixpkgs.nix { }
shell.nix # devShell, pkgs ? import ./nix/nixpkgs.nix { }
nix/
nixpkgs.nix # imports npins source + applies overlay
overlay.nix # injects leaf packages into pkgs
env.nix # env vars shared by build + devShell + wrapper
packages/<name>/ # callPackage-style leaf packages
npins/
default.nix # generated by npins (do not edit)
sources.json # pinned source revisions
```
`default.nix` and `shell.nix` both accept `pkgs ? import ./nix/nixpkgs.nix { }` so they also work via plain `nix-build` / `nix-shell`, not just `nix develop`.
## npins workflow
```bash
npins init # creates npins/ and pins nixpkgs
npins add github nixos nixpkgs --branch nixpkgs-unstable
npins update # update all pins
npins update nixpkgs # update one
```
`nix/nixpkgs.nix`:
```nix
# Pinned nixpkgs import — managed by npins.
# To update: npins update nixpkgs
let
sources = import ../npins;
nixpkgs = import sources.nixpkgs;
in
args: nixpkgs (args // {
overlays = (args.overlays or [ ]) ++ [ (import ./overlay.nix) ];
})
```
## Leaf packages via overlay
Pure callPackage-style packages live in `nix/packages/<name>/default.nix` and are auto-injected via `nix/overlay.nix`:
```nix
# nix/overlay.nix
final: _prev: {
my-fonts = final.callPackage ./packages/fonts { };
}
```
Packages that need per-invocation arguments (commit hash, build-time env) stay in the top-level `default.nix` — overlays are for things that legitimately belong on `pkgs`.
## Shared env vars
Define a single `nix/env.nix` returning an attrset; both the build derivation and the devShell spread it into their `env`. This prevents drift between `nix build` and `nix develop`.
```nix
# nix/env.nix
{ pkgs }: {
MY_FONTS_DIR = pkgs.my-fonts;
MY_GH_BIN = "${pkgs.gh}/bin/gh";
}
```
## devShell conventions
- Use `pkgs.mkShell` directly. **Do not** introduce `flake-parts` to "structure" it.
- Use `pkgs.writeShellApplication` (not `writeShellScriptBin`) — strict mode + `runtimeInputs` validation. Always set `meta.description`.
- Run `nixpkgs-fmt` from the devShell rather than wiring up `formatter` perSystem.
- Expose extra shells via `overrideAttrs` so the base stays fast:
```nix
devShells = eachSystem (pkgs:
let default = import ./shell.nix { inherit pkgs; };
in {
inherit default;
e2e = default.overrideAttrs (prev: {
env = (prev.env or { }) // {
PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers;
};
});
});
```
`nix develop .#e2e` for the heavier shell; the default stays cold-start-fast.
## Sub-flakes for non-user-facing Nix
Module integration tests (home-manager, NixOS, Darwin) genuinely need `flake-parts`-style inputs (`home-manager`, `nix-darwin`). Keep those inputs out of the top-level flake by nesting a sub-flake under `nix/<name>/flake.nix`. CI builds it with `--override-input` pointing back at the parent:
```nix
# nix/home/example/flake.nix
{
inputs = {
self_pkg.url = "github:owner/repo"; # parent; CI passes --override-input
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, home-manager, self_pkg, ... }: {
# nixosConfigurations / checks that exercise self_pkg.homeManagerModules.default
};
}
```
Users running `nix develop` / `nix run` on the top-level flake never evaluate this graph. Only CI does.
## Language templates
### Haskell
Use [haskell-flake](https://github.com/srid/haskell-flake) via its **standalone** entry point `lib.evalHaskellProject` (not the flake-parts module). See [haskell.nixos.asia/standalone](https://haskell.nixos.asia/standalone) for the full API.
In keeping with the zero-inputs principle, pin `haskell-flake` via npins (`npins add github srid haskell-flake`) and call it from `default.nix`:
```nix
# default.nix
{ pkgs ? import ./nix/nixpkgs.nix { } }:
let
sources = import ./npins;
haskell-flake = import sources.haskell-flake;
project = (haskell-flake.lib { inherit pkgs; }).evalHaskellProject {
projectRoot = ./.;
modules = [{
settings.mypackage.haddock = false;
devShell.tools = hp: { inherit (hp) fourmolu; };
}];
};
in
project.packages.mypackage.package
```
Wire `project.devShell` into `shell.nix` the same way. If you need flake-parts and `nixos-unified` autowiring (multi-package projects, fully wired checks), see the `nix-haskell` skill — it uses [haskell-template](https://github.com/srid/haskell-template) and trades startup time for ergonomics.
### TypeScript / pnpm
See [`nix-typescript`](../nix-typescript/SKILL.md) for `fetchPnpmDeps` and hash management.
### Dev services
For multi-process dev environments (server + watcher + db), use [`process-compose-flake`](https://github.com/Platonic-Systems/process-compose-flake) and [`services-flake`](https://github.com/juspay/services-flake) via their **standalone** entry points (no flake-parts needed). Both flakes have zero inputs themselves, so pinning them via npins keeps the top-level `flake.nix` zero-input too:
- `process-compose-flake` exposes [`lib.evalModules` / `lib.makeProcessCompose`](https://github.com/Platonic-Systems/process-compose-flake/blob/main/nix/lib.nix) for module evaluation outside flake-parts.
- `services-flake` exposes `processComposeModules.default` (a path) — pass it as a module to `evalModules`.
```nix
# shell.nix
{ pkgs ? import ./nix/nixpkgs.nix { } }:
let
sources = import ./npins;
pcLib = import "${sources.process-compose-flake}/nix/lib.nix" { inherit pkgs; };
servicesMod = pcLib.evalModules {
modules = [
"${sources.services-flake}/nix/process-compose"
{ services.redis."r1".enable = true; }
];
};
in
pkgs.mkShell {
inputsFrom = [ servicesMod.config.services.outputs.devShell ];
}
```
Reference: [`services-flake/example/without-flake-parts`](https://github.com/juspay/services-flake/tree/main/example/without-flake-parts) and [`doc/without-flake-parts.md`](https://github.com/juspay/services-flake/blob/main/doc/without-flake-parts.md).
## Looking things up
When the `nixos` MCP server is available, use it to look up package names, versions, and NixOS / Home Manager options instead of guessing them.
## Companion docs
- [`nix-perf`](../nix-perf/SKILL.md) — diagnosing slow `nix develop` / `nix flake archive`
- [`nix-justfile`](../nix-justfile/SKILL.md) — justfile recipe conventions for Nix projects
- [`nix-typescript`](../nix-typescript/SKILL.md) — pnpm + Nix conventions
- [`nix-haskell`](../nix-haskell/SKILL.md) — flake-parts + haskell-template variant (when ergonomics > cold start)
- [juspay/kolu](https://github.com/juspay/kolu) — full reference implementation