web patterns from nirvash
motivations
Web programming can be enormously complicated or very simple. You can have complex data and control flow relationships to the point where you don't even know where to start, you can have a static array of pages served up cold, and everything in-between.
As I've mentioned recently, I've been trying to find a balance between these extremes to inform my style of web application development in the future. While I've got lots of professional experience working with all sorts of heavyweight web technologies (React, Django, Angular, ASP.NET and .NET Core, etc.), my personal web projects have tended to be on the static side. For one I don't need complexity in terms of features and behavior, and in addition I strive for elegance in implementation. But I want to have the ability to churn out web projects for clients and friends that I can be proud of - projects that are both robust in their features and elegant/spartan in their implementation, without relying on needless Javascript or heavy stacks on the backend.
That was how I came to create quartzgun, and as a proof of concept to show it could be used to build applications, as well as to provide an accessible CMS, I built nirvash. It's been a really fun ride so far, and an awesome learning experience, so I wanted to share what I learned.
what turns the wheels
quartzgun is minimal but it provides all the tools you need to get started with an application - authentication/authorization, routing, CSRF protection, and wrappers around templates for HTML, JSON and XML. With that plus a handful of extra middleware I ended up writing more specific to nirvash, all the networking logic is isolated and I could focus on the business logic.
I've come to really appreciate the batteries-included nature of Go, especially as it brings a clean explicitness from C that combines with this ease of use. The basics of nirvash in the business logic layer are pretty much an INI file parser/writer, a command line parser, the Adapter
interface, and the FileManager
interface. The first two were more or less complete in a couple days.
As I started work on the Adapter
, I simultaneously started work on the presentation layer. Aside from the net/http
library that powers quartzgun, the pillars that nirvash stands on are Go's excellent os
, os/exec
, path/filepath
and strings
libraries on the business logic side, and the excellent and ubiquitous html/template
library on the presentation side (leveraged by quartzgun in its renderer.Template
endpoint).
I found it incredibly refreshing to be able to just carry out my business logic in a straightforward way (with the result, err
return style idiomatic of Go helping me perform careful error checking and disaster prevention), return my data, and use it unceremoniously in my templates. Implementing the FileManager
interface was much the same, and I was able to get everything working in a piecemeal fashion by going in a data-first direction by getting my data into and out of the filesystem (both the EurekaAdapter
and unsurprisingly the FileManager
primarily just juggle files around) and then building the UI around the data. I've read that the only good boundary in Go is a network boundary, but I found that the interop here was very smooth using the filesystem as a boundary.
the challenges
There were a few unexpected hiccups that ended up informing my design choices -- design choices that I don't regret.
data flow
The first was the question how do I get my data into and out of my templates?
From the early days Go provided the FuncMap
technique to inject functions into your templates, by attaching a map of strings to functions to your template. But I find this awkward to look at, let alone use, and modern Go provides a better way: Context
.
The Context
is essentially a property of the http.Request
that we can use to store data as we pass the Request
through our application. By default, the namespace of the template (.
) is the Request
we are responding to. So if we just stuff any data or functions we want into the Context
, we can easily access it in our templates. What's more, we already have an abundance of data readily accessible - we can get almost anything with the Slug
URL parameter and the methods on our Adapter
or FileManager
. So why not just add the whole Adapter
or FileManager
object to the Context
?
And there you have the two beautiful pieces of middleware:
func WithAdapter(http.Handler, core.Adapter) http.Handler func WithFileManager(http.Handler, core.FileManager) http.Handler
We just inject the object into the Context
and then pass the request to the next piece of middleware (probably the renderer.Template
endpoint). In the template, we can just retrieve the slug and the object from the Context
right at the start like so:
{{ $slug := ((.Context).Value "params").Slug }} {{ $file := ((.Context).Value "file-manager").GetFileData $slug }} {{ $csrfToken := (.Context).Value "csrfToken" }}
And then we can keep our template logic very simple using the data we retrieved.
error handling
The other major question was how to handle errors in the UI?
The idiomatic way in Go to do error handling is to return two values from a function, one that typially contains the result and one that contains the error. This is great but you can't do this in templates! The reason is that in a template, the only way to declare two variables as the result of a function is with the range
iterator. In any other case, you can only declare one variable to carry the return value of a function at a time, and if that function would return another object as an error (or anything else), if this second return value is non-nil the template terminates its rendering/interpretation!
Obviously we would prefer to be able to present data on errors appropriately to the user instead of leaving them with a blank white page. So my solution was just to craft my datatypes carefully - any datatype being passed to a template (if not just an Error
type to begin with), carries an Error
property which is a string. So instead of returning a second Error
object and halting execution of the template, we can just return an object with the Error
property filled out.
Like the solution to our other problem, this simplifies our templates and error handling. We don't have to catch panic
s and redirect; we don't have to do any magic. We can generally just split each template into two parts - if there's a nonempty Error
in our data object, format it on the page more or less by itself. Otherwise, we can look at all the other properties in the data object and render the template as usual.
extensibility
One of my primary goals with nirvash was for it to be extensible - if somebody wants to back it with a different SSG like Hugo or Jekyll, they can write the Adapter
in a couple hours at most and be up and running. In addition to page operations, Adapter
s have Config
and BuildOptions
which can be very powerful and make using nirvash much more streamlined than running the SSG straight from the command line. For example, the EurekaAdapter
reads the Config
from the config.h
and separates each macro into strongly typed form inputs. The build options also give you a large range of freedom in writing an adapter. Any choice in the build process can be abstracted into a field to add to the build form, and custom behavior corresponding to those values can be programmed into the Build
function in the Adapter
. Right now the only build option for the Well that was fast.EurekaAdapter
is to add a twtxt
entry, but I plan to add a few more down the line to manage thumbnails and remove the most recent twtxt
entry as well.
the experience
I liked the experience of working on my website already with the changes to eureka's markup language earlier this year, but the polish that nirvash gives me is refreshing and I may use it as much if not more than my text editor and rsync
. If anybody wants to try using this with their favorite SSG, let me know and I can give a go at writing an Adapter
for it, or take the plunge and give it a shot yourself! :)