InVision Rye: Serving Static Files and Performing Magic
I work on a services team. We don’t do websites, generally. Of course, this doesn’t mean we don’t have a need for websites. But it is somewhat odd to hear that a design-centric company has a small team of developers who really don’t work on websites. We spend most of our time deep in the depths of RESTful API development or server-side multi-threaded processing. Now, granted, all of us on the team have worked on websites in the past. We know how, we just don’t do it very often.
The Problem…
Recently, we were tasked with building a simple service to act as an overall service catalog. As we built out the service-catalog, which would have some interactions with a chat-bot ultimately, we realized that to interact with updating and viewing data, we needed a front-end. This required much discussion. Here’s a team of non-website folks, arguing over building a site in various technologies. We all come to the table with some ideas, but we came to using React because other teams in our company were starting to build out internal and external sites with React.
Myself, I felt very comfortable with this decision. I’ve build a lot of things using React and am very familiar with it. My most popular open source project was at my last company and it was written for React Native. Never thought that something I worked on would have a whole 200+ stars on Github. Anyhow, I digress. Building a website in React is not a hard task, in my opinion. Building it with all the Javascript dependencies, proper build, navigation, data management, and all the extras can be another story, however. Deployment can be difficult. NPM packaging and versions can be difficult. Building it as a single-page application can be difficult. And ultimately, serving it can be difficult.
The Desire…
What we wanted was this: we wanted to serve our single-page application from our
Golang written API server. PLUS, we’d really like to not have to copy assets
into our Docker container that relied on any other dependencies… like
node_modules
or npm
or node
or anything that wasn’t just the executable
sitting in an Alpine Golang docker image. This is what we wanted, and this is
how we solved it: using InVision Rye, serving static files and then figuring out
how to bundle those static files into one Golang executable.
The Approach…
So, we went about creating a single-page Application using React. This required
a metric-ton of named Javascript projects. We setup NPM, then created a webpack
configuration, then built out an index.html that would load our single-page
application. Of course we needed some npm
commands specifically to build our
React application in Babel
and then deliver a bundle.js
to a dist
folder.
Additionally, with React, we added react-router
, and react-redux
to manage
data interactions. On top of that we had static resources we wanted to deliver
such as react-bootstrap
which included fonts and images. Great! This is all
fantastic and works great in a folder ~ as long as you have some way to SERVE
it. For the time being, I would just use Python to serve it. Our Webpack was
built out to deliver all the assets in a single folder.
One command pointed at your folder, and you’re off to the races:
cd dist
python -m SimpleHttpServer 8081
This worked pretty well for development. But at some point we were going to have to deploy this. Since our service, Astro, was written in Golang using the Rye middleware library, we needed to figure out how to properly serve this from there. The plot thickens.
Golang http.FileServer
for delivering static files
So, we needed to figure out how to easily deliver static files for our UI from
Rye. We also use Gorilla as a router,
so getting the right mix was a big part of that. There were actually some tricky
bits around this, in regard to serving out of a Rye handler. Golang provides
http.FileServer()
which was our first approach. Since Python’s
SimpleHttpServer
was working for the most part, we figured this would be
simple. However, not true. We kept having bugs with
React-Router. You couldn’t
navigate to paths in our app using http.FileServer()
. You could visit the
homepage, and even navigate in the app using our Bootstrap Nav Bar, but you
could not go DIRECTLY to a link. Why? Our Gorilla router was interrupting the
request and not allowing React-Router
to do its work.
Finally, we realized that to get what we needed out of the single-page application, we needed to:
- Provide a path to get static assets and a route that served those static
assets through
http.FileServer()
- Provide a path that on the server side would ALWAYS serve our
index.html
file which makes up the single-page application
Since we were building this as an add-on to the service, we wanted our main-path
to be /dashboard
. Therefore, we decided that other static assets would get
served from /dist
such as fonts, images, bundle.js
, CSS, etc.
The configuration for this, using Rye, ended up not being terribly difficult (found in static_example.go).
pwd, err := os.Getwd()
if err != nil {
log.Fatalf("NewStaticFile: Could not get working directory.")
}
routes.PathPrefix("/dist/").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
rye.NewStaticFilesystem(pwd+"/dist/", "/dist/"),
}))
routes.PathPrefix("/dashboard/").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
rye.NewStaticFile(pwd + "/dist/index.html"),
}))
Two new middlewares
We required a middleware that could always serve one file and a middleware that
could serve a whole filesystem. This led to: middleware_static_file.go
and
middleware_static_filesystem.go
.
Middleware Static File
Taking in an absolute path local to the server, you can serve a single file.
Using the PathPrefix
in the Gorilla Mux sample above, this allows you to have
a path ALWAYS load this single file. The handler is really simple:
func (s *staticFile) handle(rw http.ResponseWriter, req *http.Request) *Response {
http.ServeFile(rw, req, s.path)
return nil
}
Middleware Static Filesystem
Taking in an absolute path local to the server, you can serve a full path. Using
the PathPrefix
in the Gorilla Mux sample above, this allows you to serve
INDIVIDUAL files in that filesystem on that prefix. Another simple handler:
func (s *staticFilesystem) handle(rw http.ResponseWriter, req *http.Request) *Response {
x := http.StripPrefix(s.stripPrefix, http.FileServer(http.Dir(s.path)))
x.ServeHTTP(rw, req)
return nil
}
A small caveat… StripPrefix
Since our static directory in the sample above is called /dist/
, we want to
strip that prefix from the request path BEFORE searching the file system for the
file. The StripPrefix
function is described here: StripPrefix from http
package in Golang. This
ultimately means that since you’re pointing at a dist
folder on the
filesystem, the matching of the URL will go straight to the sub-path.
So, what does this mean for my static assets, how do I point at them?
Well, from HTML (index.html), that’s being served from the /dashboard/
above,
how do you point at your bundled javascript, or CSS? An example can be found in
the
static-examples
folder in the Rye project. What you do here, is point at the static assets under
the /dist
URL prefix. Here’s an example index.html
with a reference to a
static CSS:
<html>
<head>
<title>Index.html</title>
<link rel="stylesheet" type="text/css" href="/dist/styles/index.css">
</head>
<body>
<h1>
Index.html
</h1>
</body>
</html>
Great! But wait, there’s more!
Now, we had a way, using Rye, to deliver our single-page application. NIFTY!
But, we would build out the Astro executable in Go as a single file, and then
we’d need to point at files in the file system to run the dashboard. YUK! Since
we run everything in Docker, this got worse! We would have to copy the
filesystem of the /dist
folder to the Docker image, and then configure the
paths in our route handlers to point at the local location. This added a lot of
complexity.
What we really wanted was to serve our single-page application directly from the executable. EMBEDDED RESOURCES! What if we had a way to bundle our entire application in one executable and still be able to serve the single-page application?
Magic (and a little help)
There are various resource bundling options for Golang. This article had some
great examples and some added complexity: Embedded resources in
Golang. However,
there’s a great project out there created by Jaana B.
Dogan called
Statik. The project is super simple. It uses
a command line tool, to take an entire filesystem and stuff it into a single
Golang package that you can bundle and build with your application. All we
needed was to add statik -src=/path/to/dist/folder
to our build pipeline and
build some Rye middlewares to support it. These middlewares are not found in the
Rye library, but you can use the code yourself if you want to try it out!
You’ll notice below, there’s also a _ "github.com/MyOrg/MyProject/statik"
.
This is a requirement so that the handlers can get at the static assets. NOTE,
sometimes some IDE’s will have trouble with this syntax. You can ignore it, it
works just fine.
package api
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"runtime"
"time"
// This is required by the statik package
_ "github.com/MyOrg/MyProject/statik"
"github.com/InVisionApp/rye"
"github.com/rakyll/statik/fs"
)
// This handler serves all Dashboard public artifacts from a static file system in memory
func (a *Api) uiDistStatikHandler(rw http.ResponseWriter, r *http.Request) *rye.Response {
statikFS, err := fs.New()
if err != nil {
return &rye.Response{
StatusCode: http.StatusInternalServerError,
Err: err,
}
}
x := http.StripPrefix("/dist/", http.FileServer(statikFS))
x.ServeHTTP(rw, r)
return nil
}
// This handler serves the index.html from a static file system in memory
func (a *Api) uiStatikHandler(rw http.ResponseWriter, r *http.Request) *rye.Response {
statikFS, err := fs.New()
if err != nil {
return &rye.Response{
StatusCode: http.StatusInternalServerError,
Err: err,
}
}
file, err := statikFS.Open("/index.html")
if err != nil {
return &rye.Response{
StatusCode: http.StatusInternalServerError,
Err: err,
}
}
b, err := ioutil.ReadAll(file)
if err != nil {
return &rye.Response{
StatusCode: http.StatusInternalServerError,
Err: err,
}
}
http.ServeContent(rw, r, "index.html", time.Now(), bytes.NewReader(b))
return nil
}
Routing for Dev Mode and Statik Mode
Finally, how can you develop your single-page application if it gets bundled into bytes in a Golang file? You need a DEV MODE. The way we solved this was simple. We created a command-line flag to represent dev mode. That meant that we could serve the ACTUAL assets (and could hot-load changes) when in DEV mode and then when we ran in production we could run off the STATIK assets. This allows us an easy way to make modifications and yet still be able to deliver a great bundled experience for our docker container.
if a.DebugUI {
pwd, err := os.Getwd()
if err != nil {
log.Fatalf("NewStaticFile: Could not get working directory.")
}
routes.PathPrefix("/dist/").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
rye.NewStaticFilesystem(pwd+"/dist/", "/dist/"),
}))
routes.PathPrefix("/dashboard/").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
rye.NewStaticFile(pwd + "/dist/index.html"),
}))
} else {
log.Info("ui: statik mode (from statik.go)")
routes.PathPrefix("/dist").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
a.uiDistStatikHandler,
}))
routes.PathPrefix("/ui").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
a.uiStatikHandler,
}))
}
Finally, UI from a Services Team.
So, there we have it. If Astro wasn’t a super secret project, I’d drop a screenshot here. However, the UI (from a service team) is managable, updatable and usable. But, not super pretty. We learned a ton in delivering this and were able to spread the idea to other projects.
One of those other projects is an Open-Source project called
9Volt which uses Rye as it’s mechanism for
Middleware. In 9Volt
the UI is different but there is a React application
using Redux and Semantic-UI for a dashboard experience. This example is public
and fully-fleshed out if you would like to see how it all hooks together
(including a build pipeline). The setup of the routing is here:
api.go, the setup of
the UI middlewares (doesn’t use the new Rye static middleware) is here:
ui.go, the single-page
application is here: 9Volt/ui,
the MAKEFILE, builds out
the UI in target build/ui
and supports dev in ui/dev
, and finally, to see a
statik build, look here:
9Volt/statik/statik.go.
We will likely build more dashboard user interfaces using this technique. We hope this will help you out in building easily deployable web applications along with your RESTful APIs.