The GOaT Stack: Why Go + HTMX + Tailwind is the Future of Simple Web Apps

February 15, 2026


There’s a mass exodus happening in web development. Developers who spent years wrestling with React build configs, debugging hydration mismatches, and waiting for node_modules to finish installing are quietly walking away. They’re not going back to PHP (well, some are). They’re picking up something simpler.

I call it the GOaT stack: Go + HTMX + Tailwind CSS.

It’s not new technology. It’s old ideas, executed well, with modern ergonomics. And after building Faria CMS with it, I’m convinced it’s the right tool for a huge category of web apps that we’ve been dramatically over-engineering.

What is the GOaT Stack?

The GOaT stack is three things:

  • Go — your backend, your templates, your server. One language, one binary.
  • HTMX — HTML attributes that let your server return HTML fragments instead of JSON. No client-side framework needed.
  • Tailwind CSS — utility-first CSS you can drop in via CDN. No build step required.

That’s it. No webpack. No Vite. No package.json. No virtual DOM. No state management library. No hydration. No ISR, SSG, SSR acronym soup.

You write Go. You return HTML. You style it with classes. You ship a single binary.

Why Go?

Simplicity as a Feature

Go is boring in the best possible way. There’s one way to do most things. The standard library handles HTTP servers, templating, JSON, cryptography, and more — without reaching for third-party packages.

func main() {
    http.HandleFunc("/", handleHome)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

That’s a web server. No framework. No middleware stack. No dependency injection container. It just works.

Performance You Don’t Have to Think About

Go compiles to native code. A typical Go web app handles tens of thousands of requests per second on modest hardware. You’re not paying the interpreter tax of Python or Ruby, and you’re not dealing with the event loop gymnastics of Node.js.

For most apps, Go is fast enough that you never have to think about performance. That’s a superpower.

One Binary to Rule Them All

go build gives you a single static binary. Copy it to a server. Run it. That’s your deployment.

No runtime to install. No Docker required (though it works great with a FROM scratch Dockerfile). No “works on my machine” because there’s nothing to configure on the machine.

GOOS=linux GOARCH=amd64 go build -o myapp .
scp myapp server:/usr/local/bin/
ssh server "systemctl restart myapp"

Three commands. You’re deployed.

Templates Are Built In

Go’s html/template package is production-ready out of the box. It auto-escapes HTML, supports template composition, and integrates naturally with your Go types.

tmpl := template.Must(template.ParseFiles("base.html", "home.html"))
tmpl.ExecuteTemplate(w, "base", data)

No JSX. No template language with its own build step. Just HTML with {{.Variables}}.

Why HTMX Over React/Vue?

This is the spicy take, so let me be precise about what I mean.

The Hypermedia Approach

HTMX returns web development to its roots: the server returns HTML, and the browser renders it. The difference from 2005 is that HTMX lets you update parts of the page without a full reload.

<button hx-get="/api/todos" hx-target="#todo-list" hx-swap="innerHTML">
    Load Todos
</button>

<div id="todo-list">
    <!-- Server-rendered HTML goes here -->
</div>

Click the button. The server returns an HTML fragment. HTMX swaps it into #todo-list. No JavaScript written. No JSON parsed. No state synchronized.

What You’re Not Writing

With a React/Vue SPA, a “load todos” feature requires:

  1. A REST or GraphQL API endpoint that returns JSON
  2. A client-side fetch call
  3. State management (useState, Vuex, Redux, Zustand, Pinia…)
  4. A component that maps JSON → JSX/template
  5. Loading states, error states, optimistic updates
  6. Type definitions on both sides if you’re using TypeScript

With HTMX + Go:

  1. A Go handler that returns an HTML fragment

That’s not an exaggeration. Your Go handler queries the database, passes the result to a template, and writes HTML to the response. The same code path as a full page render, just returning a fragment instead.

14KB of JavaScript

HTMX is ~14KB minified and gzipped. React + ReactDOM is ~140KB. Add a router, state management, and a UI library and you’re shipping 500KB+ of JavaScript before your app does anything.

<script src="https://unpkg.com/htmx.org@2.0.4"></script>

One script tag. Done.

No Build Step

There is no npm run build. There is no bundler. There is no tree-shaking, code-splitting, or chunk optimization because there’s nothing to bundle. Your JavaScript is a single <script> tag pointing to a CDN.

This isn’t just a developer experience win. It’s an operational win. Your CI/CD pipeline is go build && deploy. There’s no Node.js version to manage, no package-lock.json conflicts, no “npm audit found 47 vulnerabilities” to ignore.

Why Tailwind?

Utility-First Just Works

If you’ve used Tailwind, you know. If you haven’t, here’s the pitch: instead of writing CSS in a separate file and naming classes, you compose styles directly in your HTML.

<div class="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
    <h1 class="text-2xl font-bold text-gray-900 mb-4">Hello, GOaT</h1>
    <p class="text-gray-600 leading-relaxed">
        This is styled without writing a single line of CSS.
    </p>
</div>

Your styles live next to your markup. You never context-switch to a CSS file. You never invent class names. You never wonder if .card-wrapper-inner-container is used anywhere else.

CDN Mode = No Build Step

For the GOaT stack, Tailwind via CDN is the move:

<script src="https://cdn.tailwindcss.com"></script>

Yes, the Tailwind team recommends the CLI for production. And for large apps, you should use it. But for most GOaT stack apps? The CDN is fine. It’s a 300KB script that JIT-compiles your utility classes in the browser.

For production optimization, you can always add the Tailwind CLI later. But you can start without any build tooling at all, and that matters more than people think.

Pairs Perfectly with Server-Rendered HTML

Tailwind was designed for component-based architectures, but it works beautifully with Go templates. Each template partial is effectively a component, and the styles travel with the markup.

No CSS-in-JS runtime. No styled-components. No CSS modules. Just classes in HTML, exactly like the web was designed for.

All Three Together: A Complete Example

Here’s a minimal todo app showing the full GOaT stack in action.

main.go:

package main

import (
    "html/template"
    "log"
    "net/http"
    "sync"
)

var (
    todos []string
    mu    sync.Mutex
    tmpl  = template.Must(template.ParseGlob("templates/*.html"))
)

func main() {
    http.HandleFunc("/", handleIndex)
    http.HandleFunc("/todos", handleAddTodo)
    log.Println("GOaT app running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    defer mu.Unlock()
    tmpl.ExecuteTemplate(w, "index.html", todos)
}

func handleAddTodo(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    todo := r.FormValue("todo")
    if todo != "" {
        mu.Lock()
        todos = append(todos, todo)
        mu.Unlock()
    }
    mu.Lock()
    defer mu.Unlock()
    tmpl.ExecuteTemplate(w, "todo-list.html", todos)
}

templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>GOaT Todo</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
    <div class="bg-white rounded-xl shadow-lg p-8 w-full max-w-md">
        <h1 class="text-3xl font-bold text-gray-800 mb-6">🐐 GOaT Todos</h1>

        <form hx-post="/todos" hx-target="#todo-list" hx-swap="innerHTML"
              class="flex gap-2 mb-6">
            <input type="text" name="todo" placeholder="What needs doing?"
                   class="flex-1 px-4 py-2 border border-gray-300 rounded-lg
                          focus:outline-none focus:ring-2 focus:ring-blue-500">
            <button type="submit"
                    class="px-6 py-2 bg-blue-600 text-white rounded-lg
                           hover:bg-blue-700 transition-colors">
                Add
            </button>
        </form>

        <div id="todo-list">
            {{template "todo-list.html" .}}
        </div>
    </div>
</body>
</html>

templates/todo-list.html:

<ul class="space-y-2">
    {{range .}}
    <li class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
        <span class="w-2 h-2 bg-blue-500 rounded-full"></span>
        <span class="text-gray-700">{{.}}</span>
    </li>
    {{else}}
    <li class="text-gray-400 text-center py-4">No todos yet. Add one above!</li>
    {{end}}
</ul>

That’s a complete, interactive web app. No node_modules. No bundler. No framework. Just Go, HTML with HTMX attributes, and Tailwind classes.

Run it with go run main.go and you have a live, interactive todo app.

When to Use the GOaT Stack

It’s great for:

  • Internal tools and admin panels
  • Content management systems (like Faria CMS!)
  • CRUD apps with moderate interactivity
  • Prototypes and MVPs
  • Personal projects and side hustles
  • Apps where operational simplicity matters
  • Teams that are tired of JavaScript fatigue

Think twice if you need:

  • Real-time collaborative editing (Google Docs-style)
  • Complex client-side state (drag-and-drop builders, spreadsheet UIs)
  • Offline-first applications
  • Heavy animations and transitions
  • A mobile app from the same codebase (React Native, etc.)

The GOaT stack isn’t anti-JavaScript. It’s anti-unnecessary JavaScript. If your app genuinely needs rich client-side interactivity, use the right tool. But be honest — most apps don’t. Most apps are forms, lists, and dashboards. And for those, the GOaT stack is, well, the GOAT.

What’s Next

In Part 2, we’ll build something real with the GOaT stack — a full CRUD app with authentication, database integration, and deployment. We’ll go from go mod init to running in production.

Until then, try it yourself. Create a main.go, add two <script> tags, and see how far you get before you miss your bundler.

I bet you won’t miss it at all. 🐐