Originally posted on divrhino.com
In the first part of this tutorial series, we built a REST API with Go Fiber, Docker, and Postgres. We added endpoints that would allow us to create facts and list all our facts. In this second part, we’re going to convert the API into a fullstack Go Fiber app by adding some frontend views.
Prerequisites
To follow along, you will need to have Docker installed and running.
If you did not follow the first tutorial, but would like to follow along with this instalment, you can use the finished code from the first part as your starting point. You can find that in the repo for the first tutorial.
Preparing the app for templates
Out of the box, Go Fiber already has mechanisms that will allow us to add some frontend views to our app. We will need to use a template engine. Go Fiber supports several template engines. The full list of supported template engines can be found on their website. For this tutorial, we’re going to use the html
template engine.
To install the necessary package, we can enter our web
service container. Again, we’re assuming you have the code from the first part of this tutorial series.
docker compose run --service-ports web bash
Note:
⚠️ If you get an error about environment variables not being set, you may need to create an .env
file in your repository following the instructions from the first part of this tutorial series.
Then, within the container, we can use the go get
command to install the Go Fiber html
template engine
go get github.com/gofiber/template/html
Next we will need to configure our Go Fiber app to handle templates correctly. We can head into the cmd/main.go
file to do the necessary set up. First, we will import the html
template package we just installed, and then we will use html.New()
to initialise a new engine instance.
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html" // 1. import
"github.com/divrhino/divrhino-trivia/database"
)
func main() {
database.ConnectDb()
engine := html.New("./views", ".html") // 2. new engine
app := fiber.New()
setUpRoutes(app)
app.Listen(":3000")
}
We will also have to update the app initialisation code and pass in some configuration options. In the options, we can set the Views
property to the engine
instance we created above.
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html"
"github.com/divrhino/divrhino-trivia/database"
)
func main() {
database.ConnectDb()
engine := html.New("./views", ".html")
app := fiber.New(fiber.Config{
Views: engine, // new config
})
setUpRoutes(app)
app.Listen(":3000")
}
Now that our app is configured for templates, we can add our first frontend view.
Index view
The first view we will create will be the index
view. This is where we will display the list of all our Facts
.
To organise our html
template files, we will need to first create a views
directory
mkdir views
Then inside this directory, we can create a new index.html
file
touch views/index.html
Our index
view is currently empty, but we will come back to it shortly.
Now we will head into our handlers/facts.go
file because we will need to update our ListFacts
handler so it returns an html
template. Go Fiber gives us a Render()
method on the fiber.Ctx
, which will allow us to render a view.
We can pass Go data to this Render()
method and it returns a text/html
response. For now, will pass through some string data for a Title
and Subtitle
field.
package handlers
import (
"github.com/divrhino/divrhino-trivia/database"
"github.com/divrhino/divrhino-trivia/models"
"github.com/gofiber/fiber/v2"
)
func ListFacts(c *fiber.Ctx) error {
facts := []models.Fact{}
database.DB.Db.Find(&facts)
return c.Render("index", fiber.Map{
"Title": "Div Rhino Trivia Time",
"Subtitle": "Facts for funtimes with friends!",
})
}
func CreateFact(c *fiber.Ctx) error {
fact := new(models.Fact)
if err := c.BodyParser(fact); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": err.Error(),
})
}
database.DB.Db.Create(&fact)
return c.Status(200).JSON(fact)
}
These fields can be displayed in our html
file using the Go template syntax i.e. double curly braces ({{ }}
).
Now we can return to our views/index.html
file and add the following content. Here we are using dot notation to display the Title
and Subtitle
fields of the current page.
<p>{{ .Title }}</p>
<p>{{ .Subtitle }}</p>
We can pass in any kind of Go data structure when rendering templates. For our Title
and Subtitle
, we rendered string data. Later on in the tutorial, we will see how we can loop over and render nested data.
We can already test this out in the browser. We can exit
from the web
service container and run our app from the host terminal using the command:
docker compose up
Our .Title
and .Subtitle
should be present on the page at http://localhost:3000/
Global layout
HTML pages share a lot of common elements like the <head>
tag and navigation menus. Instead of rendering these elements on every page, individually, we can create a global layout. We can then use the global layout as the base for all our pages. In this section, we will update our app config settings to accomodate this.
Heading back to cmd/main.go
, we need to revisit our app’s config object. We will add a ViewsLayout
property and set it to "layouts/main"
. This will apply the main
layout to all our pages, by default.
package main
import (
"github.com/divrhino/divrhino-trivia/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html"
)
func main() {
database.ConnectDb()
engine := html.New("./views", ".html")
app := fiber.New(fiber.Config{
Views: engine,
ViewsLayout: "layouts/main", // add this to config
})
setUpRoutes(app)
app.Listen(":3000")
}
This main
layout file does not exist yet. So when we try to test this in the browser, we get this error:
We can resolve the error by creating the main
layout. Let’s first create a new folder for our layouts.
mkdir views/layouts
Then we can create the main.html
layout file here
touch views/layouts/main.html
Opening up views/layouts/main.html
, we’ll add the following content.
<!DOCTYPE html>
<head>
<title>Div Rhino Trivia</title>
</head>
<body>
<div class="wrapper">
<h1>{{.Title}}</h1>
<h2>
{{.Subtitle}}
</h2>
{{embed}}
</div>
</body>
</html>
It’s just a basic HTML layout with a couple of headings. We’re rendering our .Title
data within <h1>
tags and our .Subtitle
data within <h2>
tags. Below these, we have an {{embed}}
. The {{embed}}
is where our individual page content will be rendered.
We have arbitrarily decided that all of our pages will have a {{.Title}}
and {{.Subtitle}}
field, so we can render them in the main layout, so that all our pages get the same layout. Of course, the contents of the fields will be different on each page because each page has its own context.
Remember that we already have a {{.Title}}
and {{.Subtitle}}
rendered in our index.html
template. So if we check our browser now, we see double
To fix this, we need to remove the fields from our index.html
file and replace them with some temporary text:
<!-- index.html -->
This content is unique to the index page
If we head into our browser now, we should see our main
layout content has rendered along with the unique text we put in our index.html
file.
Displaying all facts on the index page
In the previous step, we rendered a couple of string values in our template. It was a good demonstration of how static content can be displayed in a template. In the following section, we will build on this knowledge to be able to render dynamic content.
Let’s update the ListFacts
handler so that it sends facts
from the database to our frontend template. As mentioned previously, we can send any type of Go data to our templates. The facts
variable holds a slice of Fact
, the underlying type of a Fact
is struct
.
package handlers
import (
"github.com/divrhino/divrhino-trivia/database"
"github.com/divrhino/divrhino-trivia/models"
"github.com/gofiber/fiber/v2"
)
func ListFacts(c *fiber.Ctx) error {
facts := []models.Fact{}
database.DB.Db.Find(&facts)
return c.Render("index", fiber.Map{
"Title": "Div Rhino Trivia Time",
"Subtitle": "Facts for funtimes with friends!",
"Facts": facts, // send the facts to the view
})
}
func CreateFact(c *fiber.Ctx) error {
fact := new(models.Fact)
if err := c.BodyParser(fact); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": err.Error(),
})
}
database.DB.Db.Create(&fact)
return c.Status(200).JSON(fact)
}
Inside the index.html
view, we can range over the Facts
and display each of them as follows.
<div>
{{ range .Facts }}
<div>
<p>{{ .Question }}</p>
<p>{{ .Answer }}</p>
</div>
{{ end }}
</div>
We should also handle scenarios where no Facts
are present. We can use if
/ else
blocks to do some conditional rendering. If there are Facts
present, we will range over and render each one. If there are no Facts
, we will display a message that says there are no facts with a link to /fact
where we can create new facts.
<div>
{{ if .Facts }}
{{ range .Facts }}
<div>
<p>{{ .Question }}</p>
<p>{{ .Answer }}</p>
</div>
{{ end }}
{{ else }}
<div>
<p>No facts yet.</p>
<p><a href="/fact">Add new</a></p>
</div>
{{ end }}
</div>
We’ve cleared the database, so there are currently no Facts
present. Our index
view currently looks like this in the browser:
The Add new
link goes to a page that does not exist yet, so we will create it in the next section.
Create new fact view
The next view we will create will be the new.html
view. This template will contain a form that can be used to submit new Facts
.
In the views
folder, we will create a new template file called new.html
:
touch views/new.html
In the first part of this tutorial series, we added a route to POST
new facts to the API. We now need to add a route that will GET
the “new fact” template.
package main
import (
"github.com/divrhino/divrhino-trivia/handlers"
"github.com/gofiber/fiber/v2"
)
func setupRoutes(app *fiber.App) {
app.Get("/", handlers.ListFacts)
app.Get("/fact", handlers.NewFactView) // Add new route for new view
app.Post("/fact", handlers.CreateFact)
}
The handlers.NewFactView
function doesn’t exist yet, so let’s head into the handlers/facts.go
file to create it. For now, we will just add a Title
and a Subtitle
, we will build on it later.
package handlers
import (
"github.com/divrhino/divrhino-trivia/database"
"github.com/divrhino/divrhino-trivia/models"
"github.com/gofiber/fiber/v2"
)
func ListFacts(c *fiber.Ctx) error {
facts := []models.Fact{}
database.DB.Db.Find(&facts)
return c.Render("index", fiber.Map{
"Title": "Div Rhino Trivia Time",
"Subtitle": "Facts for funtimes with friends!",
"Facts": facts,
})
}
// Create new Fact View handler
func NewFactView(c *fiber.Ctx) error {
return c.Render("new", fiber.Map{
"Title": "New Fact",
"Subtitle": "Add a cool fact!",
})
}
func CreateFact(c *fiber.Ctx) error {
fact := new(models.Fact)
if err := c.BodyParser(fact); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": err.Error(),
})
}
database.DB.Db.Create(&fact)
return c.Status(200).JSON(fact)
}
We can test out this new
template by visiting http://localhost:3000/fact in the browser.
At the moment, we only have the default content from the main layout rendering on the page because our new.html
file is empty. We will work on creating our form in the next step.
Use a form to create new facts
Back in our new.html
view, we will write some markup for our new
form:
<form id="new-form" method="POST" action="/fact">
<label for="question">
<span>Question</span>
<input id="question" type="text" name="question">
</label>
<label for="answer">
<span>Answer</span>
<input id="answer" type="text" name="answer">
</label>
<input type="submit" value="Submit">
</form>
Without adding anything more, we can already submit this form data. However, when we do submit a new fact, the UX of it is a little awkward, as it redirects to the JSON response (in Firefox, anyway). We can fix that by redirecting to a success
or confirmation
page.
Success page
Our success
or confirmation
view will show us a message to confirm that our form data has been submitted successfully.
Let’s create a new template called confirmation.html
:
touch views/confirmation.html
This confirmation.html
file will contain a link that takes users back to the fact creation page
<div class="add-more">
<p><a href="/fact">Add more</a></p>
</div>
Then we can head into the handlers/facts.go
file to add a handler for our confirmation
view. We won’t add a route for this handler because we only need to use it within our CreateFact()
handler.
We will also update the CreateFact()
handler to return our new ConfirmationView
:
package handlers
import (
"github.com/divrhino/divrhino-trivia/database"
"github.com/divrhino/divrhino-trivia/models"
"github.com/gofiber/fiber/v2"
)
func ListFacts(c *fiber.Ctx) error {
facts := []models.Fact{}
database.DB.Db.Find(&facts)
return c.Render("index", fiber.Map{
"Title": "Div Rhino Trivia Time",
"Subtitle": "Facts for funtimes with friends!",
"Facts": facts,
})
}
func NewFactView(c *fiber.Ctx) error {
return c.Render("new", fiber.Map{
"Title": "New Fact",
"Subtitle": "Add a cool fact!",
})
}
func CreateFact(c *fiber.Ctx) error {
fact := new(models.Fact)
if err := c.BodyParser(fact); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": err.Error(),
})
}
database.DB.Db.Create(&fact)
return ConfirmationView(c) // 2. Return confirmation view
}
// 1. New Confirmation view
func ConfirmationView(c *fiber.Ctx) error {
return c.Render("confirmation", fiber.Map{
"Title": "Fact added successfully",
"Subtitle": "Add more wonderful facts to the list!",
})
}
We can head into the browser to test out the confirmation
page. Once we’ve submitted a form, we will be redirected to our confirmation
page rather than the JSON response
Partials
There are several ways we can make our markup more reusable. Previously, we saw how to create a layout
to share common structural page elements between our views. Another technique would be to make use of partials
.
We need a header, but we don’t want to copy/paste the header markup for every one of our views. So let’s put the header in a partial. We can start by creating a views/partials
folder
mkdir views/partials
Then add a new header.html
file to it:
touch views/partials/header.html
Inside header.html
, we can define
our partial
{{define "header"}}
<header>
<div>
<a href="/">
<img src="image/divrhino-logo.png">
</a>
</div>
<div class="">
<a href="/fact">
<span>+ New Fact</span>
</a>
</div>
</header>
{{end}}
We can now use our header
partial in views/layouts/main.html
. The .
means we’re passing through the current context
{{define "main"}}
<!DOCTYPE html>
<head>
<title>Div Rhino Trivia</title>
</head>
<body>
<div class="wrapper">
{{template "header" .}} <!-- import partial -->
<h1>{{.Title}}</h1>
<h2>
{{.Subtitle}}
</h2>
{{embed}}
</div>
</body>
</html>
If we look at our header
partial, we will notice that we are importing a logo image that doesn’t exist yet. And while the image is not present, our app doesn’t know how to handle images yet, either. In the next section, we’ll configure our app to handle static assets.
Serving static assets
Although our app is functional, it isn’t much to look at. We can improve that with some static assets such as images, CSS or a bit of JavaScript.
Go Fiber has methods to help us achieve this fairly easily. Let’s head back into our main.go
file and use the app.Static
method to tell our app where to locate our static assets. In our case, we will be putting our images, CSS and JS in a folder called public
. We have chosen to call it public
because its somewhat of a convention, but you can call this folder anything you like.
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html"
"github.com/divrhino/divrhino-trivia/database"
)
func main() {
database.ConnectDb()
engine := html.New("./views", ".html")
app := fiber.New(fiber.Config{
Views: engine,
ViewsLayout: "layouts/main",
})
setUpRoutes(app)
app.Static("/", "./public")
app.Listen(":3000")
}
The public
folder does not exist yet, so let’s create it in the root of our project
mkdir public
Now we can move on and adding our logo image.
Images
To keep our images organised, we will create a folder in our public
directory called image
:
mkdir public/image
We can put our logo image file into this folder.
Our logo image is now present in the header on our index
page. If we visit other pages, the header partial is also present.
Linking stylesheets
Apart from images, we can also add some CSS stylesheets.
To keep our stylesheets organised, let’s create a folder in our public
directory called style
:
mkdir public/style
Then we can create a stylesheet within it:
touch public/style/main.css
We don’t have anything in our main.css
file yet, but let’s link it in our head tag first and add some styles later. In the main
layout, we can add this link
tag to the <head>
. We’ve added a query param of v=1
after the href
value as a rudimentary way to bust the cache while we’re developing, locally. I’m sure there are other ways to do this.
<link rel="stylesheet" href="style/main.css?v=1">
Let’s also add the viewport
meta tag to the <head>
so our app can be responsive and display correctly on mobile devices:
<meta name="viewport" content="width=device-width,initial-scale=1">
The index.html
template should look like this:
<!DOCTYPE html>
<head>
<title>Div Rhino Trivia</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="style/main.css?=1">
</head>
<body>
<div class="wrapper">
{{template "header" .}}
<h1>{{.Title}}</h1>
<h2>
{{.Subtitle}}
</h2>
{{embed}}
</div>
</body>
</html>
Inside our main.css
file, we can change the body
element’s background colour
body {
background-color: aquamarine;
}
We will have to update our markup and paste in some pre-prepared styles. We will not go over the specifics here, as this is not a CSS tutorial. But the completed markup and stylesheet can be found in the Github repo.
JavaScript
We’ve added a bit more markup to make the layout a little more interesting. We’ve added a little toggle button next to each of our facts.
Right now, they are not functional. Let’s add some javascript to make these buttons interactive.
To keep our JavaScript organised, let’s create a folder in our public
directory called javascript
:
mkdir public/javascript
Then we can create a JavaScript file within it:
touch public/javascript/app.js
We don’t have anything in our app.js
file yet, but let’s link it in our head tag first and add our JS later. In the index.html
view, we can import this <script>
tag at the bottom of the file.
<div class="container">
{{if .Facts}}
{{ range .Facts }}
<div class="fact-item">
<div class="question-wrapper">
<p class="question">{{ .Question }}</p>
<button id="answer-toggle" class="answer-toggle">Toggle answer</button>
</div>
<div class="answer-wrapper">
<p class="answer">{{.Answer}}</p>
</div>
</div>
{{ end }}
{{else}}
<div class="no-facts">
<p>No facts yet.</p>
<p><a href="/fact">Add new</a></p>
</div>
{{end}}
</div>
<script src="/javascript/app.js"></script>
We will also paste in some pre-prepared JS. Again, the final javascript file can be found in the Github repo.
And with that, we’ve come to the end.
Conclusion
In this tutorial we learnt how to add a form and other frontend views to a Go Fiber app that was build from scratch using Go and Docker. We started with a simple API and extended it to include templates and static assets.
Congratulations, you did great! Keep learning and keep coding. Bye for now, <3.
divrhino / divrhino-trivia-fullstack
Convert a REST API into a fullstack Go Fiber app by adding some frontend views.
Build a fullstack app with Go Fiber, Docker, and Postgres
- Text tutorial: https://divrhino.com/articles/full-stack-go-fiber-with-docker-postgres/
- Video tutorial: https://youtu.be/B7hSjNbcVYM
Top comments (0)