DEV Community

Cover image for Setup nested HTML template in Go Echo web framework
Yuen Ying Kit
Yuen Ying Kit

Posted on • Edited on

Setup nested HTML template in Go Echo web framework

Originally posted on Boatswain Blog.


Echo is a lightweight but complete web framework in Go for building RESTful API. It is fast and includes a bunch of middleware for handling the whole HTTP request-response cycle. For the rendering part, it works with any template engine but i pick the standard html/template package for the purpose of simplicity. And at the end of this article, a nested template Echo project setup is demonstrated.

If you already have an idea on how Echo works, jump to the Using nested template section.

A basic Echo project setup

Create the project folder under proper $GOPATH

The complete project code is hosted on GitLab so we first create the project folder $GOPATH/src/gitlab.com/ykyuen/golang-echo-template-example.

Create the main.go

Inside the newly created folder, let's just copy the hello world example from the Echo official site and create the main.go.

main.go



package main

import (
  "net/http"

  "github.com/labstack/echo"
)

func main() {
  e := echo.New()
  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
  })
  e.Logger.Fatal(e.Start(":1323"))
}


Enter fullscreen mode Exit fullscreen mode

Download the Echo package using dep

Simply run dep init if dep is installed. You can refer to this post for more information about dep.

Or run go get github.com/labstack/echo to download the Echo package in $GOPATH.

Run the hello world

Start the application by go run main.go and then visit http://localhost:1323 thru browser or the curl command.

Start the Echo server.

Send a request and get the hello world.

Return a JSON response

When building a RESTful API, it is more likely that the client wants to receive and JSON response instead of a string. Let's write some Go code in main.go.

main.go



package main

import (
  "net/http"

  "github.com/labstack/echo"
)

func main() {
  e := echo.New()

  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
  })

  e.GET("/json", func(c echo.Context) error {
    return c.JSONBlob(
      http.StatusOK,
      []byte(`{ "id": "1", "msg": "Hello, Boatswain!" }`),
    )
  })

  e.Logger.Fatal(e.Start(":1323"))
}


Enter fullscreen mode Exit fullscreen mode

Return an HTML

Similar to returning a JSON object, we just need to call another method in the return statement.

main.go



package main

import (
  "net/http"

  "github.com/labstack/echo"
)

func main() {
  e := echo.New()

  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
  })

  e.GET("/json", func(c echo.Context) error {
    return c.JSONBlob(
      http.StatusOK,
      []byte(`{ "id": "1", "msg": "Hello, Boatswain!" }`),
    )
  })

  e.GET("/html", func(c echo.Context) error {
    return c.HTML(
      http.StatusOK,
      "<h1>Hello, Boatswain!</h1>",
    )
  })

  e.Logger.Fatal(e.Start(":1323"))
}


Enter fullscreen mode Exit fullscreen mode

The above are just two simple examples, Echo has a few more convenient ways to return JSON and HTML. For details please refer to the documentation.

Render HTML using template engine

As mentioned at the very beginning, we could implement template engine when returning the HTTP response but before that, let's restructure the project as follow.



golang-echo-template-example/
  ├── handler/             # folder of request handlers
  │   └── home_handler.go
  ├── vendor/              # dependencies managed by dep
  │   ├── github.com/*
  │   └── golang.org/*
  ├── view/                # folder of html templates
  │   └── home.html
  ├── Gopkg.lock           # dep config file
  ├── Gopkg.toml           # dep config file
  └── main.go              # programme entrypoint


Enter fullscreen mode Exit fullscreen mode

main.go



package main

import (
  "html/template"
  "io"

  "github.com/labstack/echo"

  "gitlab.com/ykyuen/golang-echo-template-example/handler"
)

// Define the template registry struct
type TemplateRegistry struct {
  templates *template.Template
}

// Implement e.Renderer interface
func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
  return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
  // Echo instance
  e := echo.New()

  // Instantiate a template registry and register all html files inside the view folder
  e.Renderer = &TemplateRegistry{
    templates: template.Must(template.ParseGlob("view/*.html")),
  }

  // Route => handler
  e.GET("/", handler.HomeHandler)

  // Start the Echo server
  e.Logger.Fatal(e.Start(":1323"))
}


Enter fullscreen mode Exit fullscreen mode

In this main.go, we define a type called TemplateRegistry and implement the Renderer interface. A Renderer is a simple interface which wraps the Render() function. Inside a TemplateRegistry instance, it has a templates field containing all the templates needed for the Echo server to render html response and this is configured in the main() flow.

On the other hand, we define the HomeHandler in order to keep the logic in separate file.

handler/home_handler.go



package handler

import (
  "net/http"

  "github.com/labstack/echo"
)

func HomeHandler(c echo.Context) error {
  // Please note the the second parameter "home.html" is the template name and should
  // be equal to the value stated in the {{ define }} statement in "view/home.html"
  return c.Render(http.StatusOK, "home.html", map[string]interface{}{
    "name": "HOME",
    "msg": "Hello, Boatswain!",
  })
}


Enter fullscreen mode Exit fullscreen mode

When the c.Render() is invoked, it executes the template which is already set in our TemplateRegistry instance as stated in main.go. The three paramaters are

  1. HTTP response code
  2. The template name
  3. The data object which could be used in the template

view/home.html



{{define "home.html"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>Boatswain Blog | {{index . "name"}}</title>
    </head>
    <body>
      <h1>{{index . "msg"}}</h1>
    </body>
  </html>
{{end}}


Enter fullscreen mode Exit fullscreen mode

This above template is named as home.html as stated in the define statement and it could read the name and msg strings from c.Render() for the <title> and <h1> tags.

Using nested template

In the above setup, every HTML template has a complete set of HTML code and many of them are duplicated. Using nested template makes it easier to maintain the project. Originally the templates field in the TemplateRegistry contains all the templates files. In the new setup, we make it into an map field and each item is a single set of template files for a particular HTML page.

We add a few files to the project and it should look like this.



golang-echo-template-example/
  ├── handler/              # folder of request handlers
  │   ├── home_handler.go   # handler for home page
  │   └── about_handler.go  # handler for about page
  ├── vendor/               # dependencies managed by dep
  │   ├── github.com/*
  │   └── golang.org/*
  ├── view/                 # folder of html templates
  │   ├── base.html         # base layout template
  │   ├── home.html         # home page template
  │   └── about.html        # about page template
  ├── Gopkg.lock            # dep config file
  ├── Gopkg.toml            # dep config file
  └── main.go               # programme entrypoint


Enter fullscreen mode Exit fullscreen mode

The codes below are based on this gist created by rand99.

main.go



package main

import (
  "errors"
  "html/template"
  "io"

  "github.com/labstack/echo"

  "gitlab.com/ykyuen/golang-echo-template-example/handler"
)

// Define the template registry struct
type TemplateRegistry struct {
  templates map[string]*template.Template
}

// Implement e.Renderer interface
func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
  tmpl, ok := t.templates[name]
  if !ok {
    err := errors.New("Template not found -> " + name)
    return err
  }
  return tmpl.ExecuteTemplate(w, "base.html", data)
}

func main() {
  // Echo instance
  e := echo.New()

  // Instantiate a template registry with an array of template set
  // Ref: https://gist.github.com/rand99/808e6e9702c00ce64803d94abff65678
  templates := make(map[string]*template.Template)
  templates["home.html"] = template.Must(template.ParseFiles("view/home.html", "view/base.html"))
  templates["about.html"] = template.Must(template.ParseFiles("view/about.html", "view/base.html"))
  e.Renderer = &TemplateRegistry{
    templates: templates,
  }

  // Route => handler
  e.GET("/", handler.HomeHandler)
  e.GET("/about", handler.AboutHandler)

  // Start the Echo server
  e.Logger.Fatal(e.Start(":1323"))
}


Enter fullscreen mode Exit fullscreen mode

We add a new route /about which is handled by a AboutHandler and as you can see from the above highlighted lines, the templates map contains different set of template files for different HTML pages and the Render() takes the name parameter as the templates map key so it could execute the correct template set.

view/base.html



{{define "base.html"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>{{template "title" .}}</title>
    </head>
    <body>
      {{template "body" .}}
    </body>
  </html>
{{end}}


Enter fullscreen mode Exit fullscreen mode

The template statement tells the template engine that it should look for the {{title}} and {{body}} definitions in the template set and they are defined in the home.html and about.html.

view/about.html



{{define "title"}}
  Boatswain Blog | {{index . "name"}}
{{end}}

{{define "body"}}
  <h1>{{index . "msg"}}</h1>
  <h2>This is the about page.</h2>
{{end}}


Enter fullscreen mode Exit fullscreen mode

And here is the AboutHanlder which has no big difference from the HomeHandler.

handler/about_handler.go



package handler

import (
"net/http"

"github.com/labstack/echo"
)

func AboutHandler(c echo.Context) error {
// Please note the the second parameter "about.html" is the template name and should
// be equal to one of the keys in the TemplateRegistry array defined in main.go
return c.Render(http.StatusOK, "about.html", map[string]interface{}{
"name": "About",
"msg": "All about Boatswain!",
})
}

Enter fullscreen mode Exit fullscreen mode




Summary

This is just a basic example implementing nested template using the Go standard html/template library in Echo. With proper setup, we could develop a more customized and convenient pattern for Echo or even make it works with any other template engine.

The complete example could be found on gitlab.com.

Top comments (0)