Go, Templ và HTMX: Xây dựng Web Full-stack Type-safe mà không cần JS Framework

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

Nỗi đau mang tên “lệch Type” giữa Frontend và Backend

Hãy tưởng tượng kịch bản này: Bạn vừa đổi một field trong database từ user_id (string) sang ID (int). Bạn cập nhật Backend Go, nhưng quên mất Frontend React. Kết quả? Runtime error xảy ra ngay trên production vì TypeScript không thể bắt được sự thay đổi từ API trả về.

Duy trì hai bộ định nghĩa Type ở cả hai đầu dự án là một gánh nặng thực sự. Với các ứng dụng quản lý (Admin Dashboard) hoặc form CRUD cơ bản, việc kéo theo hàng MB thư viện JavaScript, xử lý CORS và quản lý State phức tạp thường gây lãng phí tài nguyên. Bộ ba Go, Templ và HTMX xuất hiện như một giải pháp thay thế: Đưa mọi logic về lại Server nhưng vẫn giữ được trải nghiệm mượt mà như Single Page Application (SPA).

Tại sao bộ ba này lại hiệu quả?

Sự kết hợp này giải quyết trực tiếp bài toán hiệu suất và tính an toàn của mã nguồn:

  • Go: Đảm nhận vai trò xử lý logic với hiệu năng cao và khả năng thực thi đồng thời (concurrency) tuyệt vời.
  • HTMX (~14KB): Thay vì dùng React hay Vue, HTMX cho phép bạn thực hiện AJAX, WebSockets trực tiếp qua HTML attributes. Bạn có thể cập nhật một phần trang web mà không cần viết một dòng JavaScript nào.
  • Templ: Đây là mắt xích quan trọng nhất. Templ giúp bạn viết HTML bằng cú pháp gần giống Go và compile chúng thành mã Go thuần. Nếu bạn truyền sai kiểu dữ liệu vào component, trình biên dịch sẽ báo lỗi ngay lập tức.

Thực hành: Build ứng dụng Todo List chuẩn Type-safe

Chúng ta sẽ xây dựng một ứng dụng nhỏ để thấy cách dữ liệu luân chuyển an toàn từ logic nghiệp vụ đến tận giao diện người dùng.

1. Thiết lập môi trường

Đầu tiên, hãy khởi tạo dự án và cài đặt các công cụ cần thiết:

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

# Cài đặt Templ CLI để generate code
go install github.com/a-h/templ/cmd/templ@latest

# Thêm Echo Framework để handle routing
go get github.com/labstack/echo/v4
go get github.com/a-h/templ

2. Định nghĩa Component với Templ

Tạo file components.templ. Điểm khác biệt ở đây là bạn có thể sử dụng các struct của Go trực tiếp trong 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>
}

Thuộc tính hx-post="/add" báo hiệu cho HTMX biết: Khi submit, hãy gửi request đến server và chèn kết quả trả về vào cuối danh sách #todo-list.

3. Xử lý Backend

Tại file main.go, chúng ta kết nối logic dữ liệu với giao diện đã định nghĩa.

func main() {
    e := echo.New()
    todos := []Todo{{ID: 1, Task: "Học 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)
        // Trả về duy nhất component TodoItem, không load lại cả trang
        return TodoItem(newTodo).Render(c.Request().Context(), c.Response().Writer)
    })

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

Để chạy ứng dụng, bạn cần chạy templ generate trước để chuyển đổi file template thành code Go chuẩn.

Kinh nghiệm thực tế: Khi nào nên chọn Stack này?

Trong quá trình phát triển, việc xử lý dữ liệu thô từ database hoặc API bên thứ ba thường khá rối rắm. Mình thường sử dụng JSON Formatter tại toolcraft.app để nhanh chóng kiểm tra cấu trúc dữ liệu trước khi map vào struct Go. Việc này giúp giảm thiểu lỗi logic ngay từ bước thiết kế mẫu (modeling).

Lợi ích lớn nhất của hướng đi này là Single Source of Truth. Nếu bạn thay đổi một field trong struct Todo, trình biên dịch sẽ báo lỗi ở mọi nơi bạn sử dụng template đó. Bạn không còn phải lo lắng về việc “vênh” dữ liệu giữa Frontend và Backend như khi làm việc với REST hay GraphQL thông thường.

Lời kết

Go, Templ và HTMX không phải là sự thụt lùi về công nghệ SSR cũ kỹ. Đây là cách tiếp cận hiện đại giúp tối giản hóa quy trình phát triển. Bạn nhận được sự an toàn của hệ thống Type-safe, hiệu suất của Go và trải nghiệm mượt mà của HTMX mà không cần quản lý hàng nghìn file JavaScript phức tạp. Nếu dự án của bạn ưu tiên tốc độ phát triển và sự ổn định, hãy thử trải nghiệm stack này.

Share: