Go, Templ, and HTMX: Building Type-safe Full-stack Web Apps without JS Frameworks

Development tutorial - IT technology blog
Development tutorial - IT technology blog

The Pain of “Type Mismatch” Between Frontend and Backend

Imagine this scenario: You just changed a database field from user_id (string) to ID (int). You update the Go backend but forget the React frontend. The result? A runtime error occurs right in production because TypeScript couldn’t catch the change in the returned API response.

Maintaining two sets of type definitions at both ends of a project is a real burden. For administrative apps (Admin Dashboards) or basic CRUD forms, dragging in megabytes of JavaScript libraries, handling CORS, and managing complex state often wastes resources. The trio of Go, Templ, and HTMX emerges as an alternative solution: bringing all logic back to the server while maintaining a smooth Single Page Application (SPA) experience.

Why is this trio so effective?

This combination directly addresses performance and source code safety issues:

  • Go: Handles logic with high performance and excellent concurrency capabilities.
  • HTMX (~14KB): Instead of using React or Vue, HTMX allows you to perform AJAX and WebSockets directly via HTML attributes. You can update parts of a webpage without writing a single line of JavaScript.
  • Templ: This is the most crucial link. Templ lets you write HTML using syntax very similar to Go and compiles it into pure Go code. If you pass the wrong data type into a component, the compiler will throw an error immediately.

Hands-on: Building a Type-safe Todo List App

We will build a small application to see how data flows safely from business logic all the way to the user interface.

1. Environment Setup

First, initialize the project and install the necessary tools:

mkdir go-htmx-demo && cd go-htmx-demo
go mod init go-htmx-demo

# Install Templ CLI to generate code
go install github.com/a-h/templ/cmd/templ@latest

# Add Echo Framework to handle routing
go get github.com/labstack/echo/v4
go get github.com/a-h/templ

2. Defining Components with Templ

Create a components.templ file. The difference here is that you can use Go structs directly within the HTML.

package main

type Todo struct {
    ID   int
    Task string
}

templ Page(todos []Todo) {
    <!DOCTYPE html>
    <html>
    <head>
        <title>Go + Templ + HTMX</title>
        <script src="https://unpkg.com/[email protected]"></script>
        <script src="https://cdn.tailwindcss.com"></script>
    </head>
    <body class="bg-slate-50 p-10">
        <div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-sm">
            <h1 class="text-xl font-bold mb-4">My Tasks</h1>
            
            <form hx-post="/add" hx-target="#todo-list" hx-swap="beforeend" class="flex gap-2 mb-4">
                <input type="text" name="task" class="border rounded px-2 py-1 flex-1" />
                <button type="submit" class="bg-indigo-600 text-white px-4 py-1 rounded">Add</button>
            </form>

            <ul id="todo-list" class="divide-y">
                for _, todo := range todos {
                    @TodoItem(todo)
                }
            </ul>
        </div>
    </body>
    </html>
}

templ TodoItem(todo Todo) {
    <li class="py-2 text-gray-700">{ todo.Task }</li>
}

The hx-post="/add" attribute signals to HTMX: upon submission, send a request to the server and append the returned result to the end of the #todo-list.

3. Backend Processing

In the main.go file, we connect the data logic with the defined interface.

func main() {
    e := echo.New()
    todos := []Todo{{ID: 1, Task: "Learn Go"}}

    e.GET("/", func(c echo.Context) error {
        return Page(todos).Render(c.Request().Context(), c.Response().Writer)
    })

    e.POST("/add", func(c echo.Context) error {
        newTodo := Todo{ID: len(todos) + 1, Task: c.FormValue("task")}
        todos = append(todos, newTodo)
        // Return only the TodoItem component, without reloading the whole page
        return TodoItem(newTodo).Render(c.Request().Context(), c.Response().Writer)
    })

    e.Logger.Fatal(e.Start(":8080"))
}

To run the application, you need to run templ generate first to convert the template files into standard Go code.

Real-world Experience: When to Choose This Stack?

During development, handling raw data from databases or third-party APIs can be messy. I often use the JSON Formatter at toolcraft.app to quickly check the data structure before mapping it to a Go struct. This helps minimize logic errors right from the modeling stage.

The biggest benefit of this approach is the Single Source of Truth. If you change a field in the Todo struct, the compiler will flag errors everywhere you use that template. You no longer have to worry about data “skew” between the frontend and backend as you do with typical REST or GraphQL workflows.

Conclusion

Go, Templ, and HTMX are not a step back to outdated SSR technologies. This is a modern approach that simplifies the development workflow. You get the safety of a type-safe system, the performance of Go, and the smooth experience of HTMX without managing thousands of complex JavaScript files. If your project prioritizes development speed and stability, give this stack a try.

Share: