DEV Community

Cover image for Adventure: Building with NATS Jetstream KV Store -Part 8
Richard
Richard

Posted on • Edited on

Adventure: Building with NATS Jetstream KV Store -Part 8

Welcome welcome!

Welcome back again to my adventure series on the NATS JetStream Key/Value Store!

Welcome to Part 8 of our series on the NATS JetStream KV Store! We're finally hitting some real development. We're going to cover how Datastar works a bit and use it to hit our endpoints and display some UI!


Where did we leave off?

We had just created multiple boring pages and styled them in our application. Now let's add more!

Routes routes routes

First, let's create our other routes. We will have 3 pages, an Index page at the root, a Dashboard page for game lobbies and then a page for unique Games.

Let's create them!

$ touch routes/dashboard.go routes/game.go
$ touch web/pages/dashboard.templ web/pages/game.templ
Enter fullscreen mode Exit fullscreen mode

After generating the necessary files, the next step is to define the routes and corresponding pages.

Routes

Dashboard.go

package routes

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/rphumulock/ds_nats_ttt/web/pages"
)

func setupDashboardRoute(router chi.Router) error {

    router.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
        pages.Dashboard().Render(r.Context(), w)
    })

    return nil
}
Enter fullscreen mode Exit fullscreen mode

This follows the same routing approach we implemented earlier.

Game.go

package routes

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/rphumulock/ds_nats_ttt/web/pages"
)

func setupGameRoute(router chi.Router) error {

    router.Get("/game/{id}", func(w http.ResponseWriter, r *http.Request) {
        pages.Game().Render(r.Context(), w)
    })

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Similar to the previous route, but with the addition of a parameter in the endpoint URL. This will hold our unique game ids.

Router.go

func SetupRoutes(ctx context.Context, logger *slog.Logger, router chi.Router) (cleanup func() error, err error) {

    cleanup = func() error {
        return errors.Join()
    }

    if err := errors.Join(
        setupIndexRoute(router),
        setupDashboardRoute(router),
        setupGameRoute(router),
    ); err != nil {
        return cleanup, fmt.Errorf("error setting up routes: %w", err)
    }

    return cleanup, nil
}
Enter fullscreen mode Exit fullscreen mode

Pages

Dashboard.templ

package pages

import "github.com/rphumulock/ds_nats_ttt/web/layouts"

templ Dashboard() {
    @layouts.Base() {
        <div class="flex flex-col min-h-screen max-h-screen overflow-hidden">
            <nav class="bg-base-300 text-base-content py-4 shadow-lg">
                <div class="container mx-auto flex items-center justify-between px-6">
                    <div class="text-2xl font-extrabold tracking-widest uppercase text-secondary-content">
                        Tic Tac Toe
                    </div>
                    <h1 class="text-base sm:text-xl font-semibold text-primary-content text-center flex-1">
                        Welcome, 
                    </h1>
                </div>
            </nav>
            <main class="flex flex-col flex-grow w-full p-4 overflow-hidden">
                <div class="flex flex-col sm:flex-row justify-between items-center p-4 bg-accent shadow-md w-full mb-4">
                    <div class="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
                        <button
                            class="btn btn-primary rounded-none flex items-center justify-center text-center text-primary-content px-4 py-2 sm:px-6 sm:py-3 w-full sm:w-auto"
                        >
                            Create Game
                        </button>
                        <button
                            class="btn btn-secondary rounded-none px-4 py-2 sm:px-6 sm:py-3 text-secondary-content w-full sm:w-auto"
                        >
                            Logout
                        </button>
                    </div>
                </div>
                <div
                    id="list-container"
                    class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 w-full overflow-y-auto"
                    style="max-height: 75vh;"
                ></div>
            </main>
        </div>
    }
}
Enter fullscreen mode Exit fullscreen mode

Game.templ

package pages

import "github.com/rphumulock/ds_nats_ttt/web/layouts"

templ Game() {
    @layouts.Base() {
        <div class="flex flex-col min-h-screen max-h-screen overflow-hidden">
            <nav class="bg-base-300 text-base-content py-4 shadow-lg">
                <div class="container mx-auto flex items-center justify-between px-6">
                    <div class="text-xl sm:text-2xl font-extrabold tracking-widest uppercase text-secondary-content">
                        Tic Tac Toe
                    </div>
                </div>
            </nav>
            <main id="main-container" class="flex flex-col flex-grow w-full p-4 h-screen">
                <div class="flex flex-col sm:flex-row justify-between items-center p-4 bg-accent shadow-md w-full gap-3">
                    <div class="text-sm sm:text-lg font-bold text-secondary-content text-center">
                        Game:
                    </div>
                    <div class="text-sm sm:text-lg font-bold text-secondary-content text-center">
                        Host:
                    </div>
                    <div class="text-sm sm:text-lg font-bold text-secondary-content text-center">
                        Challenger: 
                    </div>
                    <div class="flex flex-col sm:flex-row gap-3 w-full sm:w-auto"></div>
                </div>
                <div
                    class="flex flex-grow items-center justify-center bg-base-300 mt-2 overflow-y-auto max-h-[75vh] w-full"
                ></div>
            </main>
        </div>
    }
}
Enter fullscreen mode Exit fullscreen mode

Nice! Nothing too wild here—pretty straightforward again-just HTML. Once we run task live, Templ will do the heavy lifting and generate our templates for us. Easy peasy!

Check it out

We can also see our new pages.

Alright, great. We've got buttons that don’t do anything and pages we have to navigate to manually... yeah, this is not it. Let's fix it.

Don't worry, Enter Datastar


Datastar

Ok so what's Datastar about? On the front of the Datastar site it says...

"Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework."

Oh neat, another front-end framework. Haha, well sort of but not really. Datastar was born from the same ideas that created HTMX. I'm sure you've heard of it. The approach is a bit different than the common SPA's people use such as React.


A Brief Overview

Traditionally, SPAs rely on JavaScript frameworks like React to fetch JSON from an API and dynamically build the UI. While techniques like Server-Side Rendering (SSR) and WebSockets exist, most applications use a standard request-response cycle, where JSON is retrieved and then rendered into the UI.

HTMX

HTMX, on the other hand, changes this paradigm. Instead of fetching JSON and constructing the UI in JavaScript, it directly requests HTML fragments from the server. These fragments are then dynamically inserted into the DOM without a full page reload, providing a smoother, more efficient experience.

This approach resembles traditional multi-page applications (MPAs), but with the advantage of partial page updates rather than full reloads.

Despite this shift, HTMX still operates in a request-response pattern—you're just receiving ready-to-render HTML instead of raw JSON. This allows you to build rich, interactive applications with minimal JavaScript, keeping logic on the server where it belongs.

For a deeper dive into this, check out HyperMedia Systems, an excellent read where one of the authors happens to be the creator of HTMX.

Datastar

Datastar takes this concept a step further. Like HTMX, it delivers HTML fragments that are rendered directly into the DOM, but instead of a pull-based approach, it operates more like a push from the server.

Rather than fetching HTML on demand, Datastar leverages Server-Sent Events (SSE) to stream HTML fragments from the server to the front-end. While you still initiate a request, the response isn’t limited to a single payload—instead, the server can continuously send one-to-many HTML fragments over time.

SSE establishes a however-long-you-want-lived HTTP connection, unlike traditional request-response cycles where the connection closes immediately after the response is received. With SSE, the server decides when to close the connection, allowing for real-time updates without requiring multiple requests from the client.

SSE is quite literally just a different Content-Type Header on the server.

We've got a high-level overview of Datastar—but don’t worry, there will be a dedicated series on that later. Right now, we're here for NATS, dang it!

So, let’s use Datastar to dive back into some NATS goodness! 🚀


Make our UI do stuff!

Let's add some functionality to those buttons using Datastar! First, we need to get Datastar!

Oh snap! That's easy, we can just use a link tag in our header and the CDN. Let's fix our web/layouts/Base.templ and add Datastar.

Base.Templ

package layouts

templ Base() {
    <!DOCTYPE html>
    <html lang="en" data-theme="retro">
        <head>
            <title>Tic+Tac+Toe</title>
            <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
            <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.1/bundles/datastar.js"></script>
            <link href="/static/index.css" rel="stylesheet" type="text/css"/>
            <link rel="icon" type="image/svg+xml" href="/vite.svg"/>
        </head>
        <body class="min-h-screen w-full bg-base-content">
            { children... }
        </body>
    </html>
}
Enter fullscreen mode Exit fullscreen mode

Sweet, let's use it! There's quite a few SDK's created for utilizing Datastar with various languages, but we're sticking with Go!

Let's add the SDK.

$ go get github.com/starfederation/datastar@v1.0.0-beta.5
$ go get github.com/starfederation/datastar/sdk/go@v1.0.0-beta.5
Enter fullscreen mode Exit fullscreen mode

And let's import into our page. We're going to use the alias datastar for it.

package pages

import (
    "github.com/rphumulock/ds_nats_ttt/web/layouts"
    datastar "github.com/starfederation/datastar/sdk/go"
)

templ Index() {
    @layouts.Base() {
        <!-- Full-Screen Container -->
        <div class="flex flex-col min-h-screen max-h-screen overflow-hidden">
            <!-- Navigation Bar -->
            <nav class="bg-base-300 text-base-content py-4 shadow-lg">
                <div class="container mx-auto flex items-center justify-between px-6">
                    <!-- Left: Game Title -->
                    <div class="text-2xl font-extrabold tracking-widest uppercase text-secondary-content">
                        Tic Tac Toe
                    </div>
                </div>
            </nav>
            <!-- Main Content -->
            <main class="min-h-screen flex flex-col items-center justify-center flex-grow gap-6">
            </main>
        </div>
    }
}
Enter fullscreen mode Exit fullscreen mode

This SDK will simply give us syntactic sugar. We'll explain as we add our login functionality.


Login

Let's first think about what we need to do. We want to maintain sessions per User so that we can save Games and Games State on a per User basis.

We will need to login somehow to maintain these sessions. So let's do it.

We're going to add this to our template.

<div id="login" data-on-load={ datastar.GetSSE("/api/index") }></div>
Enter fullscreen mode Exit fullscreen mode

Let's talk about it really quick. What this is doing is tapping into data-attributes which is built into the HTML spec. You can read about them there and we can talk about them more in a Datastar specific series.

Our Datastar data-attribute is data-on-load, which as you make suspect, will do something when the element is loaded into the DOM.

Right away, we’ll make a GET request to an endpoint. Our SDK provides some syntactic sugar with datastar.GetSSE("/api/index"), making the process cleaner.

Under the hood, this function call in our Templ template translates to: "@get('/api/index')".

Without the SDK, we’d just use this string directly—nothing too complex, just a slight convenience. Again, not a big deal, but it keeps things tidy. 🚀

Let's use this in our template.

package pages

import (
    "github.com/rphumulock/ds_nats_ttt/web/layouts"
    datastar "github.com/starfederation/datastar/sdk/go"
)

templ Index() {
    @layouts.Base() {
        <!-- Full-Screen Container -->
        <div class="flex flex-col min-h-screen max-h-screen overflow-hidden">
            <!-- Navigation Bar -->
            <nav class="bg-base-300 text-base-content py-4 shadow-lg">
                <div class="container mx-auto flex items-center justify-between px-6">
                    <!-- Left: Game Title -->
                    <div class="text-2xl font-extrabold tracking-widest uppercase text-secondary-content">
                        Tic Tac Toe
                    </div>
                </div>
            </nav>
            <!-- Main Content -->
            <main class="min-h-screen flex flex-col items-center justify-center flex-grow gap-6">
                <div id="login" data-on-load={ datastar.GetSSE("/api/index") }></div>
            </main>
        </div>
    }
}
Enter fullscreen mode Exit fullscreen mode

Now go ahead and check it out, open your dev-tools (chrome is Ctrl + Shift + i. When we navigate to our Index.page you will see a GET request to /api/index.

It's working! We also did this declaratively and without using any Javascript!

Of course the endpoint is still broken so let's work on that.

Let's fix the Index.go route and add our new endpoint.

Index.go

package routes

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/rphumulock/ds_nats_ttt/web/pages"
    datastar "github.com/starfederation/datastar/sdk/go"
)

func setupIndexRoute(router chi.Router) error {

    router.Get("/", func(w http.ResponseWriter, r *http.Request) {
        pages.Index().Render(r.Context(), w)
    })

    router.Route("/api/index", func(indexRouter chi.Router) {

        indexRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
            sse := datastar.NewSSE(w, r)
            sse.ExecuteScript("alert('Hello from the server!')")

        })

    })

    return nil
}
Enter fullscreen mode Exit fullscreen mode

In Chi, router.Route and router.Get serve different purposes when defining endpoints. router.Get is a simple way to define a single GET request for a specific path, whereas router.Route creates a subrouter that allows grouping multiple related endpoints under a common prefix. This makes router.Route useful for organizing API routes, especially when dealing with multiple endpoints that share a common base path.

In the provided code, router.Route("/api/index", func(indexRouter chi.Router) { sets up a subrouter for /api/index. This means that any additional routes defined within this block will be nested under /api/index/*, keeping the routing structure clean and maintainable. If only a single GET request were needed at /api/index, router.Get("/api/index", handlerFunc) could be used instead.

The line sse := datastar.NewSSE(w, r) initializes a Server-Sent Events (SSE) connection using the Datastar SDK. SSE allows for a persistent connection where the server can push updates to the client without requiring repeated requests. The datastar.NewSSE(w, r) function wraps the HTTP request with the necessary SSE headers, ensuring that the connection remains open for streaming data. This setup enables real-time updates to be sent to the client efficiently.

sse.ExecuteScript("alert('Hello from the server!')")
Enter fullscreen mode Exit fullscreen mode

We're again using the SDK to send a Datastar event.

This will do what it looks like it should: show an alert on the front-end when we hit the endpoint.

Since we're only performing one action the connection will close out after the handler function exits. We could've send 1, 2 and many more events if we wanted to all thanks to SSE and Datastar.

Datastar does the heavy lifting for us!


Ok, we have a nice little introduction. Let's keep building our application in Part 9!

Top comments (0)