Webmachine in Elixir Tutorial, Part 2
by Sean Cribbs
Last time, we got our project set up and serving some simple dynamic content. In this installment, we’ll show how to serve static files via Webmachine so we can discuss lots of its best features.
Serving static files
Most times you would let a web-server like Apache or nginx serve your
static files, but for our tutorial it’s nice to serve our content
directly via Webmachine. By doing so, we can demonstrate several
important features related to the dispatcher, content-negotiation, and
conditional requests. Basically, everything you’d expect out of a
well-configured web-server but in a resource module! First make a
priv directory (where OTP apps store non-code files) and we’ll put
our design assets in there. For now, we’ll just copy files from the
Let’s make a new resource module called
fill out the boilerplate. For our resource state, we’ll use a map this
time, but we’ll probably change it to a struct later.
Now we need to think about a few things, namely, how to determine
which file is being requested, what media type it is, and then how to
read it from the filesystem out to the client. Let’s start from the
end, assuming we’ve already determined the correct file to read. We’ll
make a body-producing function that simply reads the file and sends it
to the client. This is not the most efficient way –
other streaming would be better – but we are serving small files so
it won’t be too bad.
That was easy! Continuing backwards through our list, let’s
determine the media type of the file and point it at our
body-producing function using the
Webmachine callback. This callback tells Webmachine what media
types you provide, and what to call to produce each one. Since ours
is just reading a file from the filesystem, we’ll call
produce_resource, but vary the type it produces.
This is the first time we’ve used a Webmachine library function in
:wrq.disp_path gives us the portion of the path that
the dispatcher matched against. So at the root URL, this will be
the empty string, otherwise, it’ll be a partial path to some file,
:webmachine_util.guess_mime is used
to guess what a proper media type will be. For fun, let’s try that
function from the shell via
iex -S mix.
Now that we have a body producing function, and the correct MIME
type, let’s find the file on the filesystem, via one of the most
resource_exists. Obviously, if the file
doesn’t exist in our static assets, we should return a
Found, and this is also a perfect place to populate the state with
an absolute path to the requested file.
Before we move on, there’s some repeated functionality with
content_types_provided, and we have a minor bug too – at the
root path we want to serve
index.html. Let’s extract that shared
functionality into a new function.
To get this resource to actually serve content, we now need to hook
it up to the dispatcher. Let’s edit
tweeter.ex again, replacing
Hello resource with
Note the special path segment
:*. This tells the Webmachine
dispatcher to match any number of trailing path
segments. Kill/restart your
mix process and refresh the page!
This strategy of reading a file from disk and sending it to the client is as old as the web itself, but there’s much more we can do! HTTP includes caching in the protocol, and it’d be pretty inefficient for a client to fetch unchanged design assets every time they refresh the page.
Let’s add some simple validation caching to our assets resource. We
can start by using the
This is pretty simple: we read the file statistics, pulling out the
mtime field which represents when it was last modified. We can
File.stat! function instead of its safe equivalent
because of the flow of Webmachine’s decision graph. That is, we
last_modified will not be called if
We can go even further by using entity tags, or “ETag” for
short. These are usually a hash string of various aspects of the
file’s metadata. Since we might be doing that
File.stat! call in
multiple places, let’s put it in
resource_exists while we’re at
it and save the result.
We use the built-in
:erlang.phash2 function to compute the ETag,
but you should probably use a better hash in other resources.
Finally, I noticed that our CSS and HTML, although small, are still
multiple kilobytes. We can reduce the transmission time
significantly through compression, using the
callback. Somewhat similar to
content_types_provided, it returns
a list of pairs, where the first is an encoding and the second is a
fn that performs the encoding.
Note that this is a case again where Webmachine requires character
lists and not binaries (single-quoted strings). Now that our
compression is in place, I see the
index.html file going from
~1KB to 560B, and the CSS file from 6.7KB to ~1KB. Nice bandwidth
In the next installment, we’ll learn start serving some dynamically-generated content from a resource.