DEV Community

Cover image for Go full stack web app tutorial with sqlc and htmx. Part 1
Sergey Lymar
Sergey Lymar

Posted on

Go full stack web app tutorial with sqlc and htmx. Part 1

Introduction

In this series, that I create for my own learning and enjoyment, I will be building a simple web application using Go, sqlc, goose, htmx, and Tailwind CSS. The goal is to create a responsive and accessible web application that is easy to use and understand. My approach is probably flawed and not robust enough to be valuable for seasoned Go developers. But it is my learning journey, and I hope it will be helpful for others who are just starting out with Go and web development.

Project Details

Tis series will be focused on building a simple web application using Go, HTMX, sqlc with PostgreSQL, goose and Tailwind CSS for styling (I don't know why, but it seems that the fabric of the universe itself demands any web application to have Tailwind nowadays, so here it is).

  • sqlc – Generate type-safe Go from SQL.
  • Goose – Database migrations.
  • htmx – Modern web development with HTML.
  • Tailwind CSS – Utility-first CSS.

This list is not exhaustive, but it covers the main technologies that will be used in this series. I may decide to throw bun in as well. And I think that we will have to use some kind of web framework in some point in the future, at least something minimal like Chi. Apart from that I will try to use as minimal different libraries or frameworks as possible, as I am trying to learn Go, not a framework.

The goal is to create a responsive and accessible web application that will help us to cover a variety of topics related to web development with Go. So here we have it: "Warehouse Management Application". Let us begin!

Application Goals

Let us first define what we want to achieve with this application.

  1. The main goal is to create a simple web application that allows users to manage their warehouse inventory. This includes adding, updating, and deleting items from the inventory, as well as viewing the current inventory levels.
  2. The application should be responsive and accessible, with a clean and intuitive user interface.
  3. The application should be secure and reliable, with proper error handling and data validation.
  4. The application should be scalable and maintainable, with a modular architecture and proper separation of concerns.
  5. The application should implement authentication and authorization to ensure that only authorized users can access and modify the inventory.
  6. The application should implement a search, sorting, filtering and pagination features to allow users to easily work with the inventory.
  7. The application should notify users of any changes to the inventory, such as low stock levels or new items added.
  8. The application should provide a way for users to track the history of changes made to the inventory.

The list is already too long, but we will cover all these topics in separate articles. We will start with the project setup and then move on to the database setup, authentication, and authorization and so on. So let us start with the project setup.

Prerequisites

This article series will assume that you have a basic understanding of Go. If you are new to it, you may still follow along, but you may need to learn some basics first. You can find a good introduction to Go at https://tour.golang.org/. We will also use sqlc, that requires familiarity with SQL. Web technologies familiriaty such as HTML, CSS, and JavaScript are also required.

Hello World

First order of business is to set up the project. We will use Go as our programming language, HTMX for client-side interactivity, sqlc for database access, goose for database migrations, and Tailwind CSS for styling. Let us first of all create a new directory for our project and initialize a new Go module. We will create it with a name github.com/zaggy/go-htmx-tutorial, this is where the source code will be stored. The code for each article will be stored in a separate tagged git commit. It may be cloned by running git clone https://github.com/zaggy/go-htmx-tutorial.git and then checkout the appropriate tag, like git checkout 01-Project-Setup. In each article, I will put the git tag in the beginnin of the article. You may use any other name you prefer, but it is somehow a convention in the Go community to have as a module name a full address of where the source code is stored.

mkdir go-htmx-tutorial
cd go-htmx-tutorial
go mod init github.com/zaggy/go-htmx-tutorial
Enter fullscreen mode Exit fullscreen mode

After that we need to create a package called main which will be the entry point of our application. We will create a file called main.go inside the main package.

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, World!")
}
Enter fullscreen mode Exit fullscreen mode

Now, we can run our application by executing the following command:

go run main.go
Enter fullscreen mode Exit fullscreen mode

And we should see the output:

Hello, World!
Enter fullscreen mode Exit fullscreen mode

Wait. Are you confused? I was. What is going on here? What modules? What packages? Let us break it down.

Go Modules and Packages

Go modules are a way to manage dependencies in Go projects. Here is a great article, and another one. They allow you to import and use packages from other projects (and this is exactly why we need to have our module initialized, otherwise we cannot import any packages), and they also allow you to share your own module with others.

Go packages are a way to organize code into reusable units. They allow you to group related code together and to import and use code from other packages. And this is where it matters. Seasoned Go developers will say that in order to write idiomatic Go code, you should start thinking about your code packages. In our application we already know that we will have several packages such like models, db, routes, utils, etc. We may be tempted to just create a bunch of these packages in root of our project and it may be done. Our application will be perfectly fine. But it is not idiomatic. And we should always remember that other people will eventually look into our codebase, so we should from the first lines of our application try to make it easier for them to understand our codebase and start contributing to it as fast as possible.

Hence, we should look for conventions and best practices when creating packages. Here is have what the official docs say: Organizing a Go module. Following these conventions we will have the following structure:

├── go.mod
├── go.sum
├── cmd
│   └── main.go
└── internal
    ├── models
    │   └── models.go
    ├── db
    │   └── db.go
    ├── routes
    │   └── routes.go
    └── utils
        └── utils.go
Enter fullscreen mode Exit fullscreen mode

We will have one executable file in the cmd directory, and all other packages will be in the internal directory, because we do not want to expose our internal packages to the outside world. But where should we store our web files? Where would our docker-related files go? Is there a convention for that? Turns out there is. We should store our web files in the web directory, and our docker-related files in the docker directory. This is described in a community supported Github project called Standard Go Project Layout. So, our final structure will look like this:

├── cmd/               # Entry point of our application
├── internal/          # Internal business logic (not accessible externally)
│   ├── db/            # Database-related code
│   ├── models/        # Data structures
│   ├── routes/        # HTTP handlers and routing logic
│   └── templates/     # HTML templates
├── web/               # Frontend assets (CSS, JS, static files)
└── docker/            # Dockerfiles and scripts
Enter fullscreen mode Exit fullscreen mode

So, let us create the necessary directories. We will also create a README.md file in the root directory to document our project.

mkdir web
mkdir docker
mkdir internal
mkdir cmd
Enter fullscreen mode Exit fullscreen mode

Minimal Web Server with Template

Next, let’s set up our Go project structure and make sure everything runs correctly.

Move our main.go file to the cmd directory. Now, let's add some basic functionality for our application. We will use built-in packages for templates and web server capabilities. Add following code to cmd/main.go:

package main

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

type PageData struct {
    PageTitle string
    UserName  string
    Products  []string
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    // Parse and execute template
    tmpl, err := template.ParseFiles("internal/templates/home.html")
    // I know, it is daunting, but handling errors is crucial for robust applications.
    // Here is an interesting article about error handling in Go: 
    // https://go.dev/blog/error-syntax
    if err != nil {
        http.Error(w, "Error loading template", http.StatusInternalServerError)
        log.Println("Template parse error:", err)
        return
    }

    // Initialize data
    data := PageData{
        PageTitle: "Warehouse Management Application - Home",
        UserName:  "Sergey",
        Products:  []string{"UltraTech Wireless Mouse", "Quantum Mechanical Keyboard", "AeroFlex Ergonomic Office Chair"},
    }

    // Execute template with data
    // and use our ResponseWriter to write the response
    err = tmpl.Execute(w, data)
    if err != nil {
        http.Error(w, "Error rendering template", http.StatusInternalServerError)
        log.Println("Template execute error:", err)
    }
}

func main() {
    http.HandleFunc("/", homeHandler)

    log.Println("Server started on http://localhost:8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

And following to internal/templates/home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.PageTitle}}</title>
</head>
<body>
    <h1>Welcome, {{.UserName}}!</h1>
    <p>Here are our products:</p>
    <ul>
        {{range .Products}}
        <li>{{.}}</li>
        {{end}}
    </ul>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now, let’s see what happens when we run go run cmd/main.go, and open web browser at http://localhost:8080. We should see following page:

First Web Page

Let's unwrap what is going on here. First we start with main function. Here, we define a homeHandler function that handles requests to the root URL ("/"). This function then parses and executes the template, passing in the necessary data. The data structure is defined in PageData struct.

Adding interactivity and flare 🎉

At this point, we’ve set up the project structure, created our first template, and served a basic page. In the next step, we’ll create a new package to store our handler functions. Also, we will add Tailwind for styling and htmx for interactivity. But first let us create a new file internal/templates/layout.html that is going to be our layout template.

Side note: I am bad at styling, and it is not what I want to learn really here, so all styles are very AI generated, deal with it 😝.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>{{block "title" .}}Warehouse Management Application{{end}}</title>
        <!--using CDN for simplicity-->
        <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    </head>
    <body class="bg-gray-100 text-gray-900 min-h-screen flex flex-col">
        <header class="bg-sky-700 text-white p-4 shadow-md">
            <div
                class="max-w-4xl mx-auto flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
            >
                <h1 class="text-2xl font-bold">
                    Warehouse Management Application
                </h1>
                <div class="text-sm">
                    <span class="font-semibold">Signed in as:</span>
                    <span
                        >{{if .UserName}}{{.UserName}}{{else}}Guest{{end}}</span
                    >
                </div>
            </div>
        </header>

        <main class="flex-1 w-full">
            <div
                class="max-w-4xl mx-auto mt-6 px-4 flex flex-col gap-6 md:flex-row"
            >
                <nav
                    class="bg-white rounded-lg shadow p-4 w-full md:w-56 space-y-3"
                >
                    <a
                        hx-get="/home"
                        hx-target="#main-content"
                        hx-swap="innerHTML"
                        class="block rounded px-3 py-2 hover:bg-sky-600/20"
                        >Home</a
                    >
                    <a
                        hx-get="/products"
                        hx-target="#main-content"
                        hx-swap="innerHTML"
                        class="block rounded px-3 py-2 hover:bg-sky-600/20"
                        >Products</a
                    >
                </nav>
                <section
                    hx-get="/home"
                    hx-target="#main-content"
                    hx-swap="innerHTML"
                    hx-trigger="load"
                    id="main-content"
                    class="flex-1 bg-white rounded-lg shadow p-4 min-h-[200px]"
                >
                    Waiting for content...
                </section>
            </div>
        </main>

        <footer class="bg-gray-200 text-center text-sm p-3 mt-auto w-full">
            &copy; 2025 Warehouse Company
        </footer>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Key features here:

  • Tailwind CSS: Loaded via CDN for instant styling with utility classes (e.g., bg-sky-700, flex, shadow). This keeps things simple for now.
  • HTMX: Enables dynamic updates without JavaScript. The <section id="main-content"> auto-loads the home page on initial render via hx-trigger="load". Nav links use hx-get to fetch and swap content into #main-content with innerHTML.
  • Template blocks: {{block "title" .}} allows child templates to override the page title if needed.

Partial Templates for Dynamic Content

Our home.html is now a partial—just the content for #main-content, not a full HTML page. Update it to:

<div>
    <p class="text-gray-700 mb-6">
        This is a home page for our Warehouse Management System. It provides a user-friendly interface for managing products, orders, and inventory.
    </p>
    <a
        hx-get="/products"
        hx-target="#main-content"
        hx-swap="innerHTML"
        class="inline-block px-6 py-3 bg-sky-600 text-white font-medium rounded hover:bg-sky-700 transition-colors"
    >
        View Products
    </a>
</div>
Enter fullscreen mode Exit fullscreen mode

Create a new partial internal/templates/products.html to list some dummy products:

<div>
    <p class="text-gray-600 mb-6">Here are our products:</p>
    {{if .Products}}
    <ul
        class="divide-y divide-gray-200 border border-gray-200 rounded-lg overflow-hidden"
    >
        {{range .Products}}
        <li class="px-4 py-3 hover:bg-sky-50 transition">
            <span class="text-gray-800 font-medium">{{.}}</span>
        </li>
        {{end}}
    </ul>
    {{else}}
    <div
        class="p-6 bg-yellow-50 border-l-4 border-yellow-400 text-yellow-700 rounded"
    >
        <p>No products available at the moment.</p>
    </div>
    {{end}}
</div>
Enter fullscreen mode Exit fullscreen mode

These partials use Tailwind for a clean, responsive look and HTMX for the "View Products" link to switch content seamlessly.

Refactor Handlers into a Routes Package

To follow Go best practices, move HTTP handlers to internal/routes/routes.go. This keeps cmd/main.go lean—just bootstrapping the server.

Create internal/routes/routes.go:

package routes

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

var (
    templates *template.Template
)

func RegisterRoutes() error {
    var err error
    templates, err = template.ParseGlob("internal/templates/*.html")
    if err != nil {
        return err
    }
    // Register routes
    http.HandleFunc("/home", homeHandler)
    http.HandleFunc("/products", productsHandler)
    // Register root route
    http.HandleFunc("/", rootHandler)
    return nil
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    data := struct {
        PageTitle string
    }{
        PageTitle: "Warehouse Management Application - Home",
    }

    err := templates.ExecuteTemplate(w, "home.html", data)
    if err != nil {
        http.Error(w, "Error rendering template", http.StatusInternalServerError)
        log.Println("Template rendering error:", err)
    }
}

func productsHandler(w http.ResponseWriter, r *http.Request) {
    data := struct {
        PageTitle string
        Products  []string
    }{
        PageTitle: "Warehouse Management Application - Products",
        Products:  []string{"UltraTech Wireless Mouse", "Quantum Mechanical Keyboard", "AeroFlex Ergonomic Office Chair"},
    }

    err := templates.ExecuteTemplate(w, "products.html", data)
    if err != nil {
        http.Error(w, "Error rendering template", http.StatusInternalServerError)
        log.Println("Template rendering error:", err)
    }
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
    data := struct {
        UserName string
    }{
        UserName: "zaggy",
    }
    err := templates.ExecuteTemplate(w, "layout.html", data)
    if err != nil {
        http.Error(w, "Error rendering template", http.StatusInternalServerError)
        log.Println("Template rendering error:", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Update cmd/main.go to use the routes package:

package main

import (
    "log"
    "net/http"

    "github.com/zaggy/go-htmx-tutorial/internal/routes"
)

func main() {
    err := routes.RegisterRoutes()
    if err != nil {
        log.Fatal(err)
    }

    log.Println("Server started on http://localhost:8080")
    err = http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

What's new:

  • template.ParseGlob("internal/templates/*.html") parses all templates once for efficiency.
  • ExecuteTemplate renders specific templates (e.g., "home.html").
  • Root / renders the full layout; /home and /products render partials for HTMX swaps.

Run and Test

Execute:

go run cmd/main.go
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8080. You should see a responsive dashboard with a sidebar. Click Products — content updates instantly via HTMX, no page reload.

HTMX Demo

Git Tag

git add .
git commit -m "01-Project-Setup: Basic server with HTMX and Tailwind"
git tag 01-Project-Setup
Enter fullscreen mode Exit fullscreen mode

What's Next?

We've got a solid foundation: project structure, templates, routing, Tailwind styling, and HTMX interactivity. Next up: PostgreSQL with Goose migrations and sqlc for type-safe queries.

Top comments (0)