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!