Bringing the fun back with Go and HTMX

2024-01-10T19:36:54Z

After seeing a video from ThePrimeagen about HTMX, I though about taking a stab at building a full-stack web application with Go and well HTMX.

By using the net/http and the fmt packages, you can get something like a counter, which hasn't been used as an example enough times, working fairly quick.

var counter int
var tpl string = `
<!DOCTYPE html>
<html>
    <head>
        <script src="https://unpkg.com/htmx.org@1.9.10"></script>
        <title>Go/HTMX Counter</title>
    </head>
    </body>
        <p id="counter">0</p>
        <button hx-get="/increment" hx-target="#counter" hx-swap="outerHTML">Increment</button>
    </body>
</html>
`
// var counter int...
// var tpl string...

func increment(w http.ResponseWriter, r *http.Request) {
    counter++
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte(fmt.Sprintf(`<p id="counter">%d</p>`, counter)))
}

func index(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte(tpl))   
}

func main() {
    http.HandleFunc("/", index) 
    http.HandleFunc("/increment", increment)
    http.ListenAndServe(":8080", nil)
}

And you can easily dive deeper by incorporating the html/template package into the mix.

var tasks []string
var tpl string = `
{{ define "index" }}
<!DOCTYPE html>
<html>
    <head>
        <script src="https://unpkg.com/htmx.org@1.9.10"></script>
        <title>Go/HTMX Tasks</title>
    </head>
    <body>
        <form hx-post="/tasks/add" hx-target="#task-list" hx-swap="innerHTML">
            <input name="title" type="text" placeholder="The title of the task">
            <button type="submit">Add task</button>
        </form>
        <ul id="task-list">
            {{ range .tasks }}
            <li>{{.}}</li>
            {{ else }}
            <li>No tasks found</li>
            {{ end }}
        </ul>
    </body>
</html>
{{ end }}
`
// var tasks []string...
// var tpl string...

func addTask(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseForm(); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    if len(r.Form.Get("title")) <= 0 {
        http.Error(w, "empty task title", http.StatusBadRequest)
        return
    }

    t, err := template.New("task-list").Parse(`
    {{ define "task-list" }}
        {{ range .tasks }}
        <li>{{.}}</li>
        {{ end }}
    {{ end }}
    `)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    tasks = append(tasks, r.Form.Get("title"))

    var buf bytes.Buffer
    if err = t.Execute(&buf, map[string]interface{}{
        "tasks": tasks,
    }); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write(buf.Bytes())
}

func index(w http.ResponseWriter, r *http.Request) {
    t, err := template.New("index").Parse(tpl)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    var buf bytes.Buffer
    if err = t.Execute(&buf, map[string]interface{}{
        "tasks": tasks,
    }); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write(buf.Bytes())   
}

func main() {
    http.HandleFunc("/", index) 
    http.HandleFunc("/tasks/add", addTask)
    http.ListenAndServe(":8080", nil)
}

If you know a bit of Go interacting with data using HTMX is a piece of cake, from this point it should be trivial to replace our hard-coded string slice for real data from our database of choice, but I'll leave that as a feature to add on your own.

So far HTMX has made me enjoy working on the front-end again, I enjoyed it to the point of feeling the necessity of remaking my entire website with it, I was previously using Bloggo.

As per this introduction I've only scrached the surface of the topic at hand, if you find it worth while feel free to check the source code of this website to see a more real-world example; and if you wish to see the complete code for this two examples you can do so here.

Copyright © 2024 Kevin Suñer