BeepBeep, CouchDB and a Trivial Blog

One of the core concepts of Django is that of loose coupling. The Django framework is a well-stocked toolkit filled with great tools which work well together, but it makes no effort to force its most-favored software into your project: you can ignore the existing tools and bring your own, if you so choose.

Comparing BeepBeep to Django, one might suggest that the term micro framework is a polite way of saying that BeepBeep is an empty toolkit. BeepBeep is able and willing glue between your components, but has a strong Bring-Your-Own-Tools philosophy.

In this tutorial we'll look at a powerful tool you should consider pairing with BeepBeep: CouchDB


Using CouchDB, we're going to create a very simple blog where we'll create blog posts by using a form to send HTTP POSTs, and have simple views for listing all the posts and viewing individual posts as well. It's unquestionably feature light, but perhaps a bit concept heavy, so let's get started1.

You can look at the completed code in the BeepBeep Examples repository on GitHub.

Dependencies & Setup

  1. First, follow the BeepBeep installation instructions if it isn't already installed.

  2. Next, install and configure CouchDB if you haven't already.

  3. Use new_beep.erl to create the project files for BB_Blog.

    cd ~/git/beepbeep/
    ./script/new_beep.erl BB_Blog ../BB_Blog
    
  4. Install erlang_couchdb from its GitHub repository:

    git clone git://github.com/ngerakines/erlang_couchdb.git
    

    Then make it and copy the resulting beams into the BB_Blog/deps/ folder.

    cd erlang_couchdb
    make
    mkdir ../BB_Blog/bb_blog/deps/erlang_couchdb
    mkdir ../BB_Blog/bb_blog/deps/erlang_couchdb/ebin
    cp  ebin/* ../BB_Blog/bb_blog/deps/erlang_couchdb/ebin/
    

    Please be aware that erlang_couchdb requires Erlang R12B5 or later, so you may need to update your Erlang installation.

  5. The last step in setup is to verify that the dependencies have been installed correctly. To do this, first we need to build the BB_Blog application.

    cd ~/git/BB_Blog/bb_blog/
    make
    

    Then start CouchDB in the background.

    couchdb &
    

    And start the BB_Blog app server.

    cd ~/git/BB_Blog/bb_blog/
    ./start-server.sh
    

    And then test out creating a CouchDB database.

    2>  erlang_couchdb:create_database({"localhost", 5984}, "test_db2").
    ok
    

    This means that CouchDB is running, erlang_couchdb is running, BeepBeep is running, and everything is gravy. Excellent.

    If something looks different, please post a comment with the error message you received.

Piecing Together Some CouchDB Functionality

In order to use CouchDB as our document store, we'll need to create a database. In order to retrieve documents we'll want to create a CouchDB view. We might also want a way to clear out the current documents, a way to retrieve a given document, and so on.

We'll house this functionality in the file src/bb_blog_utils.erl. I am going to skim over the details of this file, so if you haven't used CouchDB before, you may want to take a look at the documentation.

-module(bb_blog_utils).
-export([create_db/0, delete_db/0, info_db/0, reset_db/0]).
-export([create_document/3, delete_document/2, documents/0, documents/2, document/1]).
-define(COUCHDB_CONN, {"localhost", 5984}).
-define(BB_BLOG_DB, "bb_blog").

% Create a database in CouchDB, and then generate                                                 
% a CouchDB view for querying documents by slug.                                                  
create_db() ->
    erlang_couchdb:create_database(?COUCHDB_CONN, ?BB_BLOG_DB),
    erlang_couchdb:create_view(?COUCHDB_CONN, ?BB_BLOG_DB, "_blog", <<"javascript">>,
                               [{<<"entries">>, <<"function(doc) { emit(doc.slug, doc) }">>}]).

% Drop the database, including all documents and views.                                           
delete_db() ->
    erlang_couchdb:delete_database(?COUCHDB_CONN, ?BB_BLOG_DB).

% Retrieve the current status of the database.                                                    
info_db() ->
    io:format("~p",[erlang_couchdb:database_info(?COUCHDB_CONN, ?BB_BLOG_DB)]).

% Delete and then recreate the database.                                                          
reset_db() ->
    delete_db(),
    create_db().

% Create a blog entry in the database.                                                            
create_document(Title, Slug, Body) ->
    erlang_couchdb:create_document(?COUCHDB_CONN, ?BB_BLOG_DB,
                                   [{<<"title">>, list_to_binary(Title)},
                                    {<<"slug">>, list_to_binary(Slug)},
                                    {<<"body">>, list_to_binary(Body)}]).

% Delete a blog entry from the database.                                                          
delete_document(Id, Rev) ->
    erlang_couchdb:delete_document(?COUCHDB_CONN, ?BB_BLOG_DB, Id, Rev).

% Retrieve up to the first ten documents in the _blog/entries view.                               
documents() ->
    documents(0,10).

% Retrieve documents Offset - Offset+Limit from _blog/entries view.                               
documents(Offset, Limit) ->
    Attrs = [{"skip",Offset},{"limit",Limit}],
    Resp = erlang_couchdb:invoke_view(?COUCHDB_CONN, ?BB_BLOG_DB, "_blog", "entries",Attrs),
    {json, {struct, Data}} = Resp,
    Data.

% Retrieve a given document by its slug.                                                          
document(Slug) ->
    Attrs = [{"key",lists:concat(["\"",Slug,"\""])}],
    Resp = erlang_couchdb:invoke_view(?COUCHDB_CONN, ?BB_BLOG_DB, "_blog", "entries",Attrs),
    {json, {struct, Data}} = Resp,
    Rows = proplists:get_value(<<"rows">>, Data),
    [{struct, Row}] = Rows,
    Row.

Now go ahead and make the application again,

make

You can test the interface out quickly by running ./start-server.sh and then trying these commands:

26> bb_blog_utils:create_db().
{json,{struct,[{<<"error">>,<<"conflict">>},
               {<<"reason">>,<<"Document update conflict.">>}]}}
27> bb_blog_utils:delete_db().
ok
28> bb_blog_utils:create_db().
{json,{struct,[{<<"ok">>,true},
               {<<"id">>,<<"_design/_blog">>},
               {<<"rev">>,<<"1-800947609">>}]}}
29> bb_blog_utils:create_document("Title","slug","<p>some text</p>").
{json,{struct,[{<<"ok">>,true},
               {<<"id">>,<<"94f2f8eb0a94959454c3bfdbcdaa54e0">>},
               {<<"rev">>,<<"1-304401008">>}]}}
30> bb_blog_utils:info_db().
{ok,[{<<"db_name">>,<<"bb_blog">>},
     {<<"doc_count">>,2},
     {<<"doc_del_count">>,0},
     {<<"update_seq">>,2},
     {<<"purge_seq">>,0},
     {<<"compact_running">>,false},
     {<<"disk_size">>,14595},
     {<<"instance_start_time">>,<<"1247033421520632">>}]}ok

Now our interface to CouchDB is prepared, and we just need to do... everything else.

Teaching start-server.sh Some New Tricks

The first step in the reworking will be to make the start-server.sh script a bit more flexible. After extended it will let us create the CouchDB database, check the status of the database, and also reset the database, all from the command line.

mv start-server.sh manage.sh
emacs manage.sh

Replace the current code with this:

#!/bin/sh                                                                                       
cd `dirname $0`
case $1 in
#!/bin/sh                                                                                       
cd `dirname $0`
case $1 in
    server)
        exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader \
            -s bb_blog -detached
        ;;
    dev_server)
        exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader \
            -s bb_blog -detached
        ;;
    create_db)
        echo "create database..."
        exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils create_db \
            -run init stop -noshell
        ;;
    delete_db)
	echo "delete database..."
	exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils delete_db \
            -run init stop -noshell
	;;
    reset_db)
	echo "reset database..."
	exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils reset_db \
            -run init stop -noshell
	;;
    info_db)
	echo "info for database..."
	exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils info_db \
            -run init stop -noshell
	;;
    *)
      	echo "Usage: $0 {server|dev_server|create_db|delete_db|reset_db|info_db}"
	exit 1
esac
exit 0

After saving the file, you can test out the new manage.sh script.

will-larsons-macbook:bb_blog will$ ./manage.sh create_db
create database...
will-larsons-macbook:bb_blog will$ ./manage.sh reset_db
reset database...
will-larsons-macbook:bb_blog will$ ./manage.sh info_db
info for database...
{ok,[{<<"db_name">>,<<"bb_blog">>},
     {<<"doc_count">>,0},
     {<<"doc_del_count">>,0},
     {<<"update_seq">>,0},
     {<<"compact_running">>,false},
     {<<"disk_size">>,4096}]}
will-larsons-macbook:bb_blog will$ ./manage.sh server

Note that the distinction between server and dev_server is that server runs in -detached mode, and dev_server runs in the current console.

Extending BB_Blog to Display Blog Entries

To display blog entries we will need to make two controllers, two implementations of handle_request and three templates.

First let's create the index page, which will allow users to view all existing blog entries. Go ahead and open up src/home_controller.erl and replace the curernt contents with:

-module(home_controller,[Env]).
-export([handle_request/2]).
handle_request("index",[]) ->
    Data = bb_blog_utils:documents(),
    RawRows = proplists:get_value(<<"rows">>, Data),
    Rows = lists:map(fun({struct, X}) ->
                             {struct, Row} = proplists:get_value(<<"value">>, X),
                             CleanRow = lists:map(fun({Key, Value}) ->
                                               {list_to_atom(binary_to_list(Key))
                                                ,Value}
                                       end, Row),
                             CleanRow
                     end, RawRows),
    {render,"home/index.html",[{rows, Rows}]}.

This is a bit ugly, largely as a result of the format for deserialized JSON. It could undoubtedly be cleaned up a bit, but very simply it:

  • Retrieves the documents from CouchDB.
  • Transforms the CouchDB documents into an ErlyDLT friendly format.
  • Renders the home/index.html template with the transformed documents.

A quick debugging tip: if you add lines like:

error_logger:info_report(Data),

to your BeepBeep code and run in dev_server mode (i.e. with the server attached to a console), then you'll be able to see the incoming data quite handily.

Sure printing to console isn't the most advanced debugging technique, but it's rather handy when you're stuck trying to remember the awkward format of those deserialized JSON datastructures.


Relying on our CouchDB utility functions, the logic here is pretty concise. Then we need to tweak views/base.html to clean up the default template a bit:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>BB_Blog: Rational Exuberance</title>
  <link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
</head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>

And also tweak views/home/index.html:

{% extends "../base.html" %}
{% block content %}
  {% for row in rows %}
    <div class="doc">
      <h1><a href="/blog/view/{{ row.slug }}/">{{ row.title }}</a></h1>
    </div>
  {% endfor %}
{% endblock %}

Then make the code and navigate to http://localhost:8000. It should display a simple list of blog entries. Huzzah!


This is a good time to throw away views/home/show.html, since we don't need it.

rm views/home/show.html

The last step in displaying the blog entries is we want to create a controller and view pair to display the individual entries based on their slug. Go ahead and create src/blog_controller.erl.

-module(blog_controller,[Env]).
-export([handle_request/2]).

handle_request("view",[Slug]) ->
    Resp = bb_blog_utils:document(Slug),
    error_logger:info_report(Resp),
    {struct, Doc} = proplists:get_value(<<"value">>, Resp),
    CleanDoc = lists:map(fun({K,V}) ->
                                 {list_to_atom(binary_to_list(K)), V}
                         end, Doc),
    {render,"blog/view.html",[{doc, CleanDoc}]};

Since we're only dealing with one result, the cleaning process is a bit more compact this time.

Next, we need to create the blog/view.html view.

mkdir views/blog
emacs views/blog/view.html

And populate it with these contents:

{% extends "../base.html" %}
{% block content %}
  <div id="doc">
  <h1><a href="/blog/view/{{ doc.slug }}/">{{ doc.title }}</a></h1>
    <div class="body">
      {{ doc.body }}
    </div>
  </div>
{% endblock %}

Then make the source once more, jump on over to http://localhost:8000, and then click on one of the (or, more likely, the) document we created earlier to view it in its full glory.

The One Where We Can Create Blog Entries

Now that we can view blog entries, it would be mighty helpful if we could actually create blog entries as well. So, let's get cracking on that.

Note that this section introduces something a bit new, which is how to manage data received in POST requests via BeepBeep.

Return to the src/blog_controller.erl and add this request handler:

handle_request("create",[]) ->
    Title = beepbeep_args:get_param("title", Env),
    Slug = beepbeep_args:get_param("slug", Env),
    Body = beepbeep_args:get_param("body", Env),
    case lists:member("", [Title, Slug, Body]) of
        true ->
            {render, "blog/create.html",[]};
        false ->
            bb_blog_utils:create_document(Title, Slug, Body),
            {redirect, lists:concat(["/blog/view/",Slug,"/"])}
    end.

You'll also need to change the ending . to an ending ; on the existing implementation of handle_request.

Then go ahead and create the views/blog/create.html template,

{% extends "../base.html" %}
{% block content %}
  <div id="create">
    <h2>Create New Blog Entry</h2>
    <form method="POST" action="/blog/create/">
      <label for="title">Title:</label><input name="title"><br/>
      <label for="slug">Slug:</label><input name="slug"><br/>
      <label for="body">Body</label><br/>
      <textarea name="body"></textarea><br/>
      <input type="submit" value="Create">
    </form>
  </div>
{% endblock %}

With that, make the code, and head off to http://localhost:8000/blog/create/ to get started with your blogging... your painfully limited and awkward blogging.

The End

With this third BeepBeep tutorial, we're approaching a point where we've covered many of the tools needed to create a legitimate web application. Throughout this tutorial I cut away features that I wanted to cover, because this was really about showcasing using CouchDB with BeepBeep, rather than trying to build a servicable blog application. As such, due apologies for the minimal ending state, but hopefully it was educational nonetheless.

You can grab the source for BB_Blog in the BeepBeep Examples repository, under the BB_Blog directory. As always, glad to hear any comments or complaints, and I'll figure out where this thread of BeepBeep tutorials is headed.


  1. If you want a more full-featured blog powered by BeepBeep, you could take the CouchDB aspects of this tutorial and merge them with the blog example in the beepbeep/examples folder, which contains quite a bit more functionality (although, even so checks in as an extremely lightweight blogging client :). Beyond that, it's a great example of BeepBeep techniques worth reading on its own merits.

All Rights Reserved, Will Larson 2007 - 2014.