Golang + htmx + Tailwind CSS: Create a Responsive Web Application

CanopasCanopas
5 min read

Background

In today’s web development landscape, JavaScript has long been the language of choice for creating dynamic and interactive web applications.

As a Go developer, what if you don’t want to use Javascript and still implement a responsive web application?

Imagine a sleek to-do list app that updates instantly as you check off tasks without a full-page reload. This is the power of Golang and htmx!

Combining Go and htmx allows us to create responsive and interactive web applications without writing a single line of JavaScript.

In this blog, we will explore how to use htmx and Golang to build web applications. (It can be used with other your favorite platforms, too.)

As a learning, we will implement basic create and delete operations for users.

What is htmx?

htmx is a modern HTML extension that adds bidirectional communication between the browser and the server.

It allows us to create dynamic web pages without writing JavaScript, as it provides access to AJAX, server-sent events, etc in HTML directly.

How htmx works?

  • When a user interacts with an element that has an htmx attribute (e.g., clicks a button), the browser triggers the specified event.

  • htmx intercepts the event and sends an HTTP request to the server-side endpoint specified in the attribute (e.g., hx-get="/my-endpoint").

  • The server-side endpoint processes the request and generates an HTML response.

  • htmx receives the response and updates the DOM according to the hx-target and hx-swap attributes. This can involve:

 — Replacing the entire element’s content.
 — Inserting new content before or after the element.
 — Appending content to the end of the element.

Let’s understand it in more depth with an example.

<button hx-get="/fetch-data" hx-target="#data-container">
   Fetch Data
</button>
<div id="data-container"></div>

In the above code, when the button is clicked:

  1. htmx sends a GET request to /fetch-data.

  2. The server-side endpoint fetches data and renders it as HTML.

  3. The response is inserted into the #data-container element.

Create and delete the user

Below are the required tools/frameworks to build this basic app.

  • Gin (Go framework)

  • Tailwind CSS

  • htmx

Basic setup

  • Create main.go file at the root directory.

main.go

package main

import (
 "fmt"
 "github.com/gin-gonic/gin"
)

func main() {
 router := gin.Default()

 router.Run(":8080")
 fmt.Println("Server is running on port 8080")
}

It sets up a basic Go server, running at port 8080.
Run go run main.go to run the application.

  • Create a HTML file at the root directory, to render the user list.

users.html

<!DOCTYPE html>
<html>
   <head>
      <title>Go + htmx app </title>
      <script src="https://unpkg.com/htmx.org@2.0.0" integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw" crossorigin="anonymous"></script>
      <script src="https://cdn.tailwindcss.com"></script>
   </head>
   <body class="text-center flex flex-col w-full gap-6 mt-10">
      <table id="user-list" class="w-1/2 mx-auto mt-4 border border-gray-300">
         <thead>
            <tr class="border border-gray-300">
               <th class="px-4 py-2">Name</th>
               <th class="px-4 py-2">Email</th>
               <th class="px-4 py-2">Actions</th>
            </tr>
         </thead>
         <tbody>
            {{ range .users }}
            <tr class="border border-gray-300">
               <td class="px-4 py-2">{{ .Name }}</td>
               <td class="px-4 py-2">{{ .Email }}</td>
               <td class="px-4 py-2">
                  <button class="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">Delete</button>
               </td>
            </tr>
            {{ end }}
         </tbody>
      </table>
   </body>
</html>

We have included,

htmx using the script tag — https://unpkg.com/htmx.org@2.0.0

Tailwind CSS with cdn link —
https://cdn.tailwindcss.com

Now, we can use Tailwind CSS classes and render the templates with htmx.

As we see in users.html, we need to pass users array to the template, so that it can render the users list.

For that let’s create a hardcoded static list of users and create a route to render users.html .

Fetch users

main.go

package main

import (
 "fmt"
 "net/http"
 "text/template"

 "github.com/gin-gonic/gin"
)

func main() {
 router := gin.Default()

 router.GET("/", func(c *gin.Context) {
  users := GetUsers()

  tmpl := template.Must(template.ParseFiles("users.html"))
  err := tmpl.Execute(c.Writer, gin.H{"users": users})
    if err != nil {
       panic(err)
    }
 })

 router.Run(":8080")
 fmt.Println("Server is running on port 8080")
}

type User struct {
 Name  string
 Email string
}

func GetUsers() []User {
 return []User{
  {Name: "John Doe", Email: "johndoe@example.com"},
  {Name: "Alice Smith", Email: "alicesmith@example.com"},
 }
}

We have added a route / to render the user list and provide a static list of users (to which we will add new users ahead).

That’s all. Restart the server and let’s visit — http://localhost:8080/ to check whether it renders the user list or not. It will render the user list as below.

Rendered users list

Create user

Create file user_row.html. It will be responsible for adding a new user row to the user table.

user_row.html

<tr class="border border-gray-300">
    <td class="px-4 py-2">{{ .Name }}</td>
    <td class="px-4 py-2">{{ .Email }}</td>
    <td class="px-4 py-2">
        <button class="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">Delete</button>
    </td>
</tr>

P.S. — You will notice that it has the same structure as the user table row in users.html . That’s because we want the same styling for the new row we are going to add.

  • Modify users.html. Add the Below code above the <table></table> tags.
<form hx-post="/users" hx-target="#user-list" hx-swap="beforeend">
   <input type="text" name="name" placeholder="Name" class="border border-gray-300 p-2 rounded">
   <input type="email" name="email" placeholder="Email" class="border border-gray-300 p-2 rounded">
   <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add User</button>
</form>
  • hx-post=“/users” — When the form will be submitted, it will trigger post request to /users route.

  • hx-target=“#user-list” —Specify the target where we want to add data.

  • hx-swap=“beforeend”  — Specify the position where want to add data. In our case we want to add new user at the end of list, so we have used beforeend value.

Let’s implement /users(POST) route in the main.go file.

router.POST("/users", func(c *gin.Context) {
  tmpl := template.Must(template.ParseFiles("user_row.html"))

  name := c.PostForm("name")
  email := c.PostForm("email")

  user := User{Name: name, Email: email}
  err := tmpl.Execute(c.Writer, user)
  if err != nil {
   panic(err)
  }
})

It takes the name and email from the form input and executes the user_row.html.

Let’s try to add a new user to the table. Visit http://localhost:8080/ and click the Add User button.

Add new user to the list

Yayy! We’ve successfully added a new user to the list 🎉.

To dive deeper into the detail implementation guide, check out the complete guide at Canopas.


If you like what you read, be sure to hit 💖 button! — as a writer it means the world!

I encourage you to share your thoughts in the comments section below. Your input not only enriches our content but also fuels our motivation to create more valuable and informative articles for you.

Happy coding!👋

1
Subscribe to my newsletter

Read articles from Canopas directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Canopas
Canopas

Unless you’re a Multimillion or a Billion dollar company, you probably don’t have a multimillion-dollar ad budget or professional Spinners. Your product needs to stand out on its own merits like App Quality, Performance, UI design, and User Experience. Most companies don't care about you, your product, and your vision or dreams. They don't give a damn about either their work helped you to get more business, revenue, users, or solving a problem. That's where CANOPAS comes into the picture. Whether you have a GREAT IDEA and you want to turn it into a DIGITAL PRODUCT. OR You need a team that can turn your NIGHTMARES into SWEET DREAMS again by improving your existing product. We help Entrepreneurs, startups, and small companies to bring their IDEA to LIFE by developing digital products for their business. We prefer using Agile and Scrum principles in project management for flexibility and rapid review cycles. We are not bound by technology. We will learn new technology if it significantly improves the performance of your app. We will solve your tech-related problems even though we are not THE EXPERT in it. And we've done it multiple times in the last 7 years. In the last seven years, we helped... A STARTUP to expand its users from 2500 to over 100000 by developing mobile apps for them. An enterprise client to redevelop their app that has 1M+ monthly paid users and 10M+ app downloads. Another enterprise client(5M+ app downloads in each store) to fix bugs and broken parts in the app and as a result, they had over 98% crash-free users. We offer a 100% MONEY BACK GUARANTEE if you don't like our work. No questions asked. Visit : https://canopas.com/blog