Build a fullstack app with Go Fiber, Docker, and Postgres
Tags: go, docker, css, webdev
Published: 2022-11-19 06:30:55

_Originally posted on [divrhino.com](https://divrhino.com/articles/full-stack-go-fiber-with-docker-postgres/)_

{% youtube B7hSjNbcVYM %}

In the [first part of this tutorial series](https://dev.to/divrhino/build-a-rest-api-from-scratch-with-go-and-docker-3o54), 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](https://www.docker.com/products/docker-desktop/) 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](https://github.com/divrhino/divrhino-trivia).

## Preparing the app for templates

Out of the box, [Go Fiber](https://docs.gofiber.io/guide/templates) 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](https://docs.gofiber.io/guide/templates#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.

```bash
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](https://divrhino.com/articles/rest-api-docker-go-fiber-from-scratch/#environment-variables) of this tutorial series.

Then, within the container, we can use the `go get` command to install the Go Fiber `html` template engine

```bash
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.

```go
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.

```go
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

```bash
mkdir views
```

Then inside this directory, we can create a new `index.html` file

```bash
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.

```go
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.

```html
<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:

```bash
docker compose up
```

Our `.Title` and `.Subtitle` should be present on the page at [http://localhost:3000/](http://localhost:3000/)


![The Title and Subtitle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ye7w5zdu8b4b82pfadr.png)


## 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.

```go
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:


![Main layout does not exist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0cfymzbai18kr2qmet8k.png)

We can resolve the error by creating the `main` layout. Let’s first create a new folder for our layouts.

```bash
mkdir views/layouts
```

Then we can create the `main.html` layout file here

```bash
touch views/layouts/main.html
```

Opening up `views/layouts/main.html`, we’ll add the following content.

```html
<!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


![Double title and subtitle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6nrrryq5ay749owmf3no.png)


To fix this, we need to remove the fields from our `index.html` file and replace them with some temporary text:

```html
<!-- 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.


![Main layout with unique text from index page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iwdkhrbkgxmq1k15ty34.png)


## 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`.

```go
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.

```html
<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.

```html
<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:


![No facts present yet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sltamniehuvktvca0nm8.png)



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`:

```bash
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.

```go
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.

```go
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](http://localhost:3000/fact) in the browser.


![New fact view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z2o5pfilgq91pbq4ygtk.png)



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:

```html
<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.


![Submitted fact redirects to JSON in browser](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bfdhrqbnwo788zbbdpt2.png)



## 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`:

```bash
touch views/confirmation.html
```

This `confirmation.html` file will contain a link that takes users back to the fact creation page

```html
<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`:

```go
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


![Confirmation page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5lc9rrjslipa4unexip.png)


## 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

```bash
mkdir views/partials
```

Then add a new `header.html` file to it:

```bash
touch views/partials/header.html
```

Inside `header.html`, we can `define` our partial

```html
{{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

```html
{{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.


![Missing logo image in header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kj564kc3whsz3z2tscgz.png)


## 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.

```go
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

```bash
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`:

```bash
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.


![Header with logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d4c0e9zcroql1z0l4hts.png)



**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`:

```bash
mkdir public/style
```

Then we can create a stylesheet within it:

```bash
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.

```html
<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:

```html
<meta name="viewport" content="width=device-width,initial-scale=1">
```

The `index.html` template should look like this:

```html
<!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

```css
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](https://github.com/divrhino/divrhino-trivia-fullstack/pull/8/commits/8b42951019a57d08e2bf3d243f075e294ae25270) and [stylesheet](https://github.com/divrhino/divrhino-trivia-fullstack/blob/main/public/style/main.css) 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.


![Toggle button not interactive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mnk77fu0wdkajbzu4nk.png)



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`:

```bash
mkdir public/javascript
```

Then we can create a JavaScript file within it:

```bash
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.

```html
<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](https://github.com/divrhino/divrhino-trivia-fullstack/blob/main/public/javascript/app.js) can be found in the Github repo.


![Toggle button animated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q6vkbikdw8ucpm4d2ue1.gif)



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.

{% github divrhino/divrhino-trivia-fullstack %}