blackhole://nilFM

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 panics 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, Adapters 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 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. Well that was fast.

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! :)