Building a SPA-like Website with Go and HTMX

Building a SPA-like Website with Go and HTMX

Are you a tired JavaScript developer? Are you sick of having to learn so many different libraries that need to be pieced together to make something work? Maybe you are a burned-out React developer who is sick of learning the rules of React, different hooks, and how they work. That was me around a year and a half ago. Now, I see things very differently, and I want to show you the way.

In this tutorial, I’ll run you through how you can build a SPA-like website where you can look up different Pokemons. The experience is very good, and you won’t have to touch much JavaScript. Your users will love it, and you will love writing this kind of code. You will learn a lot about using HTMX and Golang together.

HTMX seared in popularity in the past year. Mostly because of simplicty and the way it uses HTML as a based to build upon; not to replace. HTMX will allow us to bring back the glory days of the monolith, where Server Side Rendering was the norm and reactive islands weren’t an innovation; just an everyday tool!

First we need to setup a simple server in Golang to serve our HTML. Then we will add HTMX to add interactivty and handle form submissions gracefully. Let’s get started!

Setting Up a Simple HTTP Server in Go

First, let’s start by creating a very simple HTTP server in Go. We’ll go through this in a few steps:

Step 1: Initialize a Router

First, we initialize a router. In Go, we use http.ServeMux as our router. From Golang docs we see this mux defined as:

ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.

You can easily think about this as your router. It’s the equivalent of that in many other languages. So we will name it as such

func main() {
    router := http.NewServeMux()
}

Nice with that done, we need to instruct this router to handle different routes differenly; We do this by attaching a different handler functions to each route patter.

Step 2: Define a Handler

We define a handler function for the GET method on the root. Notice the use of GET / - this pattern matching feature was only released in Go 1.22. Make sure you at least have that version installed for this to work. For the sake of simplicity, our router will only return “Hello, world!” for now.

router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
    w.write([]byte("Hello, world!"))
})

Step 3: Define the Server

Now we need to be able to use that router somewhere. The way it works in Go is that you need to make a new server and attach that previously defined router to that server. Let’s dfeine a server that will run on port 3000 and use the router as a handler.

server := &http.Server{
    Addr:    ":3000",
    Handler: router,
}

Step 4: Listen and Serve

Finally, we start the server by calling ListenAndServe.

	fmt.Println("Listening on 3000")
	server.ListenAndServe()

after that, our server will start listening on port 3000 as intructed.

Putting it all together, our main.go file looks like this:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    router := http.NewServeMux()
    router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
      w.write([]byte("Hello, world!"))
    })

    server := &http.Server{
        Addr:    ":3000",
        Handler: router,
    }

	fmt.Println("Listening on 3000")
	server.ListenAndServe()
}

When you run this code and navigate to http://localhost:3000, you should see “Hello, World”.

Keen to get your hands on the code? ๐Ÿ’ป

Enter your details below and we will send you the code. You can also subscribe to get our latest posts. Don't worry, you can unsub anytime. ๐Ÿ‘Š

Rendering HTML in Go

Alright that’s a start. Now you know how easy it is to start HTTP server in Golang in 4 steps. It’s quite easy actually. Sending back strings is not very useful I take it. So let’s improve this and make it into an actual website by sending back HTML instead strings.

Embedding the HTML files

Since Golang is a Compiled Lanugage we need to embed the HTML files in the final compiled binary. This is important since Golang build will result in a simple excutable file - any resources needed at runtime need to embedded in the executable. We can do this imbedding by using the embed module.

//go:embed views/*
var views embed.FS

Make sure all the HTML files that you use are saved into the view folder.

Parse HTML Templates

Now that we are embedding the files it’s time to register them as templates that we are going to use. This is simple and we do this by using the html/template module.

var t = template.Must(template.ParseFS(views, "views/*"))

Execute the Template

Now that we have parsed the html template it’s time to use them to render HTML and return them. We do this by using the ExecuteTemplate function. This function will allow us:

ExecuteTemplate applies the template associated with t that has the given name to the specified data object and writes the output to wr. If an error occurs executing the template or writing its output, execution stops, but partial results may already have been written to the output writer. A template may be executed safely in parallel, although if parallel executions share a Writer the output may be interleaved.

So let’s use that in our handler to send back an index.html file instead of the boring Hello, World! from before.

router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
	if err := t.ExecuteTemplate(w, "index.html", nil); err != nil {
		http.Error(w, "Something went wrong", http.StatusInternalServerError)
	}
})

Create index.html

Now we don’t actually have an index.html yet. So let’s add one so we can serve it. For now we will send a super simple HTML file. That shows nothing but an h1 element that has the words “Pokemon Cards” in it.

Create a file named index.html inside the views folder with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pokemon Cards</title>
</head>
<body>
    <h1 class="text-2xl">Pokemon Cards</h1> 
</body>
</html>

Notice on this line: <h1 class="text-2xl">Pokemon Cards</h1> we added a class here will take effect soon once we add tailwind soon.

Now, when you navigate to http://localhost:3000, you should see “Pokemon Cards”.

Adding static card to index html

Our HTML looks very boring now. So let’s add some libraries to handle our styling and componets for us.

Adding needed libraries

There are many choices for you to do this. It’s up to you. I prefer things to simple and quick. So I have chosen tailwind CSS to supply me with simple utility first classes. I also chose DaisyUI for giving me dead simple components that I can render with single class. Both projects are amazing and work super well together. There are also other options I would recommend you have a look at - PicoCSS is one such option that I like very much and will be using more often. Simplicty always wins. ๐Ÿ˜ผ

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pokemon Cards</title>
    <link
      href="https://cdn.jsdelivr.net/npm/daisyui@4.12.2/dist/full.min.css"
      rel="stylesheet"
      type="text/css"
    />
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- adding htmx script from CDN -->
    <script
      src="https://unpkg.com/htmx.org@2.0.0"
      integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw"
      crossorigin="anonymous"
    ></script>
</head>
<body>
    <h1 class="text-2xl">Pokemon Cards</h1> 
</body>
</html>

Notice How I using the CDN version of those libraries. I am oversimplifying here. You shouldn’t use the CDN version when using tailwind in production environment. Make sure to add a build step to only ship the CSS you need.

Warning

Make sure to build your css before going to production when using tailwind unlike what I am doing here. I’m using the CDN version to make things simpler.

I have also included HTMX from CDN. This is an acceptable and recommended way to use HTMX - a build step is not needed.

Adding a static card

Now that we have our libaries handy, let’s add a static card to act as a placeholder for our Pokemon data we will get soon. This card is super simple HTML. It’s an article with some properties inside of it.

<article
  class="card w-48 bg-base-100 shadow-lg border-2 border-gray-500"
>
  <div class="card-body p-4">
    <img
      class="self-center"
      alt="{{.Name}}"
      width="80"
      src="{{.Sprites.Other.Showdown.FrontDefault}}"
    />
    <h2 class="card-title self-center">POKE NAME</h2>
    <div class="divider mx-[-18px] my-0 border-color-gray-400"></div>
    <ul>
      <li><strong>Type:</strong> POKE TYPE</li>
      <li><strong>Height:</strong>POKE HEIGHT</li>
      <li><strong>Weight:</strong> POKE WEIGHT</li>
    </ul>
  </div>
</article>

I’m using the card class name from DaisyUI to get a quick card div that I use as container. This is not enough, we need to also add a grid containter to contain this card and the cards that will follow. So let’s wrap it up in a div with class of grid on it.

<div class="grid grid-cols-4 align-center gap-5">
  <article
    class="card w-48 bg-base-100 shadow-lg border-2 border-gray-500"
  >
    <div class="card-body p-4">
      <img
        class="self-center"
        alt="{{.Name}}"
        width="80"
        src="{{.Sprites.Other.Showdown.FrontDefault}}"
      />
      <h2 class="card-title self-center">POKE NAME</h2>
      <div class="divider mx-[-18px] my-0 border-color-gray-400"></div>
      <ul>
        <li><strong>Type:</strong> POKE TYPE</li>
        <li><strong>Height:</strong>POKE HEIGHT</li>
        <li><strong>Weight:</strong> POKE WEIGHT</li>
      </ul>
    </div>
  </article>
</div>

Final step now is to make a full conatiner that will center everything and place the h1 element inside of that conatiner as well. Our final HTML body tag looks like this now:

<body>
  <div class="flex flex-col gap-20 align-center items-center">
    <h1 class="text-2xl">Pokemon Cards</h1>
    <div class="grid grid-cols-4 align-center gap-5">
      <article
        class="card w-48 bg-base-100 shadow-lg border-2 border-gray-500"
      >
        <div class="card-body p-4">
          <img
            class="self-center"
            alt="{{.Name}}"
            width="80"
            src="{{.Sprites.Other.Showdown.FrontDefault}}"
          />
          <h2 class="card-title self-center">POKE NAME</h2>
          <div class="divider mx-[-18px] my-0 border-color-gray-400"></div>
          <ul>
            <li><strong>Type:</strong> POKE TYPE</li>
            <li><strong>Height:</strong>POKE HEIGHT</li>
            <li><strong>Weight:</strong> POKE WEIGHT</li>
          </ul>
        </div>
      </article>
    </div>
  </div>
</body>

Now, when you navigate to http://localhost:3000, you should see a styled card with static data.

static_card

Obviously that is not what we want, we want to be able to render dynamic pokemon data. So let’s do that

Rendering dynamic pokemon data

To make the data dynamic, we need to fetch Pokemon data from the PokeAPI

Fetch Pokemon Data

We’ll use the PokeAPI to fetch data for a specific Pokemon. In order to do that, we will use http.GET method to fetch some data about one pokemon. Once we get the response we need to parse that response body into a golang object. To do this, we need to make a struct that matches the response data. In a file called types.go you need to add the followint struct data:

package main

// Define the struct to map the JSON response
type Pokemon struct {
	Name           string  `json:"name"`
	BaseExperience int     `json:"base_experience"`
	Height         int     `json:"height"`
	Weight         int     `json:"weight"`
	Sprites        Sprites `json:"sprites"`
	Types          []Type  `json:"types"`
}

type Sprites struct {
	Other        OtherSprites `json:"other"`
	FrontDefault string       `json:"front_default"`
}

type OtherSprites struct {
	Showdown Showdown `json:"showdown"`
}

type Showdown struct {
	FrontDefault string `json:"front_default"`
}

type Type struct {
	Slot       int        `json:"slot"`
	TypeDetail TypeDetail `json:"type"`
}

type TypeDetail struct {
	Name string `json:"name"`
	URL  string `json:"url"`
}

With this struct ready we can now parse the response body that we will get back from PokeAPI into a struct object we can handle in Golang. We use json encoding module to do this.

router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
	resp, err := http.Get("https://pokeapi.co/api/v2/pokemon/pichu")
	if err != nil {
		http.Error(w, "Unable to grab the pokemon data", http.StatusInternalServerError)
	}
	data := Pokemon{}
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		http.Error(w, "Unable to parse the Pokemon data", http.StatusInternalServerError)
	}

	if err := t.ExecuteTemplate(w, "index.html", data); err != nil {
		http.Error(w, "Something went wrong", http.StatusInternalServerError)
	}
})

Update index.html to Use Dynamic Data

Brilliant! Now we are passing this data object as context into the template. We can access this object in the template using the Dot Notation as follows.

<body>
  <div class="flex flex-col gap-20 align-center items-center">
    <h1 class="text-2xl">Pokemon Cards</h1>
    <div class="grid grid-cols-4 align-center gap-5">
      <article
        class="card w-48 bg-base-100 shadow-lg border-2 border-gray-500"
      >
        <div class="card-body p-4">
          <img
            class="self-center"
            alt="{{.Name}}"
            width="80"
            src="{{.Sprites.Other.Showdown.FrontDefault}}"
          />
          <h2 class="card-title self-center">{{.Name}}</h2>
          <div class="divider mx-[-18px] my-0 border-color-gray-400"></div>
          <ul>
            {{range .Types}}
            <li><strong>Type:</strong> {{.TypeDetail.Name}}</li>
            {{end}}
            <li><strong>Height:</strong> {{.Height}}</li>
            <li><strong>Weight:</strong> {{.Weight}}</li>
          </ul>
        </div>
      </article>
    </div>
  </div>
</body>

Now, when you navigate to http://localhost:3000, you should see dynamic data for Pichu.

Dynamic Data

Keen to get your hands on the code? ๐Ÿ’ป

Enter your details below and we will send you the code. You can also subscribe to get our latest posts. Don't worry, you can unsub anytime. ๐Ÿ‘Š

Taking User Input

Now that we are able to make a request to PokeAPI and we are rendering Dynamic Data, we are in a great spot to start making this more dynamic by accepting the user input. To do this, we will add a simple form - simple HTML form no fancy libraries or anything. We always want to use the platform. So let’s start with a simple form then I will show you how we can use HTMX to make it a super form! ๐Ÿš€

Adding Simple Form to index.html

Add a form to the index.html for user input. This form will be a simple HTML “dumb” form. This form will look like this:

<div class="card w-48 bg-base-100 shadow-xl border-2 border-gray-500">
  <form class="card-body justify-center p-4">
    <h2 class="card-title self-center">Add more</h2>
    <input
      type="text"
      name="pokemon"
      placeholder="Type here"
      class="input input-sm input-bordered w-full max-w-xs"
    />
    <button type="submit" class="btn btn-sm btn-primary">Add</button>
  </form>
</div>

Now this form is not doing anything at the moment. The idea is to be able to send back HTML template in response to the submission of this form.

So there are two parts here to this :

  1. Adding a new endpoint to handle the form submission
  2. Making the form use the endpoint and handle the response gracefully - HTMX Magic ๐Ÿช„

So let’s first add a new endpoint at POST /poke to parse and handle the form submission. This endpoint will get the input value from the form, send a request to get the pokemon data that the user has asked for, finally render some HTML in reponse to that form submission. let’s have a look.

Adding a new endpoint to handle form submission

router.HandleFunc("POST /poke", func(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Unable to parse form", http.StatusInternalServerError)
	}

	resp, err := http.Get("https://pokeapi.co/api/v2/pokemon/" + strings.ToLower(r.FormValue("pokemon")))
	if err != nil {
		http.Error(w, "Unable to fetch new pokemon", http.StatusInternalServerError)
	}
	data := Pokemon{}
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		http.Error(w, "Unable to parse the Pokemon data", http.StatusInternalServerError)
	}
	if err := t.ExecuteTemplate(w, "response.html", data); err != nil {
		http.Error(w, "Something went wrong", http.StatusInternalServerError)
	}
})

Notice how we handle the form from the frontend - Simple call to r.ParseForm followed by r.FormValue. Notice how I am using the platform here. No JS needed on client or any fancy ways to extract values from the form. My tools work for me, not against me. Notice also we are sending back the response.html file in this case with a context of data that has the requested pokemon. This file will have hold the card with using the same context from before. It’s a simple copy/paste at this point. here is what this file looks like now:

<article class="card w-48 bg-base-100 shadow-lg border-2 border-gray-500">
  <div class="card-body p-4">
    <img
      class="self-center"
      alt="{{.Name}}"
      width="80"
      src="{{.Sprites.Other.Showdown.FrontDefault}}"
    />
    <h2 class="card-title self-center">{{.Name}}</h2>
    <div class="divider mx-[-18px] my-0 border-color-gray-400"></div>
    <ul>
      {{range .Types}}
      <li><strong>Type:</strong> {{.TypeDetail.Name}}</li>
      {{end}}
      <li><strong>Height:</strong> {{.Height}}</li>
      <li><strong>Weight:</strong> {{.Weight}}</li>
    </ul>
  </div>
</article>

This is suffecient to send back the response with the details of the requested pokemon rendered. Nice!

Let’s pay attention to the second step:

Making the form use the endpoint and handle the response gracefully - HTMX Magic ๐Ÿช„

To do this we will use simple HTMX values that will allow this form to become a powerhouse. We will add some HTML properties to this form that wil make it do the following:

  1. Submit the values of the form to with a post request to /poke endpoint
  2. Take whatever HTML that comes back from the endopoint and replace the #targetDiv div with it

To do this we will use hx-post to tell the form what to do on submission. We will use hx-swap to tell HTMX what behaviour we need when handling the new HTML We will use hx-target to tell HTMX where to place the recieved HTML

Our form looks like this now:

<div
  class="card w-48 bg-base-100 shadow-xl border-2 border-gray-500"
  id="targetDiv"
>
  <form
    hx-post="/poke"
    hx-swap="outerHTML"
    hx-target="#targetDiv"
    class="card-body justify-center p-4"
  >
    <h2 class="card-title self-center">Add more</h2>
    <input
      type="text"
      name="pokemon"
      placeholder="Type here"
      class="input input-sm input-bordered w-full max-w-xs"
    />
    <button type="submit" class="btn btn-sm btn-primary">Add</button>
  </form>
</div>

Now, when you navigate to http://localhost:3000, you can enter a Pokemon name and see the data dynamically update.

Dynamic Form

You will now notice we don’t get another form to submit another request. One way to handle to this is to simply, add another instant of that form in the response of our endpoint. So now our response.html file will look like this:

<article class="card w-48 bg-base-100 shadow-lg border-2 border-gray-500">
  <div class="card-body p-4">
    <img
      class="self-center"
      alt="{{.Name}}"
      width="80"
      src="{{.Sprites.Other.Showdown.FrontDefault}}"
    />
    <h2 class="card-title self-center">{{.Name}}</h2>
    <div class="divider mx-[-18px] my-0 border-color-gray-400"></div>
    <ul>
      {{range .Types}}
      <li><strong>Type:</strong> {{.TypeDetail.Name}}</li>
      {{end}}
      <li><strong>Height:</strong> {{.Height}}</li>
      <li><strong>Weight:</strong> {{.Weight}}</li>
    </ul>
  </div>
</article>

<div
  class="card w-48 bg-base-100 shadow-xl border-2 border-gray-500"
  id="targetDiv"
>
  <form
    hx-post="/poke"
    hx-swap="outerHTML"
    hx-target="#targetDiv"
    class="card-body justify-center p-4"
  >
    <h2 class="card-title self-center">Add more</h2>
    <input
      type="text"
      name="pokemon"
      placeholder="Type here"
      class="input input-sm input-bordered w-full max-w-xs"
    />
    <button type="submit" class="btn btn-sm btn-primary">Add</button>
  </form>
</div>

Now, when you navigate to http://localhost:3000, multiple Pokemone names and get their details.

Dynamic Form

Conclusion

In this tutorial, we built a SPA-like website using Go and HTMX. We started by setting up a simple HTTP server in Go, rendered HTML, added styling with libraries, fetched dynamic Pokemon data, and allowed user input to dynamically update the content. This approach minimizes the need for JavaScript and leverages server-side rendering for simplicity and performance.

Thank you for reading!

Did find this post helpful?

Using React with AdonisJS to handle Auth - Full stack tutorial

Using React with AdonisJS to handle Auth - Full stack tutorial

JavaScript is an incredibly versatile language that can be used for both front-end and back-end development.

Read More
Understanding Bubble Sort Slowly

Understanding Bubble Sort Slowly

Sorting algorithms, I know, I know. Everybody keeps telling you that you have to learn them, no idea why they are important, and they seem like such a hu ge hassle to you.

Read More
Nextjs Not Fullstack Framework

Nextjs Not Fullstack Framework

Welcome back to another exciting blog post! Today, we’re diving into the world of Next.

Read More