questionable services

Technical writings about computing infrastructure, HTTP & security.

(by Matt Silverlock)

Serving a Vue, React or Ember JavaScript Application with Go.


It’s 2016. You’re about to tie together a Popular Front-End JavaScript framework with a web service written in Go, but you’re also looking for a way to have Go serve the static files as well as your REST API. You want to:

Here’s how.

The Folder Layout

Here’s a fairly simple folder layout: we have a simple Vue.js application sitting alongside a Go service. Our Go’s main() is contained in serve.go, with the datastore interface and handlers inside datastore/ and handlers/, respectively.

~ gorilla-vue tree -L 1
├── datastore
├── dist
├── handlers
├── index.html
├── node_modules
├── package.json
├── serve.go
├── src
└── webpack.config.js

~ gorilla-vue tree -L 1 dist
├── build.js

With this in mind, let’s see how we can serve index.html and the contents of our dist/ directory.

Note: If you’re looking for tips on how to structure a Go service, read through @benbjohnson’s excellent Gophercon 2016 talk.

Serving Your JavaScript Entrypoint.

The example below uses gorilla/mux, but you can achieve this with vanilla net/http or httprouter, too.

The main takeaway is the combination of a catchall route and http.ServeFile, which effectively serves our index.html for any unknown routes (instead of 404’ing). This allows something like to still run your JS application, letting it handle the route explicitly.

package main

import (



func main() {
	var entry string
	var static string
	var port string

	flag.StringVar(&entry, "entry", "./index.html", "the entrypoint to serve.")
	flag.StringVar(&static, "static", ".", "the directory to serve static files from.")
	flag.StringVar(&port, "port", "8000", "the `port` to listen on.")

	r := mux.NewRouter()

    // Note: In a larger application, we'd likely extract our route-building logic into our handlers
    // package, given the coupling between them.

	// It's important that this is before your catch-all route ("/")
	api := r.PathPrefix("/api/v1/").Subrouter()
	api.HandleFunc("/users", GetUsersHandler).Methods("GET")
	// Optional: Use a custom 404 handler for our API paths.
	// api.NotFoundHandler = JSONNotFound

	// Serve static assets directly.

	// Catch-all: Serve our JavaScript application's entry-point (index.html).

	srv := &http.Server{
		Handler: handlers.LoggingHandler(os.Stdout, r),
		Addr:    "" + port,
		// Good practice: enforce timeouts for servers you create!
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,


func IndexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Request) {
	fn := func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, entrypoint)

	return http.HandlerFunc(fn)

func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
	data := map[string]interface{}{
		"id": "12345",
		"ts": time.Now().Format(time.RFC3339),

	b, err := json.Marshal(data)
	if err != nil {
		http.Error(w, err.Error(), 400)


Build that, and then run it, specifying where to find the files:

go build serve.go
./serve -entry=~/gorilla-vue/index.html -static=~/gorilla-vue/dist/

You can see an example app live here


That’s it! It’s pretty simple to get this up and running, and there’s already a few ‘next steps’ we could take: some useful caching middleware for setting Cache-Control headers when serving our static content or index.html or using Go’s html/template package to render the initial index.html (adding a CSRF meta tag, injecting hashed asset URLs).

If something is non-obvious and/or you get stuck, reach out via Twitter.

© 2023 Matt Silverlock | Mastodon | Code snippets are MIT licensed | Built with Jekyll