Basic HTMX Application using Go 1.22 with the net/http package to create a simple HTTP Server and the html/template package to generate basic templates.
This post is intended for developers who have a basic knowledge of HTML and Go. It serves purely as an example for educational purposes only and it should not be used in production.
Package Name and Imports
Set the package name and the import used in the following code.
package main
import (
"bytes"
"context"
"fmt"
"html/template"
"log"
"net/http"
"os"
"os/signal"
"path"
"strings"
"syscall"
)
Constants
These values can be changed to suit your needs.
const host = "127.0.0.1:8888"
const appTitle = "HTMX Demo"
const contentTypeTextHTML string = "text/html; charset=UTF-8"
const contentTypeJavascript string = "application/javascript; charset=UTF-8"
Main Function
This function sets up the HTTP Server and uses signals to close down the server if CTRL + C is pressed in the terminal window.
func main() {
var httpServer *http.Server
var stopChan chan os.Signal
router := http.NewServeMux()
router.HandleFunc("/", DefaultHandler)
router.HandleFunc("/javascript/", JSHandler)
router.HandleFunc("/request-table", RequestTableHandler)
log.Printf(`Starting %s http://%s`, appTitle, host)
httpServer = &http.Server{
Addr: host,
Handler: router,
}
stopChan = make(chan os.Signal, 1)
signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
go func() {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("ListenAndServe(): %s", err)
}
}()
<-stopChan
if err := httpServer.Shutdown(context.Background()); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
log.Printf(`Shutting down %s`, appTitle)
}
DefaultHandler
This function is the default handler. It sets the Content-Type HTTP Header then gets the content of the Request Table as a string to embed in the Main template. The ParseTemplate function is called to generate the template in the returned string. The last line outputs the string containing the generated HTML.
func DefaultHandler(responseWriter http.ResponseWriter, httpRequest *http.Request) {
responseWriter.Header().Set("Content-Type", contentTypeTextHTML)
data := map[string]any{
"title": appTitle,
"requestTable": GetRequestTable(httpRequest),
}
fmt.Fprint(responseWriter, ParseTemplate("main", data))
}
JSHandler
This functions sets the correct HTTP Header in order the serve the files in the javascript directory. This is important as the browser may not run the javascript code if this is not done.
func JSHandler(responseWriter http.ResponseWriter, httpRequest *http.Request) {
responseWriter.Header().Set("Content-Type", contentTypeJavascript)
http.ServeFile(responseWriter, httpRequest, `javascript/`+path.Base(httpRequest.URL.Path))
}
RequestTableHandler
This function calls the GetRequestTable functions that returns the Request Table template in the returned string which includes the Request Method and any HTMX headers that are received.
func RequestTableHandler(responseWriter http.ResponseWriter, httpRequest *http.Request) {
responseWriter.Header().Set("Content-Type", contentTypeTextHTML)
fmt.Fprint(responseWriter, GetRequestTable(httpRequest))
}
GetRequestTable
This function calls the ParseTemplate function which generates the Request Table template as a string and then returns it to the calling function.
func GetRequestTable(httpRequest *http.Request) string {
data := map[string]any{
"requestMethod": httpRequest.Method,
"httpHeader": httpRequest.Header,
}
return ParseTemplate("request-table", data)
}
ParseTemplate
This function generates a template as a string using the templateName parameter and Data parameter if passed. It uses the html/template package to do this. It includes two helper functions: “hasPrefix,” which is used in the Request Table template to filter HTMX Headers preceded by ‘Hx’, and “rawHTML,” which is used in the Main template to output HTML without being escaped. The templates are read from the “templates” directory.
Any generated errors are deliberately included in the returned string for simplicity as this is a proof of concept example and because, in this specific use case, the errors would be output straight to the display anyway. In a production application or even something more permanent you should probably return errors as a separate return parameter to be dealt with accordingly by the calling function.
func ParseTemplate(templateName string, Data ...map[string]any) string {
// optional data parameter
data := map[string]any{}
if Data != nil && Data[0] != nil {
data = Data[0]
}
var err error
var tmpl *template.Template
var outputBuffer bytes.Buffer
var outputString string
filename := fmt.Sprintf(`%s.html`, templateName)
filePath := fmt.Sprintf(`templates/%s`, filename)
tmpl, err = template.New(filename).Funcs(template.FuncMap{
"hasPrefix": strings.HasPrefix,
"rawHTML": func(htmlString string) template.HTML {
return template.HTML(htmlString)
},
}).ParseFiles(filePath)
if err == nil {
err = tmpl.Execute(&outputBuffer, data)
if err == nil {
outputString = outputBuffer.String()
}
}
if err != nil {
outputString = err.Error() // deliberately added to output string
}
return outputString
}
Javascript
HTMX does use javascript which still needs to be enabled in the browser but you do not have write any javascript code yourself in order to use it. Instead, tag attributes are embedded in your HTML code to define the requests to the server."
This application uses HTMX version 1.9.9 downloaded and saved in the “javascript” directory.
Templates
The following templates saved in the “templates” directory are used.
main.html
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.title}}</title>
<script src="javascript/htmx199.min.js"></script>
<style>
body { background-color: #DDD; font-family: monospace; font-size: large; }
table { border-collapse: collapse; border-spacing: 0; }
th, td { border: 1px solid black; padding: 8px; background-color: #EEE; text-align: left; }
button
{
display: inline-block;
background-color: #CCC;
color: black;
padding: 4px;
text-align: center;
text-decoration: none;
border: 1px solid black;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h3>{{.title}}</h3>
<button hx-get="/request-table" hx-target="#requestTable">get</button>
<button hx-post="/request-table" hx-target="#requestTable">post</button>
<button hx-put="/request-table" hx-target="#requestTable">put</button>
<button hx-patch="/request-table" hx-target="#requestTable">patch</button>
<button hx-delete="/request-table" hx-target="#requestTable">delete</button>
<br><br>
<table id="requestTable">{{- if .requestTable -}}{{ rawHTML .requestTable }}{{- end -}}</table>
</body>
</html>
request-table.html
<tr><th>Name</th><th>Value</th></tr>
<tr><td>Request Method</td><td>{{.requestMethod}}</td></tr>
{{- range $headerName, $headerValue := .httpHeader -}}
{{- if hasPrefix $headerName "Hx" -}}
<tr><td>{{$headerName}}</td><td>{{index $headerValue 0}}</td></tr>
{{- end -}}
{{- end -}}
Further Reading
More information about about HTMX can be found on the HTMX Website.
More information about the net/http, html/template and other packages can be found on the Go Website.