Embed your single page app in Golang
Image by Tim Zänkert
Continuum of this article.
Preface
As mentioned in the previous version of this article, I love the idea of having a nice modern ui embedded in a single binary. It’s clean, elegant and saves a lot of time when deploying the applcation.
This time i built a simple gallery viewe named Fuu
.
Application Structure
FS <–> Golang Fileserver <– Browsing API <–> React App
- Fileserver exposes static resources (photos)
- Background process that generates thumbnails
- HTTP Methos to retrieve the directory structure
- React App for viewing photos grouped by directory
Embed React App
Golang http package exposes the http.FileServer
method that takes a FS as parameter
//go:embed frontend/dist
var app embed.FS
// creates a hierarchical FS starting from the specified subfolder
appBuild, _ := fs.Sub(*app, "frontend/dist")
// Serve a FS
http.Handle("/", http.FileServer(http.FS(appBuild)))
This actually works pretty well!
But there is a catch: you must neither serve resources from the index route.
//go:embed frontend/dist
var app embed.FS
// creates a hierarchical FS starting from the specified subfolder
appBuild, _ := fs.Sub(*app, "frontend/dist")
// Serving a directory
http.Handle("/res/", http.StripPrefix("/res", http.FileServer(http.Dir("./res"))))
http.Handle("/app/", http.StripPrefix("/app", http.FileServer(http.FS(appBuild))))
But i want to serve my react app from the index route and the static resources from the /static route. This is a job for a http middleware.
A job for a HTTP middleware
Essentially the middleware need’s to:
- detect if we’re asking for
index.html
, if so return the document - if we’re asking for an asset open the embedded FS to the correct folder
- apply the correct MIME
- return the buffer and also might set a cache-control header
TLDR;
react_handler.go
package pkg
func reactHandler(fs *fs.FS) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w,
http.StatusText(http.StatusMethodNotAllowed),
http.StatusMethodNotAllowed,
)
return
}
path := filepath.Clean(r.URL.Path)
// Frontend routes must be known.
// Add as many routes as frontend has.
// The frontend router will take care of redirecting to the correct view/component
if path == "/" || strings.HasPrefix(path, "/someroute"){
path = "index.html"
}
path = strings.TrimPrefix(path, "/")
file, err := (*fs).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, "assets/") {
w.Header().Set("Cache-Control", "public, max-age=2592000")
}
stat, err := file.Stat()
if err == nil && stat.Size() > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
}
io.Copy(w, file)
})
}
server.go
//go:embed frontend/dist
var app embed.FS
// creates a hierarchical FS starting from the specified subfolder
appBuild, _ := fs.Sub(*app, "frontend/dist")
// Serving the react app build
http.Handle("/", reactHandler(&appBuild))
// Now i can serve a different folder while keeping the react app on "/"
http.Handle("/media/", http.StripPrefix("/media", http.FileServer(http.Dir("./media"))))
Happy Coding!
And happy new year!