One of the coolest features of Go. Embed ReactJS into a binary with Go

One of the coolest features of Go. Embed ReactJS into a binary with Go

Today we're going to attempt to embed a React Application into a Go binary. Please watch the youtube video down below for more mind blowing stuff. We're going to create a Golang REST API with Echo and a React application with Vite. From there, we're going to produce a single binary/executable containing both the API & the Web application.

Pre-requisites

  • Go version 1.18.3
  • Yarn version 1.22.18
  • Node version v16.15.1

Creating our Go project

First we're going to create our Go Project

mkdir go-react-demo
cd go-react-demo
touch main.go

Then, we'd like to install Echo which is a web framework (similar to Gin, Fiber, etc.)

go get github.com/labstack/echo/v4

Creating a basic API route endpoint with echo

In your main.go file, please write:

package main

import (
    "net/http"

    "github.com/labstack/echo/v4"
)

func main() {
    e := echo.New()
    e.GET("/api", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World!")
    })
    e.Logger.Fatal(e.Start(":8080"))
}

This will create a basic API endpoint that returns Hello, World! once a GET request is sent to http://localhost:8080/api we may also test this by running:

curl http:localhost:8080/api # <-- Should output "Hello, World!"

If everything works fine, next we'll create our React application with Vite

Creating our React App w/ Vite

Ensure you're in the root project directory then run:

yarn create vite
# Set the "project name" to "web"
# Set the "web framework" to "react" & "react-ts"

After Vite has finished bootstrapping our project, let's make sure all of the dependencies are installed

cd web
yarn install

Modifying the package.json file

We're going to modify the package.json file slightly, specifically the dev command. We don't want to serve the react application with the default vite server. We want to serve the static files ourselves with Go. We only want vite to rebuild the static files after a changes has been made (live-reload)

  "scripts": {
    "dev": "tsc && vite build --watch", <-- Change dev script to this
    "build": "tsc && vite build",
    "preview": "vite preview"
  },

Changing the dev command to tsc && vite build --watch tells vite to rebuild the static files after changes have been made to it.

Try to run yarn dev in the web directory to generate the static files located in the dist directory

# In go-react-demo/web
yarn run dev

At this point our folder structure would look like this:

go-react-demo/
├─ web/
│  ├─ dist/
│  ├─ public/
│  ├─ src/
|  ├─ ...
├─ main.go
├─ go.sum
├─ go.mod

Serving our Static files with Echo

We're going to create a web.go file in the web directory

// In go-react-demo/web/web.go

package web

import (

    "embed"
    "github.com/labstack/echo/v4"
)

var (
    //go:embed all:dist
    dist embed.FS
    //go:embed dist/index.html
    indexHTML     embed.FS
    distDirFS     = echo.MustSubFS(dist, "dist")
    distIndexHtml = echo.MustSubFS(indexHTML, "dist")
) 

func RegisterHandlers(e *echo.Echo) {
    e.FileFS("/", "index.html", distIndexHtml)
    e.StaticFS("/", distDirFS)
}

What we're doing here, is creating a route / and serving the static files built by Vite including the web/index.html and the static assets that accompanies it.

Importing our web RegisterHandlers function to our main.go file

Going back to our main.go file. Lets import the RegisterHandlers function we exposed in the web package

package main

import (

    "net/http"

    "go-react-demo/web" # <--- INCLUDE THIS

    "github.com/labstack/echo/v4"

)

func main() {
    e := echo.New() 
    web.RegisterHandlers(e) # <-- INCLUDE THIS
    e.GET("/api", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello world!")
    })
    e.Logger.Fatal(e.Start(":8080"))
}

Now let's test the go server to see if it's serving the static assets of our react application correctly. Go to the root directory of the project & run:

go run main.go

Now if you visit localhost:8080 in the browser you should see the default vite React application.

Making a request to the Go API server from within React

Now let's try to make a GET request to the Go API server from within our React app that is also served by the Go server... Sounds like some inception stuff happening here. Please add the following:

// In go-react-demo/web/src/App.tsx
import { useState, useEffect } from "react";
import "./App.css"; 

function App() {
  const [data, setData] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch("http://localhost:8080/api");
      const data = await response.text();
      setData(data);
    };

    fetchData().catch((err) => console.log(err));
  }, []);



  return (
    <div className="App">
      <h1>{data}</h1>
    </div>
  );

}

export default App;

Now we need to regenerate the React static files since we've made changes.

# assuming you're currently at the rootDirectory (go-react-demo)
cd web && yarn run dev # Generates the new static assets

Then we need to run the go server to serve the files

cd .. && go run main.go

If we visit localhost:8080, you should be greeted with "Hello world" which comes from the Go API server

A really bad development experience

I'm sure you noticed that always running 2 terminals both with different processes is a really bad dev experience, fear not for I have a solution!

We're going to install air. air which is sorta like nodemon but for go. air allows us to have hot reload with go so we don't have to manually run the go run main.go command every time we make changes.

To install air

go install github.com/cosmtrek/air@latest

Then, you'd want to create a configuration file for air that's simply done via running:

#You should be in the root directory of the go-react-demo project
air init # Should output a `.air.toml`

Now, the final step in making a better development experience. If you're using wsl Create a dev.sh file in the root directory of your project

touch dev.sh # creates the file

Modify the dev.sh script to contain

#!/bin/sh

cd web && yarn dev & air && fg

This will run both the go api server & the vite build server in parallel in one terminal

Compiling the binaries

Now, the moment of truth: to compile the binaries containing the React application simply run

go build main.go

If you're trying to build windows binaries from WSL:

env GOOS=windows GOARCH=amd64 go build main.go
# You may have a different $GOARCH so please do some research

Congratulatiions! you've created a single go binary that contains both your API & your React App!