Build Web Apps Without npm in 20 Minutes

Use HTMX + Go to create interactive web apps with zero JavaScript bundling. No webpack, no node_modules, just HTML.

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-post sends a POST request on click
  • hx-target specifies where to put the response
  • hx-swap controls 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 :8080 to :8081 in main.go
  • Template not found: Verify templates/index.html exists
  • 404 on /static/style.css: Check static/style.css path

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:

  1. User submits form → HTMX sends POST to /add-todo
  2. Server returns HTML for one <li> element
  3. 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 embed package for templates (single binary)
  • Add rate limiting on POST endpoints
  • Serve HTMX from your domain (not CDN)
  • Add proper error handling templates
  • Use http.Server with 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

AspectHTMX + GoReact + Node
Build time0s (no build)30-120s
Bundle size14KB (HTMX)150KB+ (React + libs)
Learning curve1 day1-2 weeks
SEOPerfect (server-rendered)Needs SSR framework
DeploymentSingle binaryNode server + build artifacts

Tested on Go 1.23.5, HTMX 2.0.0, macOS Sonoma & Ubuntu 24.04