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
.