Setting Up Templ Using Nix Flakes
Templ is an HTML templating package that compiles to Go code,
and its approach should be familiar to anyone who has used React or a similar component-driven
JavaScript framework. One requirement of using this library is that your templates are written
as .templ
files, and generate Go code for runtime, which should also be familiar to JavaScript
developers ;-).
Since I’ve started kicking the tires on Templ with Nix flakes, I figured this would make for a great tutorial topic.
Setting Up The Flake
Let’s start with a basic flake that can run a Go application.
{
description = "A simple Go application";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils, ...}:
utils.lib.eachDefaultSystem(system:
let pkgs = import nixpkgs { inherit system };
in {
devShell = with pkgs; mkShell {
buildInputs = [
go
gopls
gotools
go-tools
];
};
})
}
If you’re new to Nix, I’ll walk through what’s going on here:
- 2
inputs
are defined,nixpkgs
, which points to the unstable branch andutils
, which points to a utility library for working with flakes. - The
outputs
attribute then takesself
, and the 2 inputs we declared above,nixpkgs
andutils
. utils
has a helper function that will iterate over each default system (x86_64-linux, darwin, etc) and passessystem
as an argument to the handler.- A
pkgs
binding is created which importsnixpkgs
and inherits the passed system’s attributes (you can think of this like an object spread operator in JavaScript,{...system}
). - The resulting object returned has a
devShell
attribute where the packages needed for this project are declared on thebuildInputs
. In this case it’s Go, its ALanguage Server and some tooling.
Installing Templ
And as luck would have it, Templ provides an official flake. Let’s install by adding
it to the inputs
attribute.
inputs = {
# ... other inputs
templ.url = "github:a-h/templ";
};
Pass the input attribute to outputs
.
outputs = { self, nixpkgs, utils, templ }:
Create an function which takes system
as an argument and returns the templ
binary, and bind it to the the return value.
let
pkgs = import nixpkgs { inherit system; };
# Function that takes a system and returns a path to the templ package
templOverlay = system: templ.packages.${system}.templ;
Now let’s add the Templ package to the buildInputs
so it can be used in the Go application.
devShell = with pkgs; mkShell {
buildInputs = [
# Run the overlay function which passes in the current system below the packages
(templOverlay system)
];
};
As far as development is concerned, this is all that is needed. To get started this is how you would get the flake up and running in your terminal. Note how the first command tells us that we currently don’t have Go installed on our system.
$ go version
The program 'go' is not in your PATH. It is provided by several packages.
You can make it available in an ephemeral shell by typing one of the following:
nix-shell -p gccgo
nix-shell -p gccgo12
nix-shell -p gccgo13
$ nix develop
installing...when done enter a nix shell
> go version
go version go1.22.2 linux/amd64
> templ version
v0.2.668
> templ generate
(✓) Complete [ updates=1 duration=2.093097ms ]
> go run .
Listening on port :3000
# Ctrl-C
> exit
$ go version
The program 'go' is not in your PATH....
Defining a package
Now that we have the ability to develop in an isolated Go environment, let’s set up
the build step. What’s nice about how Nix flakes handles this is that to compile the
application I don’t need to install all the dependencies. Running nix build
will
download everything needed to compile my project without needing to pollute my dev machine.
Let’s start by adding a packages
attribute on the flake. It’ll contain:
- The name of the package name,
pname
, which matches the attribute name. - The version number.
- The src directory, which is useful if you have your files in a subdirectory.
- The vendor hash for verification.
- A
preBuild
action which compiles all the.templ
files to.go
before running the main build task. - Lastly create a
defaultPackage
attribute that points to thepackages.templ-app
attribute.
packages = {
templ-app = pkgs.buildGoModule {
pname = "templ-app";
version = "0.1.0";
src = ./.;
vendorHash = pkgs.lib.fakeHash;
};
# Prebuild all templates before compiling
preBuild = ''
${templOverlay system}/bin/templ generate
'';
};
defaultPackage = self.packages.${system}.templ-app;
Compiling An Executable
The first time nix build
is ran, use vendorHash = pkgs.lib.fakeHash;
.
Here I followed the notes outlined in the official Nix flakes
Go example project :
This hash locks the dependencies of this package. It is necessary because of how Go requires network access to resolve VCS. See https://www.tweag.io/blog/2021-03-04-gomod2nix/ for details. Normally one can build with a fake hash and rely on native Go mechanisms to tell you what the hash should be or determine what it should be “out-of-band” with other tooling (eg. gomod2nix). To begin with it is recommended to set this, but one must remember to bump this hash when your dependencies change.
Essentially this states that the first nix build
run will return an error with
an expected and recieved vendor hashs. To resolve this, simply copy the expected hash
and replace the fakeHash
variable. It also notes that this should be updated every
time your dependencies change. Or at least that’s how best I understand it from my
research for this tutorial, I’ll be sure to update if I find more clarity on how it works.
So to wrap it all up, let’s build the finished executable.
$ nix build
$ ./result/bin/nix-templ
Listening on :3000
Next Steps
From here the executable can be installed globally as $ nix-templ
or
distributed as a package. Hopefully this tutorial gets you started with Go and
its great ecosystem of packages running in a Nix flake development environment.