Adventure: Building with NATS Jetstream KV Store -Part 6
Welcome back! Welcome to Part 5 of our series on the NATS JetStream KV Store! Now, we're diving into real development - finally bringing the promised Tic-Tac-Toe game to life. Where did we leave off? We had just made a tic-tac-toe game with NATS Jetstream KV Store and the bash shell. This game will allow players to create unlimited game lobbies, waiting for opponents to join and play. Since this is an ongoing project, we may continue adding features even after this post. But for now, we'll start with a functional, foundational version of the game. Let's go! Project Stack First things first - we need to scaffold our project. For this build, I'll be using Go. It's a fun, concise language that gets straight to the point, and even better - NATS is built in Go! One of the coolest things about this setup is that we can embed a NATS server directly into our application. This means there's no need to set up an external server or containerize anything - unless, of course, you choose to. Since we're using Go, I've chosen Templ as our template language - it's dynamic, easy to use, and a pleasure to work with. For styling, I've opted for Tailwind CSS, which is both lightweight and simple to set up. To keep things streamlined and avoid managing a separate JavaScript project or worrying about state synchronization, we're using a powerful and fun library called Datastar. I have so much to say about Datastar that I'll be launching another series on it, but for now, here's the short version: it's small, powerful, declarative, and incredibly easy to use. Best of all, it lets us skip the JavaScript hassle entirely! (Kidding… but seriously, haha.) As we code, we'll cover some high-level aspects of how Datastar works, giving you an understanding of its functionality. However, for a deeper dive into this powerful library, the Datastar series and the Datastar site will provide a more in-depth, hands-on exploration. Stay tuned - there's a lot to love about this tool! So to list what we're using again: Golang NATS Datastar SQLite Every stack needs an acronym. We have MERN, MEAN and LAMP etc. So if you thought about it - the answer is yes, there is an acronym for this stack. I believe it's mostly a joke but the stack is no joke! You should take it very seriously! Jokes aside, let's keep going. Getting Started To get started, we need to set up a basic Go project. Here's how: # Create the project $ mkdir $HOME/Projects/ds_nats_ttt # cd into our project $ cd $HOME/Projects/ds_nats_ttt # Initialize a Go module $ go mod init github.com//ds_nats_ttt # Initialize a Git repository $ git init This sets up our project structure, initializes Go module management, and creates a Git repository to track our progress. Let's keep scaffolding our project. There are many ways to structure a Go project, and I'll be using an approach that fits both my workflow and the needs of this project. A lot of this project is structured from this repository called Northstar- in fact you'll notice it's close to identical. Well that's because I like it and I'm still learning the best way to go about things myself! Always time to learn something! # Create our folder structure $ mkdir -p cmd/web internal/routes ui/layouts ui/pages ui/static ui/styles # Create our files $ touch cmd/web/main.go $ touch internal/routes/index.go internal/routes/router.go $ touch ui/layouts/base.templ ui/pages/index.templ ui/styles/styles.css So you'll be left with something like this… ds_nats_ttt/ # Project root │── cmd/ # Application entry points │ ├── web/ # Web application │ │ ├── main.go # Web server entry point │ │── internal/ # Internal application logic (not accessible externally) │ ├── routes/ # Route handling logic │ │ ├── index.go # Index/home route logic │ │ ├── router.go # Router setup │ │── ui/ # User interface assets │ ├── layouts/ # Layout templates (shared layouts) │ │ ├── base.templ # Base layout template (e.g., HTML boilerplate) │ ├── pages/ # Page-specific templates │ │ ├── index.templ # Index page template │ ├── static/ # Static files (CSS, JS, images) | │ ├── styles/ # Tailwind CSS Baseline │ │ ├── styles.css # Index page template Let's do something that's temporary since we haven't pushed this code anywhere. Go into your go.mod file and add these lines. This is so we can reference our local files and Go doesn't try to look them up. Modify this to fit your directory structure. replace github.com//ds_nats_ttt/internal/routes => /home/.../Projects/ds_nats_ttt/internal/routes replace github.com//ds_nats_ttt/ui/layouts => /home/.../Projects/ds_nats_ttt/ui/layouts replace github.com//ds_nats_ttt/ui/page
Welcome back!
Welcome to Part 5 of our series on the NATS JetStream KV Store! Now, we're diving into real development - finally bringing the promised Tic-Tac-Toe game to life.
Where did we leave off?
We had just made a tic-tac-toe game with NATS Jetstream KV Store and the bash shell.
This game will allow players to create unlimited game lobbies, waiting for opponents to join and play. Since this is an ongoing project, we may continue adding features even after this post. But for now, we'll start with a functional, foundational version of the game. Let's go!
Project Stack
First things first - we need to scaffold our project. For this build, I'll be using Go. It's a fun, concise language that gets straight to the point, and even better - NATS is built in Go!
One of the coolest things about this setup is that we can embed a NATS server directly into our application. This means there's no need to set up an external server or containerize anything - unless, of course, you choose to.
Since we're using Go, I've chosen Templ as our template language - it's dynamic, easy to use, and a pleasure to work with. For styling, I've opted for Tailwind CSS, which is both lightweight and simple to set up.
To keep things streamlined and avoid managing a separate JavaScript project or worrying about state synchronization, we're using a powerful and fun library called Datastar. I have so much to say about Datastar that I'll be launching another series on it, but for now, here's the short version: it's small, powerful, declarative, and incredibly easy to use. Best of all, it lets us skip the JavaScript hassle entirely! (Kidding… but seriously, haha.)
As we code, we'll cover some high-level aspects of how Datastar works, giving you an understanding of its functionality. However, for a deeper dive into this powerful library, the Datastar series and the Datastar site will provide a more in-depth, hands-on exploration. Stay tuned - there's a lot to love about this tool!
So to list what we're using again:
- Golang
- NATS
- Datastar
- SQLite
Every stack needs an acronym. We have MERN, MEAN and LAMP etc. So if you thought about it - the answer is yes, there is an acronym for this stack. I believe it's mostly a joke but the stack is no joke! You should take it very seriously!
Jokes aside, let's keep going.
Getting Started
To get started, we need to set up a basic Go project. Here's how:
# Create the project
$ mkdir $HOME/Projects/ds_nats_ttt
# cd into our project
$ cd $HOME/Projects/ds_nats_ttt
# Initialize a Go module
$ go mod init github.com//ds_nats_ttt
# Initialize a Git repository
$ git init
This sets up our project structure, initializes Go module management, and creates a Git repository to track our progress.
Let's keep scaffolding our project. There are many ways to structure a Go project, and I'll be using an approach that fits both my workflow and the needs of this project.
A lot of this project is structured from this repository called Northstar- in fact you'll notice it's close to identical. Well that's because I like it and I'm still learning the best way to go about things myself! Always time to learn something!
# Create our folder structure
$ mkdir -p cmd/web internal/routes ui/layouts ui/pages ui/static ui/styles
# Create our files
$ touch cmd/web/main.go
$ touch internal/routes/index.go internal/routes/router.go
$ touch ui/layouts/base.templ ui/pages/index.templ ui/styles/styles.css
So you'll be left with something like this…
ds_nats_ttt/ # Project root
│── cmd/ # Application entry points
│ ├── web/ # Web application
│ │ ├── main.go # Web server entry point
│
│── internal/ # Internal application logic (not accessible externally)
│ ├── routes/ # Route handling logic
│ │ ├── index.go # Index/home route logic
│ │ ├── router.go # Router setup
│
│── ui/ # User interface assets
│ ├── layouts/ # Layout templates (shared layouts)
│ │ ├── base.templ # Base layout template (e.g., HTML boilerplate)
│ ├── pages/ # Page-specific templates
│ │ ├── index.templ # Index page template
│ ├── static/ # Static files (CSS, JS, images)
|
│ ├── styles/ # Tailwind CSS Baseline
│ │ ├── styles.css # Index page template
Let's do something that's temporary since we haven't pushed this code anywhere. Go into your go.mod file and add these lines. This is so we can reference our local files and Go doesn't try to look them up.
Modify this to fit your directory structure.
replace github.com//ds_nats_ttt/internal/routes => /home/.../Projects/ds_nats_ttt/internal/routes
replace github.com//ds_nats_ttt/ui/layouts => /home/.../Projects/ds_nats_ttt/ui/layouts
replace github.com//ds_nats_ttt/ui/pages => /home/.../Projects/ds_nats_ttt/ui/pages
This makes it so when we build our project Go will look for these imports in our local project files.
Our Server
Ok we need to make a server - the brain of our application. Here is the code to create our server. Go ahead and copy this to cmd/web/Main.go
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/rphumulock/ds_nats_ttt/internal/routes"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"golang.org/x/sync/errgroup"
)
func main() {
// Logger Setup – Creates a structured JSON logger (slog) for logging events.
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// Port Configuration – Checks for a PORT environment variable;
// defaults to 8080 if not set.
getPort := func() string {
if p, ok := os.LookupEnv("PORT"); ok {
return p
}
return "8080"
}
// Signal Handling – Uses signal.NotifyContext to listen for termination
// signals (SIGINT, SIGTERM), ensuring the server shuts down cleanly when
// interrupted.
logger.Info(fmt.Sprintf("Starting Server 0.0.0.0:%s", getPort()))
defer logger.Info("Stopping Server")
// Start Server – Calls run() to launch the server, passing the
// execution context, logger, and port.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Error Handling – If run() fails, it logs the error and exits the
// program with a non-zero status.
if err := run(ctx, logger, getPort()); err != nil {
logger.Error("Error running server", slog.Any("err", err))
os.Exit(1)
}
}
// This function ensures the server runs in a controlled environment,
// handles errors properly, and stops all goroutines when needed.
func run(ctx context.Context, logger *slog.Logger, port string) error {
// Create an Error Group – Uses errgroup.WithContext to manage
// multiple goroutines, ensuring errors propagate properly.
g, ctx := errgroup.WithContext(ctx)
// Start the Server – Calls startServer() within a new goroutine so it
// runs concurrently.
g.Go(startServer(ctx, logger, port))
// Wait for Completion – g.Wait() blocks until all goroutines finish
// or one returns an error.
if err := g.Wait(); err != nil {
return fmt.Errorf("error running server: %w", err)
}
return nil
}
// This function ensures the server runs properly with logging,
// structured routing, and a clean shutdown process.
func startServer(ctx context.Context, logger *slog.Logger, port string) func() error {
return func() error {
router := chi.NewMux()
// Router Setup - Creates a new Chi router and applies middleware
// for logging and panic recovery.
router.Use(
middleware.Logger,
middleware.Recoverer,
)
// Static File Handling - Configures the server to serve static files
// from the ./static directory.
router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("./ui/static"))))
// Route Initialization - Calls SetupRoutes() to register application
// routes. If route setup fails, it returns an error. A cleanup function
// is deferred to release resources when the server shuts down.
cleanup, err := routes.SetupRoutes(ctx, logger, router)
defer cleanup()
if err != nil {
return fmt.Errorf("error setting up routes: %w", err)
}
// Server Configuration - Creates an HTTP server that listens on
// 0.0.0.0: and uses the configured router.
srv := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: router,
}
// Graceful Shutdown - Runs a goroutine that waits for a shutdown
// signal (ctx.Done()) and shuts down the server when triggered.
go func() {
<-ctx.Done()
srv.Shutdown(context.Background())
}()
return srv.ListenAndServe()
}
}
These three functions basically just work together to start, run, and manage an HTTP server while ensuring proper error handling and graceful shutdown. The comments will explain what everything does.
How do we get around?
We need to define our routes. If you've noticed our server calling routes.SetupRoutes()
, you're absolutely right-that's where we'll configure all our application routes. This function also returns a deferred cleanup()
function.
Router.go
package routes
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-chi/chi/v5"
)
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),
); err != nil {
return cleanup, fmt.Errorf("error setting up routes: %w", err)
}
return cleanup, nil
}
Ok but you might also notice this calls another function setupIndexRoute()
. Yep! Again you're so smart. We will list all of our routes here but for now where only have one.
Index.go
package routes
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/rphumulock/ds_nats_ttt/ui/pages"
)
func setupIndexRoute(router chi.Router) error {
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
pages.Index().Render(r.Context(), w)
})
return nil
}
This is pretty obvious. We're going to render an Index page at our root route. Let's going our templates in place for our Index page.
I wanna see stuff!
Ok ok ok, soon enough! Let's get our templates in order.
General Layout
Copy this snippet to ui/layouts/base.templ
.
package layouts
templ Base() {
Tic+Tac+Toe
{ children... }
}
This snippet serves as a wrapper for all other templates. It allows us to include global styles and imports in one place, ensuring consistency across the application.
This approach prevents repetitive code in individual templates and ensures that shared elements are available even in other partials where direct inclusion may not be possible.
Landing Page
This is the page we will start on when anyone comes to our application. Copy this to ui/pages/Index.templ
.
package pages
import "github.com/rphumulock/ds_nats_ttt/ui/layouts"
templ Index() {
@layouts.Base() {
}
}
This is simply HTML. You can see we wrap it with our Base.templ
file.
You said we would see stuff!
I did and we will. Right now. We need to generate our templates with Templ. You can read more about this here but we're just going to get right to it.
Get Templ with Go and then generate our templates.
# Make sure we're in our project root
$ cd $HOME/Projects/ds_nats_ttt
# Get Templ
$ go get github.com/a-h/templ
# Generate
$ templ generate .
You'll see some files now generated like Index.templ.go
. That's a good thing.
Finally let's link things together.
# Get all of our dependencies
$ go mod tidy
Go will fetch all the stuff it needs to put things together. Let's run this now!
go run ./cmd/web
Voila! Go to http://localhost:8080 to see a really boring page! Don't worry it will get better because we have so much more to do!
See you in Part 6!