Updates

Background

When releasing a backend web service, adoption and usability can often be increased by also including a frontend. Traditionally, web-based frontends are often served through a separate, dedicated server (e.g. NGINX). However, this can make deployment more complex, since this requires an administrator to manage separate services in tandem.

With the release of Go’s 1.16’s embed package, we can now include these frontend assets directly in our Go binaries, making a full-stack server deployment as simple as running a single executable file.

Making a Sample Application: Hacker Laws

For this app, we’re going to make a server which will respond with a new, random hacker law when the user clicks a button. Each law will include both a name and a description.

Building the Backend

Let’s define a rudimentary HTTP API that responds with a random law at the endpoint /api/v1/law:


package main

import (
  "bytes"
  "encoding/json"
  "flag"
  "fmt"
  "io"
  "log"
  "math/rand"
  "net/http"
)

func main() {
  var port int
  flag.IntVar(&port, "port", 8080, "The port to listen on")
  flag.Parse()

  http.Handle("/api/v1/law", http.HandlerFunc(getRandomLaw))

  log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}

type Law struct {
  Name       string `json:"name,omitempty"`
  Definition string `json:"definition,omitempty"`
}

var HackerLaws = []Law{
  {
    Name:       "Amdahl's Law",
    Definition: "Amdahl's Law is a formula which shows the potential speedup of a computational task which can be achieved by increasing the resources of a system.",
  },
  {
    Name:       "Conway's Law",
    Definition: "This law suggests that the technical boundaries of a system will reflect the structure of the organisation.",
  },
  {
    Name:       "Gall's Law",
    Definition: "A complex system that works is invariably found to have evolved from a simple system that worked.",
  },
}

func getRandomLaw(w http.ResponseWriter, r *http.Request) {
  randomLaw := HackerLaws[rand.Intn(len(HackerLaws))]
  j, err := json.Marshal(randomLaw)
  if err != nil {
    http.Error(w, "couldn't retrieve random hacker law", http.StatusInternalServerError)
  }

  w.Header().Set("Content-Type", "application/json")
  io.Copy(w, bytes.NewReader(j))
}

When we run this server with go run main.go, we can access the API from curl to validate the output:

> curl http://localhost:8080/api/v1/law
{"name":"Gall's Law","definition":"A complex system t...}

Building the Frontend

Note, all these examples use Vue 2. If you’re using Vue 3, you may have to tweak a few things

To build the Vue client, we can use the Vue CLI to bootstrap our frontend. We can do this by running the following:

vue create frontend

This will create our Vue app project layout. Next, in our main.js file, we’ll initialize and use the axios plugin as our HTTP client:


import Vue from "vue";
import App from "./App.vue";
import axios from "axios";
import VueAxios from "vue-axios";

Vue.config.productionTip = false;

const client = axios.create({
  baseURL: "/api/v1",
});
Vue.use(VueAxios, client);

new Vue({
  render: (h) => h(App),
}).$mount("#app");

We’ll also update our App.vue to fetch a random hacker law from the backend, rendering it when a user presses the button:


<template>
  <div id="app">
    <button type="button" @click="getLaw()">Get a new hacker law</button>
    <div v-if="law != null">
      <h1>{{ law.name }}</h1>
      <p>{{ law.definition }}</p>
    </div>
  </div>
</template>

<script>
import Vue from "vue";

export default {
  name: "App",
  components: {},
  data() {
    return {
      law: null,
    };
  },
  methods: {
    getLaw() {
      Vue.axios.get("/law").then((response) => (this.law = response.data));
    },
  },
};
</script>

Making the Production Build

When we’re in the frontend directory, we can run our frontend production build with the following:

yarn build

This will create a new build directory in frontend/dist containing our production frontend assets. These are the assets that we’ll want to serve from the index of our Go server. To do this, let’s use Go’s embed package to indicate which folder we want to embed:


// ...

//go:embed frontend/dist
var frontend embed.FS

// ...

Then inside of function main, we’ll want our web server to serve these files at from the server root. We can do this using a few helper functions:

  • fs.Sub - Returns a new fs.FS which is a subtree of a given fs.FS. We can use this to strip the leading frontend/dist from our embedded files.
  • http.FS - Converts any fs.FS to a format suitable for http.FileServer
  • http.FileServer - Creates a new handler that serves the given filesystem

Putting this all together, we can serve our files from the root of the web server using the following code:


func main() {
    // ...

    stripped, err := fs.Sub(frontend, "frontend/dist")
    if err != null {
        log.Fatalln(err)
    }

    frontendFS := http.FileServer(http.FS(stripped))
    http.Handle("/", frontendFS)

    // ...
}

Now if we do our production backend build:

go build main.go
./main

We now have a simple web server that we can get a random hacker law from:

Our hacker law web app

The best part is that we can deploy this binary anywhere, and all the static frontend assets will be bundled with it!

Development Setup

Unfortunately, we’re not quite done yet. Although we’ve addressed how to bundle our static assets into our production build, we still need to come up with a strategy for developing our frontend and backend together. It would be annoying to have to do a yarn build and go build every time we wanted to make a minor code change. The Vue CLI service has a nice development server we can use with yarn serve. This allows hot-reloading the frontend assets each time our code changes, as well as enabling tighter integration with Vue debugging tools such as Vue Devtools. However, we face a problem: now we have two separate services (the Vue development server and the Golang backend server) that need two separate ports to bind to

We might be tempted to simply update the backend server port like so:

go run main.go -port 8081

And similarly update our axios client settings like so:


const client = axios.create({
  baseURL: "http://localhost:8081/api/v1",
});

However, if we do this, nothing will happen when we click the button on our frontend running at http://localhost:8080. This is because of the web browser’s Same-Origin Policy, which prevents us from making API calls to different origins from our frontend. In Firefox, it shows up like this:

An SOP issue

Fortunately, we have a couple options to solve this:

Option 1: Implement a CORS Middleware On Our Backend

With this option, we can tell the backend server which frontend URL our app will be accessed from, which will enable it to respond with the appropriate CORS headers. A great module for this is github.com/rs/cors. We can do this like so:


func main() {
    //...

    // First, we define a new rudimentary CORS middleware
    corsMiddleware := cors.New(cors.Options{
    AllowedOrigins: []string{"http://localhost:8080"},
  })

    // Then we wrap our original API handler to use the CORS middleware
    http.Handle("/api/v1/law", corsMiddleware.Handler(http.HandlerFunc(getRandomLaw)))

    //...
}

Note: This CORS policy is kept simple for this example, but wouldn’t be good for a production application. I highly recommend reading up on CORS at the Mozilla docs, as well as looking at the options supported by github.com/rs/cors

Now we can run our server like so:

go run main.go -port 8081

If we point our frontend’s axios client to http://localhost:8081/api/v1, we can complete our API requests successfully now that our backend responds with a valid Access-Control-Allow-Origin:

Successful CORS Validation

Lastly, we need a way to distinguish between development and production Vue builds, since our API client should use /api/v1/ as the base for production, but http://localhost:8081/api/v1 for development. Fortunately, Vue has the concept of environment modes. We can specify different values by creating a .env.production containing our production config, and a .env.development containing our development config.

Important Note: You should NEVER store secrets in these files, since they will be visible to anyone who uses your app. This is fine for non-secret information like our backend server URL, but not for secrets like API keys.


VUE_APP_API_BASE_URL=/api/v1

VUE_APP_API_BASE_URL=http://localhost:8081/api/v1

Then we can update our axios client to use it like so:


const client = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
});

Now our frontend will talk to the appropriate API endpoint in both development and production.

Option 2: Vue Dev Server Proxy

Another option is using the Vue development server to proxy traffic to our backend. Inside of vue.config.js, we can specify a backend address to proxy to, as well as rules about which traffic should be proxied. This enables us to create a configuration which sends everything starting with /api to our server running on http://localhost:8081. However, the browser will think it’s only dealing with http://localhost:8080, thereby satisfying the same-origin policy. We can do this with the following file in our frontend directory:


module.exports = {
  devServer: {
    proxy: {
      "^/api": {
        target: "http://localhost:8081",
        changeOrigin: true,
      },
    },
  },
};

Then we can set the axios client to use /api/v1 as our base URL:


const client = axios.create({
  baseURL: "/api/v1",
});

If we’re running in development mode, our Vue development server will transparently proxy all axios requests to http://localhost:8081. If we’re in production mode, our Golang server will receive the traffic and route to the correct endpoints.

Option 3: Use the Golang Server to Serve Development Files

Credit goes to @arran4 for this method

Rather than using the Vue dev server to deliver our development assets, we can use our Go server. The Vue service has an option to build our assets and automatically rebuild them each time they change. This is done via the build --watch command. First, we’ll update the package.json to include a new watch script:


"scripts": {
  "watch": "vue-cli-service build --watch"
},

This will build to the frontend/dist directory. Since we want our Go server to fetch the latest version from disk (and not embed them statically), we can use an os.DirFS, an alternative implementation of fs.FS. We can do this like so.


func main() {
  //...

  http.Handle("/", http.FileServer(http.FS(os.DirFS("frontend/dist"))))
}

However, we now have an issue: we want different behaviors between production and development builds. In development, we want to serve the frontend directly from disk; in production, we want to embed the assets statically and serve them from memory. While there are a couple of ways we can distinguish whether we’re in production or development mode (e.g. introduce a new CLI flag, or read some environment variable), I’m going to use build tags. Since this won’t need to be changed on-the-fly at runtime, it’s OK to make this a build-time option.

First, we’ll make two separate function implementations to get our frontend assets. For our production build:


// +build prod

package main

import (
  "embed"
  "io/fs"
)

//go:embed frontend/dist
var embedFrontend embed.FS

func getFrontendAssets() fs.FS {
  f, err := fs.Sub(embedFrontend, "frontend/dist")
  if err != nil {
    panic(err)
  }

  return f
}

And for development:


// +build !prod

package main

import (
  "io/fs"
  "os"
)

func getFrontendAssets() fs.FS {
  return os.DirFS("frontend/dist")
}

In our main function, now all we have to do is call getFrontendAssets, which will be filled in with the correct implementation at build time:


func main() {
  //...

  frontend := getFrontendAssets()
  http.Handle("/", http.FileServer(http.FS(frontend)))

  //...
}

Now during development, we can start the dev server like this:

cd frontend
yarn watch

# In a separate terminal window
go run .

Then to build for production

cd frontend
yarn build
cd ..
go build -tags prod

Which Option Should I Choose?

Which option you should choose will vary depending on your app’s deployment needs:

  • Option 1 is more complex, but gives the most generic and flexible solution. This enables it to solve a wider variety of deployment use cases, such as serving our production frontend and backend on different URLs.
  • Option 2 is the simplest, since it requires no special attention from our Go code. However, it is more specific to Vue’s development server, and may not work the same on other frontend frameworks.
  • Option 3 allows us to have a more “realistic” development environment, since our app is responsible for serving frontend assets in both development and production scenarios. However, it involves using build tags, which is a more advanced technique that not every developer may be familiar with.

References

Note: All example code is offered under the MIT license. This example code demonstrates both development setup options