Thoughts on backend and scaling

Templating with ErlyDTL 2

In this second blogpost I introduce some advanced features of Erlydtl templating (in the first post I showed how to create custom behaviours) like template inclusion, extension and basic setup of a template-enabled Erlang application.

Set up environment

Create a directory and download erlang.mk. For the last couple of projects I used erlang.mk over rebar because for me it is better customizable.

curl https://raw.githubusercontent.com/ninenines/erlang.mk/master/erlang.mk -O

In the project we will have src and templates directories. Let us create a Makefile with which we can compile the application including the templates, and also create the release itself. The Makefile looks like this

PROJECT = product
DEPS = cowboy erlydtl

include erlang.mk

Issuing a make command will bootstrap erlang.mk and then we can compile the application by make app. Let us create a simple Restful application.

Restful Cowboy

The product application will have and application description file (product.app.src), the application behaviour (product.erl), a supervisor (product_sup.erl) and a rest handler (product_rest.erl). The supervisor won’t do anything but start cowboy and register dispatch rules. That is the minimal set of Erlang files we can live with. File will be put into src directory. See the product.app.src file

{application, product, [
    {description, "Simple product RESTful app"},
    {id, "product"},
    {vsn, "0.0.1"},
    {modules, []},
    {applications, [
        kernel, stdlib,
        cowboy, erlydtl
    ]},
    {registered, []},
    {mod, {product, []}}
]}.

It contains our minimal needs, the dependent applications (cowboy and erlydtl), and it starts the product application module.

-module(product).
-behaviour(application).

-export([start/2, stop/1]).

start(_StartType, _Args) ->
    Rules = [{'_', [{"/product/:id", product_rest, []}]}],
    Dispatch = cowboy_router:compile(Rules),
    cowboy:start_http(product_http, 5, [{port, 8080}],
                      [{env, [{dispatch, Dispatch}]}]),
    product_sup:start_link().

stop(_State) ->
    ok.

The application module starts the supervisor and besides registering cowboy dispatch rules it starts cowboy.


-module(product_sup).
-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    {ok, {{one_for_one, 1, 1}, []}}.

Now we stop a bit and create the relx.config to see if we have the minimal application which we want. With relx the release creation is very simple, we just need to specify the release name and the applications we use and that i is. During development it is a good idea to specify dev_mode, in this way the available OTP release won’t be copied into our _rel directory but symlinks will be created. Put the relx.config into the project root, erlang.mk will download relx executable automatically, so when we execute make rel make will call relx to create the release in the _rel directory by default. If the extended_start_script is true it will create a product script into _rel/product/bin directory with which we can start the application (or by make run).

{release, {product, "0.0.1"},
          [cowboy, erlydtl, product]}.
{extended_start_script, true}.
{dev_mode, true}.

With make rel the Erlang release is build, so with make run we can run the application. Bingo.

Implement rest handler

For the sake of simplicity the rest handler contains a wired database. It contains a generic product (with id 1) which will be displayed by generic.dtl template (see later), and another product which is rendered by guitar.dtl template.


-module(product_rest).

-export([init/3,
        content_types_provided/2,
        content_types_accepted/2,
        allowed_methods/2,
        handle_get/2,
        handle_post/2]).

init(_Protocol, _Req, _Opts) ->
    {upgrade, protocol, cowboy_rest}.

content_types_provided(Req, State) ->
    Handlers = [{<<"application/json">>, handle_get}],
    {Handlers, Req, State}.

content_types_accepted(Req, State) ->
    Accepted = [{{<<"application">>, <<"json">>, '*'}, handle_post}],
    {Accepted, Req, State}.

allowed_methods(Req, State) ->
    {[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.

handle_get(Req, State) ->
    {Param, _} = cowboy_req:binding(id, Req),
    Id = binary_to_integer(Param),
    {ok, Msg} = case {Id, get_product(Id)} of
                    {1, P} ->
                        generic_dtl:render([{product, P}]);
                    {2, P} ->
                        guitar_dtl:render([{product, P}])
                end,
    {Msg, Req, State}.

%% Sample POST handler for sake of example :)
handle_post(Req, State) ->
    {ok, Body, Req2} = cowboy_req:body(Req),
    {process_json(Body), Req2, State}.

process_json(Binary) ->
    case post_handler(Binary) of
        ok ->
            true;
        {error, _Reason} ->
            halt
    end.

get_product(Id) ->
    case Id of
        1 ->
            #{id => 1,
              description => <<"Guitar leather bag">>,
              category => #{name => <<"Other">>}};
        2 ->
            #{id => 2,
              description => <<"Jackson SL-3">>,
              category => #{name => <<"Electric guitar">>},
              frets => 24,
              body => <<"Alder">>,
              pickup => <<"Seymour Duncan">>}
    end.

post_handler(_) ->
    ok.

This is the longest module, there are mandatory functions which implements the REST API (all exported functions). The get_product/1 function is the wired product database, and in the handle_get/2 we will get the Id sent in the URL path, and also gets the product and then render it conditionally.

The base template is generic.dtl which is


{
    "id": {{ product.id }},
    "description": "{{ product.description }}",
    {% block category %}
    "category": "{{ product.category.name }}"
    {% endblock %}
    {% block specific %}
    {% endblock %}
}

It defines the generic part serializing id and description which are in every product. We give a default implementation for category, and we are waiting the specific part, which will be defined by specific products like guitars, keyboards, etc. The guitar.dtl template extends the generic template, and refines the implementation of category.


{% extends "generic.dtl" %}

{% block category %}
    "category": "Guitar/{{ product.category.name }}"
{% endblock %}
{% block specific %},
    "frets": {{ product.frets }},
    "body": "{{ product.body }}"
{% endblock %}

We can use {% include "file.dtl" %} for externalizing complex and/or reusable parts of the template. It is not a big deal, all variables which are in the including template will be visible in the included template.

Takeaways

So when we need to create formatted messages which collect a number of variables or behave differently depending on some input parameters we can use ErlyDTL template with success. Also, creating JSONs or maps which are the datasource of JSONs, can make the source hard-to-understand. A lot of boilerplate code, and only a small lines of real business logic. If it is the case, use templates. In part 1 you can learn how to implement custom ErlyDTL library, which gives the possibility to enrich the functionality of the templates you create.