Go has a great feature that it can be packaged into a single binary which is easy for distribution. However when it comes to web application it’s a bit tricky becuase 1. you need to bind frontend assets into the binary and 2. you need to deal with the routing if you are building a single page application(SPA).

In this post I will walk you through how to embed a vite built react app into a go binary and how to handle the routing using gin framework.

Project structure

The project structure is as follows:

govite
├── go.mod
├── go.sum
├── main.go
├── ui
│   ├── embed.go
├── backend
    └── api.go

The ui folder contains the frontend code which you can initiated by vite. The backend folder contains the backend code which include an api server. The main.go is the entry point of the application.

Build the frontend

Depends on your frontend framework, you can build the frontend in different ways. In this post I will use vite to build the frontend. the default output folder of vite is dist under ui. You can change it by modifying the build.outDir in the vite.config.js file.

In this post I will use the default dist folder.

Embed frontend assets

The first step is to embed the frontend assets into the binary. This can be done by using the embed package introduced in go 1.16. The embed package allows you to embed files into the binary and access them through the embed.FS type.

So what you need to do is to create a embed.go file under the ui folder and add the following code:

package ui

import "embed"

//go:embed dist
var Dist embed.FS

The embed package will automatically embed the dist folder into the binary. The dist folder is the output folder of vite. for more information about the embed package, please refer to the official documentation.

Routing with gin

We need to set up the routes to api handlers in go and the static files in ui/dist folder. we can do this with gin like


func main() {
    engine := gin.Default()
    apiHandler(engine)
    staticHandler(engine)
    engine.Run()
}

The staticHandler would find the embeded files in the binary and serve them while it would need to exclude the api routes. The following code shows how to do this:


func staticHandler(engine *gin.Engine) {
	dist, _ := fs.Sub(ui.Dist, "dist")
	fileServer := http.FileServer(http.FS(dist))

	engine.Use(func(c *gin.Context) {
		if !strings.HasPrefix(c.Request.URL.Path, "/api") {
			// Check if the requested file exists
			_, err := fs.Stat(dist, strings.TrimPrefix(c.Request.URL.Path, "/"))
			if os.IsNotExist(err) {
				// If the file does not exist, serve index.html
				fmt.Println("File not found, serving index.html")
				c.Request.URL.Path = "/"
			} else {
				// Serve other static files
				fmt.Println("Serving other static files")
			}

			fileServer.ServeHTTP(c.Writer, c.Request)
			c.Abort()
		}
	})
}

This would ensure that

  • All the requests to paths other than /api would be served with the static files
  • If the requested file does not exist, it would serve the index.html file which is the entry point of the frontend application. this is important for SPA because the frontend router would handle the routing.

dev mode

In dev mode you might want to run frontend and backend separately. What you need to keep in mind is since the frontend and backend api run on different ports, you need to set up the cors policy to allow cross origin requests.

Package & Run

Now we can package the application into a single binary by running

go build -o go-vite

and run it by

./go-vite

then you can open the browser and go to http://localhost:8080 to see the ui and check the api under http://localhost:8080/api.