Building a SPA-like Website with Go and HTMX
- Mahmoud Mousa
- Golang , Htmx
- August 17, 2024
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”.
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.
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.
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 :
- Adding a new endpoint to handle the form submission
- 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:
- Submit the values of the form to with a post request to
/poke
endpoint - 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.
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.
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!
That's not good! ๐ข
Thank you! You just made my day! ๐