Gary Bernhardt at PyCon 2014 talked about “The Birth and Death of Javascript”. He predicted, among other things, a giant exclusion zone around San Francisco, a world altering event in 2020, and that WASM would eventually take over the web. Let’s ignore the prior two prescient predictions and focus on the third, WASM taking over.

So, I want to build a simple web app by joining a bunch of new and fun technologies together:

  1. Bazel: A build tool that simplifies complex pipelines
  2. Golang: A statically typed, compiled programming language that can compile to multiple platforms including WASM
  3. WASM: WebAssembly is a binary format to be executed in browsers
  4. Proto: An IDL for data structures and RPC endpoints

TLDR: The code is here, _git clone_ then run _bazel run :server_ go to _localhost:7000_:

image Golang WASM application built with Bazel, using Proto IDL

I want to build a web-app like ec2instances.info where I can search the specs and costs of AWS EC2 instances. I want to use Golang for both the front and backend language (no Javascript, yay!) by compiling it into WASM and using the go-app package. Communication will be defined using the Proto IDL so I can generate all the boilerplate code. Everything will be stitched together Bazel. Let’s get started.#### Bootstrapping

Bazel and Golang are good friends, but to get them working together you need to setup [rules_go](https://github.com/bazelbuild/rules_go) for build logic, and [gazelle](https://github.com/bazelbuild/bazel-gazelle) for automation. For Protobuf Bazel needs [com_google_protbuf](https://github.com/protocolbuffers/protobuf) and [rules_proto_grpc](https://github.com/rules-proto-grpc/rules_proto_grpc) to compile proto files.

For importing Go modules rules_go does not use the go.mod file butits own go_repository rule. gazelle can convert go.mod to rules_go with bazel run //:gazelle — update-repos -from_file=go.mod. The packages this project needs are:

  1. [github.com/lyft/protoc-gen-star](http://github.com/lyft/protoc-gen-star): for generating proto code
  2. [github.com/maxence-charriere/go-app](http://github.com/maxence-charriere/go-app): for building WASM sites in Golang

The last external files needed are bootstrap.css to style the site, and the instances.json from ec2instances.info as a data-source.

All this code can be seen in the [WORKSPAC](https://github.com/grahamjenson/bazel-golang-wasm-proto/blob/master/WORKSPACE)E.

Caveat: there are some libraries that don’t play nice with WASM and require patching. In Bazel this is pretty easy and you can see the required patches in the _third_party_ dir.#### Proto & Generation

The apps API is described in api.proto: message Instance { string name = 1; ... string price = 6; }``message Instances { repeated Instance instances = 1; }``message SearchRequest { string query = 1; }``service Api { **rpc Search (SearchRequest) returns (Instances);** }

This describes the Search API which takes a query and returns a list of Instance models.

Here is where gazelle is useful, it will generate the needed Bazel files like protos/[BUILD.bazel](https://github.com/grahamjenson/bazel-golang-wasm-proto/blob/master/protos/BUILD.bazel) : proto_library( name = "api_proto", srcs = ["api.proto"], )``go_proto_library( name = "api_go_proto", compilers = [ "[@io_bazel_rules_go](http://twitter.com/io_bazel_rules_go)//proto:go_grpc", ], importpath = ".../bazel-golang-wasm-protoc/protos/api", proto = ":api_proto", )``go_library( name = "go_default_library", embed = [":api_go_proto"], importpath = ".../bazel-golang-wasm-protoc/protos/api", )

This tells Bazel how to take the api.proto file and convert it to a go_library for this application. At the moment the only compiler is gRPC, but I want HTTP. So let’s write some custom proto compilers and add them.

Proto Server and Client Code Generation

The Proto IDL is powerful because there are lots of tools to write custom compilers. Here I am using Lyft’s [protoc-gen-star](http://github.com/lyft/protoc-gen-star) because it provides lots of the features I need to parse proto files and generate code.

I want HTTP server and client code generated from the proto. First lets write the server [tools/protoc-gen-server/main.go](https://github.com/grahamjenson/bazel-golang-wasm-proto/blob/master/tools/protoc-gen-server/main.go). The generated code’s template looks like: const serviceTpl = package {{ .Package.ProtoName }}
import (…)
{{ range .Services }}
func Register{{ .Name }}HTTPMux(
mux *http.ServeMux,
srv {{ .Name }}Server,
) {
{{ range .Methods}}
mux.HandleFunc(
“{{ $method }}”,
func(w http.ResponseWriter, r *http.Request) {
in := new({{ .Input.Name }})
inJSON, _ := ioutil.ReadAll(r.Body)
defer r.Body.Close()
json.Unmarshal(inJSON, in)
ret, _ := srv.{{ .Name }}(context.Background(), in)
retJSON, _ := json.Marshal(ret)
w.Write(retJSON)
})
{{ end }}
}
{{ end }}
``

This template creates the RegisterApiHTTPMux method to register the API with an [http.ServeMux](https://golang.org/pkg/net/http/#ServeMux).

A ModuleBase is needed to implement a few methods to render the code: type protoModule struct {...}``func (m *protoModule) **Name**() string { return "server" } func (m *protoModule) **InitContext**(c pgs.BuildContext) {...} func (m *protoModule) **Execute**( targets map[string]pgs.File, pkgs map[string]pgs.Package, ) []pgs.Artifact {...}

The main function of this generator passes our module in and calls **Render**(): func main() { pgs.Init().RegisterModule( &protoModule{ModuleBase: &pgs.ModuleBase{}}, ).RegisterPostProcessor( pgsgo.GoFmt(), ).**Render**() }

The client code is mostly the same except for the template: const serviceTpl = package {{ .Package.ProtoName }}
import (…)
{{ range .Services }}
{{ range .Methods}}
func Call{{ .Service.Name }}{{ .Name }}(
input {{ .Input.Name }},
) (*{{ .Output.Name }}, error) {
str, _ := json.Marshal(input)
req, _ := http.NewRequest(
“POST”,
“{{ $method }}”, strings.NewReader(string(str)),
)
req.Header.Add(“Content-Type”, “application/json”)
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
instances := {{ .Output.Name }}{}
json.Unmarshal(body, &instances)
return &instances, nil
}
{{ end }}
{{ end }}
``

This creates the **CallApiSearch** method that can be used to call the HTTP server.

The go_proto_compiler rule tells Bazel that these are proto compilers: **go_proto_compiler**( name = "go_server", options = ["plugins=server"], plugin = "//tools/protoc-gen-server", suffix = ".pb.server.go", ... )

Then attach attach them to the compilers list to generate our new code: go_proto_library( name = "api_go_proto", compilers = [ "[@io_bazel_rules_go](http://twitter.com/io_bazel_rules_go)//proto:go_grpc", **"//tools:go_server", # keep "//tools:go_client", # keep** ], ... )

Although this is a lot of work to define two methods, it is great to know that generating code from a proto file is not that difficult. This removes lots of the boilerplate while making it way easier to update and change the API.#### Frontend into WASM

The main goal of this project is to write frontend code in Golang. For this the go-app package is really useful as it contains lots of the primitives for building and rendering HTML.

The projects frontend code is broken into three models, a search bar as input, a table to render the results, and a manager for control.

The search bar looks like: type SearchBar struct { app.Compo manager *Manager searchString string }``func (p *SearchBar) SetManager(manager *Manager) { p.manager = manager }``func (p *SearchBar) Render() app.UI { return app.Div().Body( app.Input().Value(p.searchString).OnKeyup(p.OnInputChange), ) }``func (p *SearchBar) OnInputChange(src app.Value, e app.Event) { p.searchString = src.Get("value").String() p.Update() p.manager.UpdateInstances(p.searchString) }

This component renders an <input> box that calls OnInputChange when the value changes, which updates the manager with the query string.

The table component looks like: type InstanceTable struct { app.Compo manager *Manager instances []*api.Instance }``func (p *InstanceTable) SetManager(manager *Manager) { p.manager = manager }``func (p *InstanceTable) Render() app.UI { nodes := []app.Node{} for _, i := range p.instances { nodes = append(nodes, app.Tr().Body( app.Td().Body(app.Text(i.Name)), ... app.Td().Body(app.Text(i.Price)), )) }``return app.Table().Class("table").Body( app.Tr().Body( app.Th().Scope("col").Body(app.Text("Name")), ... app.Th().Scope("col").Body(app.Text("Price")), ), app.TBody().Body(nodes...), ) }

This component has a list of instances which are rendered into a table.

The final component is the manager: type Manager struct { app.Compo searchBar *SearchBar instanceTable *InstanceTable }``func (h *Manager) Render() app.UI { return app.Div().Body( app.Header().Body(app.Nav().Class("navbar").Body( h.searchBar, )), app.Div().Class("container-fluid").Body( h.instanceTable, ), ) }``func (h *Manager) Search(q string) []*api.Instance { instances, err := api.**CallApiSearch**(api.SearchRequest{ Query: q, }) if err != nil { return []*api.Instance{} } return instances.Instances }``func (h *Manager) UpdateInstances(q string) { instances := h.Search(q) h.instanceTable.instances = instances h.instanceTable.Update() }

The manger joins all the components together. It takes the search bar’s query string then sends it to the generated method **api.CallApiSearch** to get a list of instances, which it then updates the table component with.

Running WASM build is like executing a Go binary inside your browser, so we need a main function: func **main**() { manager := &amp;Manager{ searchBar: &amp;SearchBar{}, instanceTable: &amp;InstanceTable{}, }`` manager.searchBar.SetManager(manager) manager.instanceTable.SetManager(manager)`` app.Route("/", manager) app.Run() }

This initializes the components sets and starts the app running.

To build a WASM project in Bazel you just need to set goarch and goos: go_binary( name = "app.wasm", embed = [":go_default_library"], **goarch = "wasm", goos = "js",** )#### Server

The server for this application implements the Search API backend: type Server struct { instances []*api.Instance }``func (server *Server) **Search**( ctx context.Context, in *api.SearchRequest, ) (*api.Instances, error) { if server.instances == nil { server.parseInstances() }`` instances := []*api.Instance{} for _, instance := range server.instances { str, _ := json.Marshal(*instance) if strings.Contains(string(str), in.Query) { instances = append(instances, instance) } } return &amp;api.Instances{Instances: instances}, nil }``func (server *Server) **parseInstances**() { fileName := "external/com_github_ec2instances/file/instances.json" ec2Instances := []ec2Instance{} server.instances = []*api.Instance{}`` file, _ := ioutil.ReadFile(fileName) json.Unmarshal(file, &amp;ec2Instances)`` for _, e := range ec2Instances { server.instances = append(server.instances, &amp;api.Instance{ Name: e.PrettyName, ... Price: e.Pricing["us-east-1"]["linux"].OnDemand, }) } }

The server parses the instances.json file to build a list of instances that the Search API looks for instances in.

The backend HTTP server looks like: `func main() {
mux := http.NewServeMux() app := &amp;app.Handler{ Title: "EC2Instances", Author: "Graham Jenson", Styles: []string{"bootstrap.css"}, } mux.HandleFunc(
“/app.wasm”,
func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, “wasm/js_wasm_pure_stripped/app.wasm”)
},
)`` mux.HandleFunc(
“/bootstrap.css”,
func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, “external/…/bootstrap.css”)
},
)

api.RegisterApiHTTPMux(mux, &server.Server{})
mux.Handle("/", app)`` log.Fatal(http.ListenAndServe(":7000", mux))
}`

This function creates a Server a go-app and an http.ServeMux. It returns the app.wasm and bootstrap files, and registers the server’s API and app to will handle the all other routes.

To make sure all the files are available to this function, they are listed in data of the go_binary: go_binary( name = "server", data = [ **"//wasm:app.wasm",** "[@com_github_bootstrap](http://twitter.com/com_github_bootstrap)//file:bootstrap.css", "[@com_github_ec2instan](http://twitter.com/com_github_ec2instan)ces//file:instances.json", ], embed = [":go_default_library"], ... )

Running with Bazel

Once all this is in place, to start the server you just need to run bazel run :server which will:

  1. download all the needed files (rules, go modules, bootstrap, instances.json)
  2. build and compile the proto files into Go code
  3. build the Golang binaries (both local and WASM)
  4. then package the backend binary will all the needed files and execute it to start the server on port 7000

The power of Bazel is that all of these actions are cached and on the next build it will only recompile exactly what is needed. It means that if you change a file like the proto, Bazel will only rebuild what is needed to update your application.#### Should you use WASM?

I like it, and go-app is really cool, but compared to React or other JS frameworks it is still pretty lite. I will say that, for me, using Bazel with Golang is a pleasure compared to the horrors I have faced in the JS world. However, JS libraries are more mature and have solved problems that the Go code has not even considered yet.

At the minimum, I recommend learning to use Bazel, Proto and WASM. If just for a to keep a healthy comparison between tools, but also because after the initial learning curve you will find them surprisingly simple and powerful.

How big is the WASM file?

For this application it is 15Mb compared to the 100kb of JS in ec2instances.info site. I could probably trim that down a bit by removing unused dependencies, like gRPC, but it looks like the minimum size is still around 8MB. This is probably the most limiting aspect of this workflow. go-app mitigates this by aggressively caching the WASM binary but this has its own issues.

There are many use-cases where the bundle size is not as important. Reliability and performance sometimes are higher priorities than load time. In these cases WASM might be a good alternative. Also, there are currently many web applications bundled to be run locally with tools like electron. These are good candidates for WASM, and might be my next project.

What are the next steps?

This was a pretty basic application. More time spent building these applications will show more limitations and also hopefully make them better. go-app is really cool but has some sharp edges.

Finding or building a full fledged frontend Golang framework like React would be the next step towards Bernhardt’s original vision.