Runway
Runway is built and operated in the EU!

Embed JavaScript

Back in the days, Golang applications were mostly backend services, but of course this is no longer the case (or maybe it never was).

We also live in polyglot times — many developers know a couple languages. Using whatever is best for the job™ makes your life easier and also gives you an upside on your resume! So let’s mix and learn!

Golang has always had excellent support for HTTP — although maybe a bit bare in core but frameworks, such as gorilla, gofiber or go-chi offer a bit of abstraction on top of the core libraries and come with great middlewares; thus making building applications very easy.

Simple

One of the options to include JavaScript in your Golang code is to embed it into the binary. An example for this would include the following tree — main.go is of course our server application:

main.go
static/
  embed.go
js/
  package.json
  vite.config.js
  main.js

Details

The js folder contains a small boiler plate setup which includes a library we want to use, together with vite which is used to bundle it all up.

The package.json file would be this:

js/package.json
{
  "name": "runway-api-docs",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "vite build"
  },
  "devDependencies": {
    "rapidoc": "^9.3.8",
    "vite": "^5.3.4"
  }
}

The vite.config.js looks like this:

js/vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'main.js',
      name: 'RapiDoc',
      fileName: 'rapidoc',
      formats: ['iife']
    },
    outDir: '../static',
    rollupOptions: {
      output: {
        inlineDynamicImports: true
      }
    }
  }
})
js/main.js
import 'rapidoc'
// RapiDoc is now available as a web component
// The bundle will include all necessary dependencies

How does it work?

A CLI task (npm run build) generates a static asset into the static/ folder and then is embedded into your Golang application when it’s build on Runway:

static/embed.go
package static

import _ "embed"

//go:embed rapidoc.iife.js
var RapidocJS []byte

The handler code in main.go looks like this:

main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

	"my-go-js-project/static"

	"github.com/gorilla/mux"
	"github.com/hostwithquantum/runway-api-docs/docs"
)

func main() {
	r := mux.NewRouter()
	// serve the JS file we're building and go:embedding
	r.HandleFunc("/rapidoc.js", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/javascript")
		w.Write(static.RapidocJS)
	})
	// some very simple index.html that uses /rapidoc.js
	r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprint(w, `<!DOCTYPE html>
<body>
	<rapi-doc
		server-url='https://api.runway.horse'
		spec-url='/docs/swagger.json'>
	</rapi-doc>
	<script src="/rapidoc.js"></script>
</body>`)
	})
	// this is an API docs page, so we need a swagger.json
	r.HandleFunc("/docs/swagger.json", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write(docs.SwaggerJSON)
	})

	port := ":8484"
	if os.Getenv("PORT") != "" {
		port = ":" + os.Getenv("PORT")
	}
	log.Print("Running on " + port)
	log.Fatal(http.ListenAndServe(port, r))
}

To tie it all together:

go mod init my-go-js-project
go mod tidy

Then, you’ve got a choice: