Using mochiweb to create a web framework in erlang

 

Recently, I used Mochiweb for several projects ( Alice) I've been working on. After some investigation of the current erlang web frameworks, Mochiweb suited our needs well. It's lightweight, fast, open-source and pretty source code. Throughout this post, we'll build a little mochiweb application, so note that it will be available in full for download at the end of the article, but we'll write bits and pieces at a time.

So with no further ado:

Using mochiweb in erlang

Starting mochiweb is pretty straightforward. Just calling the mochiweb_http:start/1 function, we'll start the mochiweb application. This is, to me the most dynamic way to start a mochiweb application. While it can be done in a different way, this allows more flexibility. Notice that we'll be passing a loop function. We'll define that shortly, but for now, just note that it's the function that will be receiving the requests.

-module (mochiweb_server).
-export ([start_mochiweb/1]).

start_mochiweb(Args) ->
  [Port] = Args,
  io:format("Starting mochiweb_http with ~p~n", [Port]),
  mochiweb_http:start([ {port, Port},
                        {loop, fun dispatch_requests/1}]).

Now, as promised, let's look at how to handle we'll handle the requests:

-export ([dispatch_requests/1]).

% ...

dispatch_requests(Req) ->
  Path = Req:get(path),
  Action = clean_path(Path),
  handle(Action, Req).

% Get a clean path
% strips off the query string
clean_path(Path) ->
  case string:str(Path, "?") of
    0 -> Path;
    N -> string:substr(Path, 1, N - 1)
  end.

The Req record that is passed in is a mochiweb_request record, which gives us access to all the methods defined in the mochiweb_request record. We'll use the Req:get(path) method to pull out the path. Notice that we are also pulling out the Action the path defines by stripping off any query string at the end. Sweet.

Now, for some nifty request handling, we'll use the handle method to give us the ability to handle requests with Erlang's pattern matching:

handle("/favicon.ico", Req) -> Req:respond({200, [{"Content-Type", "text/html"}], ""});
handle(Path, Req) -> 
    Req:respond({200, [{"Content-Type", "text/html"}], "
### Hello world
"});

Sweet! Digging a little deeper, we can see that any request that is not /favicon.ico is going to respond with Hello World (Also, notice the use of respond on the Req record). The respond method takes a tuple that consists of:

{status, [{proplist_of, headers}], Body}

So obviously we can respond in different ways to our different requests. Let's dig a little deeper and build out some controllers, an obvious enhancement. First, we'll modify our handle method:

handle(Path, Req) ->
  BaseController = lists:concat([top_level_request(clean_path(Path)), "_controller"]),
  CAtom = list_to_atom(BaseController),
  ControllerPath = parse_controller_path(clean_path(Path)),

  case CAtom of
    home ->
      IndexContents = ?ERROR_HTML("Uh oh"),
      Req:ok({"text/html", IndexContents});
    ControllerAtom -> 
    Meth = clean_method(Req:get(method)),
    case Meth of
      get -> 
        run_controller(Req, ControllerAtom, Meth, [ControllerPath]);
      _ -> 
        run_controller(Req, ControllerAtom, Meth, [ControllerPath, decode_data_from_request(Req)])
    end
  end.

% parse the controller path
parse_controller_path(CleanPath) ->
  case string:tokens(CleanPath, "/") of
    [] -> [];
    [_RootPath|Rest] -> Rest
  end.

% Call the controller action here
run_controller(Req, ControllerAtom, Meth, Args) ->
  case (catch erlang:apply(ControllerAtom, Meth, Args)) of
    {'EXIT', {undef, _}} = E ->
      Req:ok({"text/html", "Unimplemented: there is nothing to see here"});
    {'EXIT', E} -> 
      Req:not_found();
    Body -> 
      Req:ok({"text/html", Body})
  end.

% Other methods
% Get the data off the request
decode_data_from_request(Req) ->
  RecvBody = Req:recv_body(),
  Data = case RecvBody of
    > -> erlang:list_to_binary("{}");
    Bin -> Bin
  end,
  {struct, Struct} = mochijson2:decode(Data),
  Struct.

% parse the controller path
parse_controller_path(CleanPath) ->
  case string:tokens(CleanPath, "/") of
    [] -> [];
    [_RootPath|Rest] -> Rest
  end.

% Get a clean path
% strips off the query string
clean_path(Path) ->
  case string:str(Path, "?") of
    0 -> Path;
    N -> string:substr(Path, 1, N - 1)
  end.

top_level_request(Path) ->
  case string:tokens(Path, "/") of
    [CleanPath|_Others] -> CleanPath;
    [] -> "home"
  end.

Now we can use any controller with the path appended to call out to a controller of our choosing that respond to the four http methods, get, put, post and delete! To finish off, let's add a controller that responds with our hello world message:

-module (home_controller).
-export ([get/1, post/2, put/2, delete/2]).

get(Path) -> "hello world".

post(_Path, _Data) -> "unhandled".
put(_Path, _Data) -> "unhandled".
delete(_Path, _Data) -> "unhandled".

Now we have an application-scalable web framework written in erlang with mochiweb.

Thanks to damjan for pointing out the clean_path correction.