DEV Community

Cover image for One of the coolest features of Go. Embed ReactJS into a binary with Go
pacholoamit
pacholoamit

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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

go get github.com/labstack/echo/v4
Enter fullscreen mode Exit fullscreen mode

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"))
}
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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

cd web
yarn install
Enter fullscreen mode Exit fullscreen mode

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"
  },

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

At this point our folder structure would look like this:

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

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)
}
Enter fullscreen mode Exit fullscreen mode

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"))
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now if you visit http://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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

cd .. && go run main.go
Enter fullscreen mode Exit fullscreen mode

If we visit http://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
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Modify the dev.sh script to contain

#!/bin/sh

cd web && yarn dev & air && fg
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
ramyasreekola profile image
Ramya Sree Kola

Thanks for this article. Very detailed and helpful to use it as boilerplate for my Go-React based application, One thing I found not working in main.go code is, continue using the explicit handler for "/api" route after adding the RegisterHandlers.

I understand its usecase in inital steps to help reader understand the mapping "/api" route to a handler that returns "Hello, World!"
I fixed it by removing that piece of code,

func main() {
e := echo.New()
web.RegisterHandlers(e)
e.Logger.Fatal(e.Start(":8080"))
}

Collapse
 
serbroda profile image
Danny Rottstegge

Hello,
first of all thank you for this helpful piece of code. I have a question: I am using 'react-router-dom' v6 in my React app. Do you know if it is possible, without using a HashRouter, for the Go app to reference to a route in the React app by refreshing the browser to a certain route?

For example, if the React app has the route '/groups/xyz' and a refresh is done there, that go app correctly returns to index.html and then the react-router-dom takes hold?

Thanks in advance!

Collapse
 
jingyifsyd profile image
Jingyifsyd

Hi, I just find you can create a middleware to change the url path mapping to the file. Here is the middleware code:

func SPA(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        url := r.URL.Path

        if !strings.Contains(url, ".") {
            r.URL.Path = "/"
        }

        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

Because http.FileServer alway treat .../ as .../index.html. It works, the full demo at Github

Collapse
 
supacoda1010 profile image
supacoda1010 • Edited

It did not work for me.
I ended up using the "rice" package as mentioned in this article: anhduongviet.medium.com/combine-go...