Problem: JavaScript Fatigue is Real
You want to build an interactive web app but don't want to spend 30 minutes configuring webpack, dealing with node_modules, or learning the latest React meta-framework.
You'll learn:
- How HTMX replaces React/Vue with HTML attributes
- Why Go's stdlib is enough for most web apps
- How to ship production apps with zero build steps
Time: 20 min | Level: Beginner
Why This Works
HTMX lets you add interactivity by swapping HTML fragments instead of building a JSON API + SPA. Go compiles to a single binary and serves HTML templates natively.
What you skip:
- npm/yarn/pnpm installations
- Bundler configuration (webpack/vite/esbuild)
- State management libraries
- Virtual DOM reconciliation
Trade-off: Less control over animations and complex UI state compared to React. Perfect for CRUD apps, dashboards, and forms.
Solution
Step 1: Install Go
# macOS/Linux
brew install go
# Or download from go.dev
# Verify installation
go version # Should show 1.23+
Expected: go version go1.23.x darwin/arm64 (or your OS/arch)
Step 2: Create a Minimal Go Server
// main.go
package main
import (
"html/template"
"log"
"net/http"
)
func main() {
// Serve static files (htmx.js, css)
http.Handle("/static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("static"))))
// Routes
http.HandleFunc("/", handleHome)
http.HandleFunc("/click", handleClick)
log.Println("Server running on http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
func handleHome(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("templates/index.html"))
tmpl.Execute(w, nil)
}
func handleClick(w http.ResponseWriter, r *http.Request) {
// HTMX expects HTML fragments, not JSON
w.Write([]byte(`<p class="success">Button clicked at server!</p>`))
}
Why this works: No frameworks needed. Go's stdlib handles routing, templates, and static files out of the box.
Step 3: Add HTMX to Your HTML
<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTMX Demo</title>
<!-- HTMX from CDN - no build step -->
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h1>No-Build Stack Demo</h1>
<!-- HTMX magic: POST to /click, swap response into #result -->
<button
hx-post="/click"
hx-target="#result"
hx-swap="innerHTML">
Click Me
</button>
<div id="result"></div>
</body>
</html>
What's happening:
hx-postsends a POST request on clickhx-targetspecifies where to put the responsehx-swapcontrols how to insert it (innerHTML, outerHTML, etc.)
No JavaScript written. No event listeners. Just HTML attributes.
Step 4: Add Basic Styling
/* static/style.css */
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
}
button {
background: #1976d2;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background: #1565c0;
}
.success {
color: #2e7d32;
margin-top: 1rem;
}
Step 5: Run Your App
# Create directory structure
mkdir -p static templates
# Run the server
go run main.go
Expected: Server starts, visit http://localhost:8080, click button, see response appear without page reload.
If it fails:
- Port already in use: Change
:8080to:8081in main.go - Template not found: Verify
templates/index.htmlexists - 404 on /static/style.css: Check
static/style.csspath
Real-World Example: Todo List
Here's how to build a functional todo app:
// Add to main.go
type Todo struct {
ID int
Text string
Done bool
}
var todos = []Todo{
{ID: 1, Text: "Learn HTMX", Done: false},
}
func handleAddTodo(w http.ResponseWriter, r *http.Request) {
text := r.FormValue("todo")
newTodo := Todo{
ID: len(todos) + 1,
Text: text,
Done: false,
}
todos = append(todos, newTodo)
// Return just the new todo HTML
tmpl := `<li id="todo-{{.ID}}">
<input type="checkbox" hx-put="/toggle/{{.ID}}" hx-target="#todo-{{.ID}}">
{{.Text}}
</li>`
t := template.Must(template.New("todo").Parse(tmpl))
t.Execute(w, newTodo)
}
<!-- In templates/index.html -->
<form hx-post="/add-todo" hx-target="#todo-list" hx-swap="beforeend">
<input type="text" name="todo" placeholder="New todo..." required>
<button type="submit">Add</button>
</form>
<ul id="todo-list">
<!-- Todos appear here -->
</ul>
How it works:
- User submits form → HTMX sends POST to
/add-todo - Server returns HTML for one
<li>element - HTMX appends it to
#todo-list(no page reload)
Verification
Test the full flow:
# Terminal 1: Run server
go run main.go
# Terminal 2: Test with curl
curl -X POST http://localhost:8080/click
# Should return: <p class="success">Button clicked at server!</p>
You should see: HTML response, not JSON. The server returns ready-to-render HTML.
When to Use This Stack
✅ Great for:
- Internal dashboards
- CRUD applications
- Forms-heavy apps
- MVPs and prototypes
- Teams without frontend specialists
❌ Not ideal for:
- Complex animations (use Framer Motion/GSAP)
- Offline-first apps (needs service workers)
- Real-time collaboration (needs WebSockets + complex state)
- Heavy data visualization (use D3.js/Chart.js)
What You Learned
- HTMX uses HTML attributes to trigger server requests
- Go's stdlib handles routing, templates, and static files
- No build step means instant deployments
- Server returns HTML fragments, not JSON
Limitation: You're trading client-side flexibility for simplicity. Complex UI state still needs JavaScript.
Production Checklist
Before deploying:
- Add CSRF protection (
gorilla/csrf) - Use
embedpackage for templates (single binary) - Add rate limiting on POST endpoints
- Serve HTMX from your domain (not CDN)
- Add proper error handling templates
- Use
http.Serverwith timeouts
// Production server setup
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(srv.ListenAndServe())
Comparison: HTMX vs React
| Aspect | HTMX + Go | React + Node |
|---|---|---|
| Build time | 0s (no build) | 30-120s |
| Bundle size | 14KB (HTMX) | 150KB+ (React + libs) |
| Learning curve | 1 day | 1-2 weeks |
| SEO | Perfect (server-rendered) | Needs SSR framework |
| Deployment | Single binary | Node server + build artifacts |
Tested on Go 1.23.5, HTMX 2.0.0, macOS Sonoma & Ubuntu 24.04