Golang Template Request Handler Middleware

In this post I'm going to describe how I write route handlers for server-side rendered apps, such as rondoBB.

I'm going to use the html/template, which is provided by Go's standard library. Sometimes it can be slow, but we're not going to focus on speed for now.

Instead of using *template.Template directly in every request handler, templates will be parsed in one place, and used as needed in the request handlers.

Additionally, it would be useful to optionally be able to set HTML tags like <title> and <meta og:description>.

The goal is to have a custom function type that we can use as a request handler. For example, let's define the following function:

1type PageHandler func(
2	w http.ResponseWriter,
3	r *http.Request,
4) (templateName string, data interface{}, err error)

Let's start from scratch:

1mkdir go-template-render-example
2cd go-template-render-example
3git init
4go mod init github.com/jeremija/go-template-render-example
5mkdir -p templates/ routes/ render/

Next, we'll define some templates:

templates/base.html

 1{{define "header"}}
 2<!DOCTYPE html>
 3<html>
 4<head>
 5  <meta property="og:title" content="{{.Meta.Title}}">
 6  <meta property="og:description" content="{{.Meta.Description}}">
 7  {{if .Meta.ImageURL}}
 8    <meta property="og:image" content="{{.Meta.ImageURL}}">
 9    <meta name="twitter:image" content="{{.Meta.ImageURL}}">
10  {{end}}
11  <meta property="og:url" content="{{.Meta.URL}}">
12  <meta name="twitter:title" content="{{.Meta.Title}}">
13  <meta name="twitter:description" content="{{.Meta.Description}}">
14</head>
15<body>
16{{end}}
17
18{{define "footer"}}
19</body>
20</html>
21{{end}}

templates/index.html

1{{template "header" .}}
2<h1>{{.Meta.Title}}</h1>
3<p>{{.Data.Value}}</p>
4{{template "footer" .}}

First, let's implement the functionality to add some data to request context:

Our simple template render can look like this:

render/context.go

 1package render
 2
 3import (
 4	"context"
 5	"net/http"
 6)
 7
 8type Meta struct {
 9	Title       string
10	ImageURL    string
11	URL         string
12	Description string
13}
14
15type MetaContext map[string]interface{}
16
17const (
18	MetaContextKey = "MetaContext"
19
20	MetaKey = "Meta"
21)
22
23func WithMetaContext(r *http.Request) *http.Request {
24	c := MetaContext{
25		MetaKey: Meta{},
26	}
27	ctx := context.WithValue(r.Context(), MetaContextKey, c)
28	return r.WithContext(ctx)
29}
30
31func getParams(r *http.Request) MetaContext {
32	params, ok := r.Context().Value(MetaContextKey).(MetaContext)
33	if !ok {
34		panic("No render params set in request context")
35	}
36	return params
37}
38
39func SetParam(r *http.Request, key string, value interface{}) {
40	getParams(r)[key] = value
41}
42
43func GetParam(r *http.Request, key string) (interface{}, bool) {
44	item, ok := getParams(r)[key]
45	return item, ok
46}
47
48func SetMeta(r *http.Request, meta Meta) {
49	SetParam(r, MetaKey, meta)
50}
51
52func GetMeta(r *http.Request) (meta Meta) {
53	value, ok := GetParam(r, MetaKey)
54	if !ok {
55		return
56	}
57	v, ok := value.(Meta)
58	if !ok {
59		return
60	}
61	meta = v
62	return
63}

Next, we implement our Render handler:

render/render.go

 1package render
 2
 3import (
 4	"bytes"
 5	"fmt"
 6	"html/template"
 7	"io"
 8	"net/http"
 9)
10
11type PageHandler func(w http.ResponseWriter, r *http.Request) (templateName string, data interface{}, err error)
12
13const (
14	ErrRenderingPage = "Error rendering page"
15)
16
17var templates = map[string]*template.Template{}
18
19func Register(templateName string, template *template.Template) {
20	templates[templateName] = template
21}
22
23func Render(h PageHandler) http.HandlerFunc {
24	fn := func(w http.ResponseWriter, r *http.Request) {
25		r = WithMetaContext(r)
26		templateName, data, err := h(w, r)
27		if err != nil {
28			fmt.Printf("Error handling request: %s\n", err)
29			http.Error(w, ErrRenderingPage, http.StatusInternalServerError)
30			return
31		}
32
33		template, ok := templates[templateName]
34		if !ok {
35			fmt.Printf("No template not found: %s\n", templateName)
36			http.Error(w, ErrRenderingPage, http.StatusInternalServerError)
37			return
38		}
39
40		meta := GetMeta(r)
41		if meta.URL == "" {
42			meta.URL = r.URL.EscapedPath()
43		}
44
45		dataMap := map[string]interface{}{
46			"Data": data,
47			"Meta": meta,
48		}
49
50		// TODO write to buffer pool instead of directly to response
51		var b bytes.Buffer
52		err = template.Execute(&b, dataMap)
53		if err != nil {
54			fmt.Printf("Error executing template: %s\n", err)
55			http.Error(w, ErrRenderingPage, http.StatusInternalServerError)
56			return
57		}
58		io.Copy(w, &b)
59	}
60	return http.HandlerFunc(fn)
61}

The next step is to implement the main route handler. This is simple:

routes/index.go

 1package routes
 2
 3import (
 4	"net/http"
 5
 6	"github.com/jeremija/go-template-render-example/render"
 7)
 8
 9const IndexTemplate = "index.html"
10const IndexPath = "/index"
11
12type IndexData struct {
13	Value int
14}
15
16func GetIndex(w http.ResponseWriter, r *http.Request) (string, interface{}, error) {
17	render.SetMeta(r, render.Meta{
18		Title:       "My Title",
19		Description: "My Description",
20	})
21	return IndexTemplate, IndexData{Value: 5}, nil
22}

Finally, a HTTP server can be created:

 1package main
 2
 3import (
 4	"html/template"
 5	"net/http"
 6
 7	"github.com/jeremija/go-template-render-example/render"
 8	"github.com/jeremija/go-template-render-example/routes"
 9)
10
11func Configure() http.Handler {
12	render.Register(routes.IndexTemplate,
13		template.Must(template.ParseFiles("templates/index.html", "templates/base.html")),
14	)
15
16	mux := http.NewServeMux()
17	mux.HandleFunc(routes.IndexPath, render.Render(routes.GetIndex))
18	return mux
19}
20
21func main() {
22	err := http.ListenAndServe(":3000", Configure())
23	if err != nil {
24		panic(err)
25	}
26}

Note that this code can be easily reused since template-specific stuff is left out of the request handler. We can have easliy write middleware that marshalls our data as JSON and use it as:

1mux.HandleFunc(routes.APIIndexPath, asJSON(routes.GetIndex))

This code is available in a Git repository here.

Thank you for reading - feel free to leave a comment below!

Please to comment

Markdown formatting is supported in comments too:

This is a quote

0
Please to comment