Blog /

How to embed a React app in a Go binary

June 05, 2022 · by Nils Caspar · 5 min read

This blog posts describes how you can embed a React single-page application (SPA) alongside your API backend written in Go without any third-party libraries. The technique simplifies the deployment as its just a single Go binary that has to be shipped to production. And since API and frontend share the same domain, this method of bundling does away with the need for CORS preflight checks – without the need for an additional routing layer in front of your application code.

I will walk you through an example React app that will be bundled in a Go binary alongside a small API for various backend needs. While I'm focusing on React here, the process would be basically the same regardless of what frontend framework you use.

Step 0: Folder structure

We will share a single repository for UI and backend resources to simplify development as well as the bundling process later on. You can set up the package structure at the root level like you would for any "normal" Go service with the only difference being that there will also be a _ui/ folder where the React app will live.

Why the underscore at the start of the folder name? This denotes the folder as not being a real Go package and as such most Go tooling will ignore it and not unnecessarily scan it for .go files.

Step 1: Create a React App

Create a React app using the create-react-app command at the root level of the repository. We want this to be in the _ui folder but because NPM naming restrictions we'll create it using a different name first and then just move it over. While in the project folder, run:

npx create-react-app react-demo --template typescript
mv react-demo _ui

Note: As you can see, I prefer using the TypeScript template but you can also just use the regular JavaScript template instead.

Next we're just going to build the current state of the React app so we have something the Go service can serve in the next step:

cd _ui
npm run build

If everything worked, you should now have a build folder inside the _ui folder. Run cd .. after to switch back to the project root.

Hint: You should also delete the .git folder in _ui since you don't want to track the UI code separately from the "outer" Go repository, rm -rf _ui/.git.

Step 2: Initialize Go modules

As with any Go project, initialize the Go modules system by running go mod init in the project root with the appropriate parameters, for example:

go mod init example.com/react-go-embed

Step 3: Embed React app into Go binary

To instruct Go to embed the React app during build time, we create a file at the root level of the project. You can name it whatever you want (e.g. embed.go) with the following content:

package app

import "embed"

//go:embed _ui/build
var UI embed.FS

Notice that comment above var UI embed.FS? That's crucial and is what instructs Go to embed the contents of _ui/build. UI becomes a virtual file system that we then can access later to read the files created by npm build in the step before.

Step 4: Time to build a server

We will use a simple ServeMux from the http package in the Go standard library. First, we define all routes we want to handle with custom logic such as /api and /health. For all other requests we will try to serve matching static files. To achieve that, we start our cmd/server/main.go file like this:

package main

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/health", handleHealth)
	mux.HandleFunc("/api", handleApi)
	// ...
	mux.HandleFunc("/", handleStatic)
	// ...
	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Println("server failed:", err)
	}
}

The fs.FS instance that Go created has one minor inconvenience: All the files will be located under _ui/build. To remediate we can use the Sub function to define a new root.

var uiFS fs.FS

func init() {
	var err error
	uiFS, err = fs.Sub(app.UI, "_ui/build")
	if err != nil {
		log.Fatal("failed to get ui fs", err)
	}
}

And lastly we implement our serve functions:

func handleHealth(w http.ResponseWriter, r *http.Request) {
	// TODO: implement
}

func handleApi(w http.ResponseWriter, r *http.Request) {
	// TODO: implement
}

func handleStatic(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET" {
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		return
	}

	path := filepath.Clean(r.URL.Path)
	if path == "/" { // Add other paths that you route on the UI side here
		path = "index.html"
	}
	path = strings.TrimPrefix(path, "/")

	file, err := uiFS.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			log.Println("file", path, "not found:", err)
			http.NotFound(w, r)
			return
		}
		log.Println("file", path, "cannot be read:", err)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	contentType := mime.TypeByExtension(filepath.Ext(path))
	w.Header().Set("Content-Type", contentType)
	if strings.HasPrefix(path, "static/") {
		w.Header().Set("Cache-Control", "public, max-age=31536000")
	}
	stat, err := file.Stat()
	if err == nil && stat.Size() > 0 {
		w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
	}

	n, _ := io.Copy(w, file)
	log.Println("file", path, "copied", n, "bytes")
}

You can also explore the entirety of the source code in the example repository I prepared: https://github.com/pencil/react-go-embed

Step 5: Time to take it for a spin!

If you did everything correctly, you should now be able to start the Go server by running:

go run ./cmd/server/main.go

And finally, you should be greeted by the default React app template when you open http://localhost:8080/ in your browser. Congrats, you now have bundled a functional React app inside a Go binary. Next you can start implementing the API and frontend logic. Enjoy!

© 2024 Smartinary LLC