Doing Frontend Development with Golang, JavaScript and Docker (Part The First)
This series:
- Part the First: Better JavaScript in Go with Vite (You’re here)
- Part the Second: Dockerize Your Go and Vite Setup
- Part the Third: Creating Certificates and Setting up a Router
Go (aka: Golang) is Google’s general purpose programming language. It’s very popular, easy to code, and used a lot in backend web development. Go is especially good for coding web servers, since Go programs are extremely performant, so much so that you have much less need for web server software like Nginx or Apache.
Still, it’s not perfect. Nowadays, most web applications combine HTML, JavaScript and a variety of CSS libraries. Many of these libraries work best if there’s some kind of preprocessing step that converts files that are easy and convenient for developers to write and maintain, into files that are easy and convenient for your browser to process and display. Go, on the other hand, wants you to put your display data into its templating system. You can put easily put raw JavaScript or raw CSS into your templates, but that is the limit of it. Modern front end development tools like Typescript or SASS don’t really fit into this paradigm.
How do you use modern CSS and JavaScript tooling with Go?
This series of articles will show you how to use Vite 3, a JavaScript and web asset processing and bundling tool to teach the Go gopher a few new tricks. The Vite web site states:
“…as we build more and more ambitious applications, the amount of JavaScript we are dealing with is also increasing dramatically. It is not uncommon for large scale projects to contain thousands of modules. We are starting to hit a performance bottleneck for JavaScript based tooling: it can often take an unreasonably long wait (sometimes up to minutes!) to spin up a dev server, and even with Hot Module Replacement (HMR), file edits can take a couple of seconds to be reflected in the browser. The slow feedback loop can greatly affect developers’ productivity and happiness.
“Vite aims to address these issues by leveraging new advancements in the ecosystem: the availability of native ES modules in the browser, and the rise of JavaScript tools written in compile-to-native languages.”
We’re going to walk though the process of setting up a Vite-based workflow for Go using Docker and SSL. This will take a few articles to describe the whole process. This article will start us out by getting a Go web application talking to Vite. We’ll pull JavaScript and CSS out of the templates, and move them into a linked project that Vite manages for us. Let’s do a case study.
Introducing… The Join Our Quest App
Arthur Pendragon, a 5th century Briton and aspiring monarch, is recruiting knights for an exciting religious and spiritual opportunity: find an ancient drinking cup. His options for publicity are limited (it being the 5th century and all), so he decides to create a web app.

So far, it’s all back end work, with just a bit of Bootstrap 5 to make the form look neat. You can find the code up on GitHub, here. This being a demo, we won’t do too much validation or other processing in Go. Let’s assume that we will add that to Arthur’s app, but that’s beyond the scope of this article. What we will do is add JavaScript client side validation to the app.
Typically, you’d do that by adding the JavaScript into the template for this page. Right now, our setup looks like this:
├── README.md
├── go.mod
├── go.sum
├── main.go
├── templates
│ ├── base.layout.gotmpl
│ ├── quest.page.gotmpl
│ └── result.page.gotmpl
It’s a pretty simple app, as I’ve said. To get started converting over to Vite, let’s create a Vite project at the top level of the work directory; we’ll call it “frontend”. You can add pre-populated projects for Vue, React, Svelte, and more, with options to use or not use Typescript. I’m going to choose to create a plain project using Typescript:
$ npm create vite@latest
✔ Project name: … frontend
✔ Select a framework: › vanilla
? Select a variant: › - Use arrow-keys. Return to submit.
vanilla
❯ vanilla-ts
I choose “vanilla-ts”, and I now have a “frontend” directory next to my main.go file. Let’s see what Vite created for us:
$ cd frontend/
$ npm install
added 16 packages, and audited 17 packages in 3s
4 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
$ npm run dev
and Vite starts serving HTTP to us on http://localhost:5173/:

The page is simple, and looks like this:

The actual html is even more simple. It looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<script type="module" src="/@vite/client"></script> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
We won’t need this page or its formatting, but behind the scenes, Vite has added two script tags to the index page that loads the Vite hot loader (“HMR”, for “hot module replacement”). The HMR system in turn loads processed javascript into the page, along with CSS. This we do want. In order to get this loading w/o the HTML itself, we need to get our Go app to do the same thing for us.
This is what a Vite integration does: it adds tags to your web pages to bootstrap the loading process. I wrote vite-go, which does this for Golang apps. We first need to install the package, and then call the library in our Go code.
To install the module:
$ go get github.com/torenware/vite-go
Then we add code like this to our sample app. The code looks like this. In imports():
import (
// lots of stuff omitted...
vueglue "github.com/torenware/vite-go"
)
and add code like this to func main():
// We put our project in the frontend directory:
projDir := os.DirFS("frontend")
config := vueglue.ViteConfig{
Environment: "development",
FS: projDir,
}// Initialize our Vite library
glue, err := vueglue.NewVueGlue(&config)
if err != nil {
log.Panicf("could not initialize vite library: %v", err)
}
The actual configuration is very brief, since vite-go can guess what your project likely looks like. If you use a stock project as I do here, it will very likely get it right. If you’re using custom settings, you may need to add more configuration; see the vite-go docs for details.
We pass the “glue” object to Go’s templating system, and then we modify the templates to render our Vite tags into the page:
In our render routine:
if data == nil {
data = map[string]any{
"vite": viteLib,
}
} else {
data["vite"] = viteLib
}ts, err := template.ParseFiles(templateList...)
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}err = ts.Execute(w, data)
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
}
}
{{ $vite := .vite }}
{{ if $vite }}
{{ $vite.RenderTags }}
{{ end }}
Once we rebuild the Go app and restart it, this should be enough to get the correct tags rendered into our page. Make sure that Vite is still running on 5173; if you look at the network tab of your browser tools, you should see something like this:

Note that several items have loaded from the Vite server at 5173, including the rather odd blue item with code “101”. This last item is the HMR websocket that will be used to load newly changed and processed items. From this, you know that vite-go’s integration code is working.
Adding JavaScript and CSS
Now that Vite is hooked up, we can start directly on our front end development. The first step is to delete things you won’t need in the stock code in “frontend”. In particular, you should delete all of the CSS in “style.css”, and you can delete essentially everything in “src/main.ts”. This is where you will do most of your work. Once you’ve done this, your IDE should show you which items in the directory you no longer need.
I also add a (currently optional) Vite config file, vite.config.ts. This has one setting that isn’t even used for the dev server; the build.manifest setting is used for production builds. We’ll add more to this file as we continue the series.
I added client side validation code in main.ts, and corresponding CSS to style.css. Note that the code is in Typescript, which would not work in a Go template. Vite allows this by noticing when I change the code, compiling the Typescript into JavaScript, and delivering it up to the browser via the websocket. Pretty much automatically.
This is plenty to look over for a first pass. Since I like showing how things hook together, here’s what our network looks like right now when we run the current version of the app:

This looks simple, and is. Everything is running on your local computer’s network, with Go sending HTTP on port 80, and the Vite dev server operates both its asset serving and the HMR websocket (protocol “ws”) on port 5173 (more on that in a later installment). And now that we have this working locally, we will create docker containers for the app and for Vite in the next installment, and move our work load into Docker.
Rob Thorne is a full-stack developer who’s doing more and more devops these days. He’s available for hire. You can find Rob on Twitter as @torenware.