An Erlang, A Cowboy, and A Mustache Walk Into A Bar
Having just arrived at the ACT airport at 10:30AM for my flight at 6:30PM, I decided to try out another programming language. This time it's Erlang.
Given my current fascination with htmx, I though it a good idea to see if I could get a basic cowboy web server working with some HTML templating engine. We won't look at using htmx here but as templating is important for it, it seemed interesting enough to motivate me to learn Erlang
Soranoba's mustache library called bbmustache seemed the most approachable.
Code for this project can be found here.
Getting Started
The most difficult thing about getting started with Erlang wasn't the language, or the libraries, but rather the build system.
I'm sure Rebar3 is a great build system for those in the know, but for an outsider it's pretty opaque and has a steep learning curve.
The easiest and most well documented template
to get started with is the release template
which we can instantiate with
rebar3 new release example_server.
Adding dependencies
The release template allows for multiple
applications to be defined in the same project.
Because of this we have to add our dependencies
in two places; rebar.config and
apps/example_server/src/example_server.app.src.
In rebar.config we'll change the following line.
{deps, []}.
We'll add our two dependencies, cowboy, and
bbmustache.
{deps, [bbmustache, cowboy]}.
Now usually, that'd be all it takes to add the two
dependencies. Annoyingly at the moment though
it seems like rebar3 has some issues with
versioning that makes it think cowlib (a
dependency of cowboy) has an invalid version.
You can see this error now if you run
rebar3 compile, but we'll fix the issue right
away by adding some overrides. Don't ask me
how this works yet though, I'm still new. I found
this info in the issues for the cowboy project.
This should be added at the root of rebar.config.
I put mine right below deps.
{overrides, [
{override, cowboy, [{deps, [cowlib, ranch]}]}
]}.
Now you should be able to run rebar3 compile
without seeing any errors.
We're not quite done though. This adds the two
dependencies to the release so rebar3
pull them in and compiles them, but we also
need to add them to our app so rebar knows
we depend on them.
Here we open
apps/example_server/src/example_server.app.src
and add bbmustache and cowboy to our app's
dependencies.
{application, example_server, [
{description, "An OTP application"},
{vsn, "0.1.0"},
{registered, []},
{mod, {example_server_app, []}},
{applications, [
kernel,
stdlib, % Don't forget this comma.
bbmustache, % Here,
cowboy % and here
]},
{env, []},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, []}
]}.
rebar3 compile should still return without
errors if we've done everything correctly.
Now Where The Heck Does My Code Go?
Now we've got our project set up and building with our dependencies, where do we actually put our logic? By the looks of it code in Erlang applications is usually monitored by supervising code so it can be automatically restarted.
To do this we need to make a Child Spec that describes how to start the cowboy web server and when to automatically restart it. We'll supply the cowboy web server with Dispatch Rules that describe how to run our custom handlers.
First, open up
apps/example_server/src/example_server_sup.erl.
This module launches the supervisor. In particular,
we want to modify the init function, adding our
child specification.
init([]) ->
%& Create the routes and map them to dispatch
%% rules. Note that this should be moved to
%% its own module once it gets more complex.
Dispatch = cowboy_router:compile([
{~"localhost", [{~"/", handler_index, []}]}
]),
%% Create a child spec for the cowboy server
%% so it can be restarted if it fails.
%% Note that this could also be returned from a
%% function in another module.
CowboyChildSpec = #{
%% This `cowboy_web_server` is an `atom()` that
%% just acts as a uniqe identifier for the
%% process as far as I can tell
id => cowboy_web_server,
start =>
{cowboy, start_clear, [
cowboy_web_server,
[{port, 8000}],
#{
env => #{dispatch => Dispatch}
}
]},
restart => permanent,
shutdown => 5000
},
SupFlags = #{
strategy => one_for_all,
intensity => 0,
period => 1
},
%% Add the CowboyChildSpec to the ChildSpecs
%% for our supervisor
ChildSpecs = [CowboyChildSpec],
{ok, {SupFlags, ChildSpecs}}.
Let's check out Dispatch first.
Dispatch = cowboy_router:compile([
{~"localhost", [{~"/", handler_index, []}]}
]),
cowboy_router:compile turns a mapping of hosts to lists of
routes and their handling modules into actionable dispatch rules
for the cowboy server.
Here handler_index is a module with -behaviour(cowboy_handler)
that we haven't implemented yet. This module defines how requests
to the root (/) of the server are handled. Note ~ here just
means the string is encoded as UTF-8.
Next, we'll create the handler.
A Toy Handler
So we can test to make sure we haven't done anything wrong yet,
we'll create a placeholder handler for the index. We'll fill
this in with templating logic making use of bbmustache next,
but for now we'll create a new file at
apps/example_server/src/handler/handler_index.erl.
-module(handler_index).
-behaviour(cowboy_handler).
-export([init/2]).
init(Req, State) ->
Req_1 = cowboy_req:reply(
200,
#{~"content-type" => ~"text/html"},
"<h1>Hello, World!</1>",
Req
),
{ok, Req_1, State}.
Note the line -behaviour(cowboy_handler). This enforces
that this module contains the required functions to be
considered a cowboy_handler. If you're using the language
server from the
Erlang Language Platform,
you can actually generate the required functions as a code action
in your editor once -behaviour has been added.
In our init function here we create a reply to the request
with the code 200 (ok), a content-type of text/html with
a body that contains a simple heading.
If you run rebar3 shell from the root of our release to start
the server, then check out http://localhost:8000 in your
browser, you should see the HTML that we're serving.
Templates Are Easy, Rebar3 Is Weird
Now, we'll write a quick mustache template for our
index page called index.html but where do we put it?
We need to find some way to ship files with our release
when we build it. This is what the /priv directory is
for.
Where apps/example_server/src contains the Erlang
source files, apps/example_server/priv contains
any files that are required at runtime and should be
shipped along with the application. Erlang provides the
code:priv_dir function specifically to find where
this /priv directory is, as it varies depending on
how your code is running (release, debug, etc).
What we'll do then is create a new module at
apps/example_server/src/template.erl to help us find,
read, and compile templates from our /priv directory.
-module(template).
-export([load/1]).
-doc "Reads and parses a mustache file from /priv".
load(TemplateName) ->
%% Get the /priv dir of the example_server app
PrivDir = code:priv_dir(example_server),
%% Ensure the results is filename() by checking that it's
%% a string() which is a list of chars.
case is_list(PrivDir) of
true ->
%% If it is, generate the file path
TemplatePath = filename:join([PrivDir, TemplateName]),
%% Then read template from the path
{ok, TemplateBinary} = file:read_file(TemplatePath),
%% And compile it
bbmustache:parse_binary(TemplateBinary);
false ->
error({priv_dir_not_found, example_server})
end.
We'll use this to load an index.html template and generate
a new index from that template next.
A Marginally More Interesting Index
First, we'll need a template in apps/example_server/priv/index.html.
<h1>{{heading}}</h1>
{{#paragraphs}}
<p>{{.}}</p>
{{/paragraphs}}
I know, truly a work of art right? In reality you'd hope to have some more complex things to display for all this work to be worth doing.
Now this is in here, we can rewrite our handler_index.erl
module.
-module(handler_index).
-behaviour(cowboy_handler).
-export([init/2]).
init(Req, State) ->
% Get the template for the index
Template = template:load("index.html"),
% Create a map for the template
Data = #{
"heading" => "Hello, World!",
"paragraphs" => ["Paragraph 1", "Paragraph 1"]
},
% Compile the template
Body = bbmustache:compile(Template, Data),
Req_1 = cowboy_req:reply(
200,
#{~"content-type" => ~"text/html"},
Body,
Req
),
{ok, Req_1, State}.
Here we get the template from our /priv directory using
the template module we wrote, define some data in a map to
fill the template, then compile the template into the final
HTML to send along in the body.
Running rebar shell, we should now see our template-generated
HTML at http://localhost:8000. If you don't, good luck.
You should be able to build a shippable project with
rebar3 release, but we might leave that as an exercise to the
reader because I've got to go catch a plane.