Want more? Subscribe to my free newsletter:

Building Backbone.js Apps With Ruby, Sinatra, MongoDB and Haml

March 23, 2012

Introduction

In this post we're going to explore writing Backbone.js applications with a Ruby back-end. To assist with this, we're going to use Sinatra - a DSL (domain specific language) for rapidly creating web applications in Ruby. Similar to the section in Backbone Fundamentals on writing an application with Node.js, our server-side language (Ruby) will be used to power an API whilst Backbone.js will be the client consuming it.

What Is Sinatra?

In the past, you've likely come across or used Ruby on Rails (RoR) - a popular web application framework for the Ruby programming language that helps organize applications using the MVC pattern. Sinatra is a much smaller, more light-weight alternative to it.

Whilst a very basic Rails application may require a more strict project structure (such as requiring the use of controllers, views and routing etc.), Sinatra doesn't require as many of these dependencies, sacrificing the helpers needed to connect to databases, tools to create forms or any of the other utilities Rails comes with out of the box.

What Sinatra does have is a minimal set of features most useful for tying specific URLs and RESTful HTTP actions to blocks of Ruby code and returning this code's output as a response. Sinatra is particularly useful for getting projects up and running quickly where we don't have a need for the extra pieces RoR provides.

For those who are familiar with more Rails, you probably know that it requires a separate routes file to define how an application should be responding to requests. These are then piped into the relevant models and controllers as needed.

Sinatra takes a more straight-forward approach, providing us with the most simple path to handling routing. By declaring get,post, put or delete actions, we can inform Sinatra to add a new route, which we can then have respond to requests.

The framework is particularly useful for writing APIs, widgets and small-scale applications that can power the backend of a client-heavy application. As mentioned, we will be using it to power our API.

Getting Started With Sinatra

Let's review how to write and run a very basic Sinatra application. As most programming languages and frameworks typically start with some variation of "Hello World", we'll start with a similar example.

Note: Before beginning this section, I recommend installing Sinatra on your system. A guide to doing this can be found in the prerequisites section lower down in the article.

Routes

As mentioned, Sinatra allows us to define new routes using HTTP actions. Semantically, a route follows quite a simple structure:

[a HTTP action] [the desired route] do
   # some behaviour
end

A tiny route that outputs a "Hello World"-like message when we attempt to "get" the root could thus be written as follows:

require 'sinatra'
get '/' do
   "Hello World! Is it me you're looking for?"
end

To run this snippet, we can can simply save it to a local '.rb' file and execute it as follows:

ruby -rubygems example.rb

If we now navigated to http://localhost:4567 in our browser we could now see the application running successfully.

The HTTP verbs we commonly work with when writing RESTful web services are: get, post, delete and put. As we now know, all Sinatra routes are basically HTTP actions (`get etc.) that are paired with a URL-matching pattern. We associate a pair of an action and route with code we would like sent back to the browser (executed)if the route is reached. Sinatra doesn't enforce much in the way of architectural structure, instead relying on simplicity to supporting writing powerful APIs.

Here's an example of a skeleton service we could put together supporting four common HTTP actions:

get '/items' do
  # list all items available
end

get '/item/:id' do
  # get a single item
end

post '/item' do
  # create a new item
end

put '/item/:id' do
  # update an existing item
end

delete '/item/:id' do
  # delete an item
end

Sinatra's routing is both easy for beginners to get started with but is also flexible enough for those wishing to define more complex routes. As you probably noticed in the above example, routes can include named parameters (e.g /item/:id). We can actually access the content of these routes using the params hash as follows:

get '/item/:id' do
  # this matches "GET /item/10" and "GET /item/11"
  # params[:id] is "10" or "11"
  "You reached #{params[:id]}"
end

Sinatra also supports route matching via splats, wildcards and regular expressions. For more information on this I recommend reading the official docs. Let's now take a look at handlers.

Sinatra includes convenient handler methods for tasks such as redirection, halting and passing.

Redirection

A simple route supporting redirection which returns a 302 response can be written as follows:

get '/items' do
      redirect '/items/welcome'
end

And if we wish to pass additional parameters such as arguments we can do so like this: redirect 'http://site.com/', 'Oops! I think we have a problem!'

Halting

To immediately stop a request (halting) we can use 'halt'. Heres an example of halting a request where we specify the message body:

halt "who goes there!?"

Passing

'Passing' is the concept of deferring processing of a block to the next matching route. We do this using pass. In the following example if a parameter isnt the username we expect (rick-astley) we simply pass it on:

get '/members/:username' do
 pass unless params[:username] == 'rick-astley'
 'Never gonna give you up, never gonna let you down'
end

get '/member/*' do
 'Welcome!'
end

There are also handler methods that can assist with sessions (specifically, cookie-based session handling). To use Sinatra's session handling, first enable it in your application with:

enable :sessions

You can then use the session handling capabilities as follows:

get '/items' do
  session['visitCounter'] ||= 0;
  session['visitCounter'] += 1;
  "This page has been accessed #{session['visitCounter']} times"
end

Note: By default enable:sessions will store all data in cookies. If this is not desired, you can not call this and instead use some Rack middleware instead. For more on this see here.

This only touches the surface of what can be done using routes and handlers, but is sufficient for us to write the Sinatra-powered API service we require in the practical section of this post.

Templating And HAML

Let's now discuss templating.Out of the box, we can begin using templates in our Sinatra applications with ERB. ERB is included with Ruby and allows Ruby code to be added to any plain text document for the purpose of generating information or flow control. In the following example using an ERB template, note that views are by default located in the views directory of our application.

get '/items' do
  erb :default
  # renders views/default.erb
end

A useful Sinatra convention worth noting is how layouts are handled. Layouts automatically search for a views/layout template which is rendered before any other views are loaded. With ERB, our views/layout.erb file could look as follows:

<html>
  <head></head>
  <body>
    <%= data %>
  </body>
</html>

Haml is a popular alternative to ERB which offers an abstract syntax for writing application templates. It has been said to be:

  • Straight-forward to learn
  • Very easy to read and use for visually expressing a hierarchy of DOM elements
  • Popular with web designers as it builds on top of CSS syntax
  • Well documented with a large community backing it
  • Almost as fast as ERB

For the purpose of comparison, below we can see an ERB template compared to it's Haml equivalent.

ERB

<div class="todo" id="content">
  <h2 class="entry_title"><%= h @todo.title %></h2>
  <div class="entry_link">
  <%= link_to('link', @todo.link) %></div>
</div>

Haml

.todo#content
  %h2.entry_title= @todo.title
  .entry_link= link_to('link', @todo.link)

One of the first things we notice is that the Haml snippet looks significantly more like CSS than it does traditional markup. It's much easier to read and we no longer need to be concerned with divs, spans, closing tags or other semantic rules that usually mean more keystrokes. The approach taken to making whitespace a part of the syntax also means it can be much easier to compare changes between multiple documents (especially if you're doing a diff).

In the list of Haml features, we briefly mentioned web designers. As developers, we regularly need to communicate and work with designers, but we always have to remember that at the end of the day, they are not programmers. They're usually more concerned with the look and the feel of an application, but if we want them to write mark-up as a part of the templates or skins they create, Haml is a simpler option that has worked well for teams at a number of companies.

%h1 This is some h1 text
%h2 This is some h2 text.

%p Now we have a line containing a single instance
%p variable: @content
%p= @content

%p Embedding Ruby code in the middle of a line can
%p be done using ==.
%p== Here is an example: #{@foobar}

%p We can also add attributes using {}
%p{:style => "color:green"} We just made this paragraph
%p green!

%p You'll want to apply classes and ids to your DOM, too.
%p.foo This has the foo class
%p.bar This has the bar class
%p#foobar This has the foobar id
%p.foo#foobar Or you can combine them!

%p Nesting can be done like this
%p
  Or even like this

Note: Haml is whitespace sensitive and will not correctly work if it isn't indented by an even number of spaces. This is due to whitespace being used for nesting in place of the classic HTML markup approach of closing tags.

MongoDB Ruby Driver

Getting started

Once the MongoDB Ruby driver is installed, we can begin to use it to connect to a Mongo database. To create a connection using localhost, we simply specify the driver as a dependency. Assuming we're using the default port we can then connect as follows:

require 'mongo'

where 'learning-mongo' is the name of our database:

db = Connection.new.db('learning-mongo');

We probably also want to place some data into 'learning-mongo'. It could be as simple as a note, so why don't we go ahead and begin a notes collection?:

notes = db.collection('notes')

Something interesting worth noting is that at this point, we haven't actually created the database nor the collection we're referencing above.

Neither of these items exist in Mongo (just yet) but as we're working with a new database but they will once we insert some real data.

A new note could be defined using key/value pairs as follows and then inserted into 'learning-mongo' using collection.insert():

our_note = {
  :text => 'Remember the milk',
  :remindInterval => 'weekly'}
note_id = notes.insert(our_note)

What is returned from inserting a note into the notes collection is an ObjectId reference for the note from Mongo. This is useful as we can re-use it to locate the same document in our database.

note = notes.find( :id => note_id ).first

 

This can also be used in conjunction with Mongo's collection.update() method and query operators (i.e $set) to replace fields in an existing document.

We might update an entire document as follows:

 

note = notes.find( :id => note_id ).first
note[:text] = 'Remember the bread'
notes.update({ :_id => note_id }, note)

 

or using $set, update an existing document without overwriting the entire object as like this:

notes.update({ :_id => note_id }, '$set' => { :text = > 'Remember the bread' })

Useful to know: Almost each MongoDB document has an _id field as it's first attribute. This can normally be of any type, however a special BSON datatype is provided for object ids. It's a 12-byte binary value that has a high probability of being unique when allocated.

Note: Whilst we opted for the MongoDB Ruby Driver for this stack, you may also be interested in DataMapper - a solution which allows us to use the same API to talk to a number of different datastores. This works well for both relational and non-relational databases and more information is available on the official project page. Sinatra: The Book also contains a brief tutorial on DataMapper for anyone interested in exploring it further.

Practical

We're going to use Sinatra in a similar manner to how we used Express in the last post. It will power a RESTful API supporting CRUD operations. Together with a MongoDB data store, this will allow us to easily persist data (todo items) whilst ensuring they are stored in a database. If you've read the previous post or have gone through any of the Todo examples covered so far, you will find this surprisingly straight-forward.

Remember that the default Todo example included with Backbone.js already persists data, although it does this via a localStorage adapter. Luckily there aren't a great deal of changes needed to switch over to using our Sinatra-based API. Let's briefly review the code that will be powering the CRUD operations for this sections practical, as we go course won't be starting off with a near-complete base for most of our real world applications.

Installing The Prerequisites

Ruby

If using OSX or Linux, Ruby may be one of a number of open-source packages that come pre-installed and you can skip over to the next paragraph. In case you would like to check if check if you have Ruby installed, open up the terminal prompt and type:

$ ruby -v

The output of this will either be the version of Ruby installed or an error complaining that Ruby wasn't found.

Should you need to install Ruby manually (e.g for an operating system such as Windows), you can do so by downloading the latest version from http://www.ruby-lang.org/en/downloads/. Alternatively, (RVM)[http://beginrescueend.com/rvm/install/] (Ruby Version Manager) is a command-line tool that allows you to easily install and manage multiple ruby environments with ease.

Ruby Gems

Next, we will need to install Ruby Gems. Gems are a standard way to package programs or libraries written in Ruby and with Ruby Gems it's possible to install additional dependencies for Ruby applications very easily.

On OSX, Linux or Windows go to http://rubyforge.org/projects/rubygems and download the latest version of Ruby Gems. Once downloaded, open up a terminal, navigate to the folder where this resides and enter:

$> tar xzvf rubygems.tgz
$> cd rubygems
$> sudo ruby setup.rb

There will likely be a version number included in your download and you should make sure to include this when tying the above. Finally, a symlink (symbolic link) to tie everything togther should be fun as follows:

$ sudo ln -s /usr/bin/gem1.8.17 /usr/bin/gem

To check that Ruby Gems has been correctly installed, type the following into your terminal:

$ gem -v

Sinatra

With Ruby Gems setup, we can now easily install Sinatra. For Linux or OSX type this in your terminal:

$ sudo gem install sinatra

and if you're on Windows, enter the following at a command prompt:

c:\ > gem install sinatra

Haml

As with other DSLs and frameworks, Sinatra supports a wide range of different templating engines. ERB is the one most often recommended by the Sinatra camp, however as a part of this post, we're going to explore the use of Haml to define our application templates.

Haml stands for HTML Abstractional Markup Language and is a lightweight markup language abstraction that can be used to describe HTML without the need to use traditional markup language semantics (such as opening and closing tags).

Installing Haml can be done in just a line using Ruby Gems as follows:

$ gem install haml

MongoDB

If you haven't already downloaded and installed MongoDB from an earlier post, please do so now. With Ruby Gems, Mongo can be installed in just one line:

$ gem install mongodb

We now require two further steps to get everything up and running.

1.Data directories

MongoDB stores data in the bin/data/db folder but won't actually create this directory for you. Navigate to where you've downloaded and extracted Mongo and run the following from terminal:

sudo mkdir -p /data/db/
sudo chown `id -u` /data/db
2.Running and connecting to your server

Once this is done, open up two terminal windows.

In the first, cd to your MongoDB bin directory or type in the complete path to it. You'll need to start mongod.

$ ./bin/mongod

Finally, in the second terminal, start the mongo shell which will connect up to localhost by default.

$ ./bin/mongo

MongoDB Ruby Driver

As we'll be using the MongoDB Ruby Driver, we'll also require the following gems:

The gem for the driver itself:

$ gem install mongo

and the driver's other prerequisite, bson:

$ gem install bson_ext

This is basically a collection of extensions used to increase serialization speed.

That's it for our prerequisites!.

Tutorial

To get started, let's get a local copy of the practical application working on our system.

Application Files

Clone this repository and navigate to /practicals/stacks/option3. Now run the following lines at the terminal:

ruby app.rb

Finally, navigate to http://localhost:4567/todo to see the application running successfully.

Note: The Haml layout files for Option 3 can be found in the /views folder.

The directory structure for our practical application is as follows:

--public
----css
----img
----js
-----script.js
----test
--views
app.rb

The public directory contains the scripts and stylesheets for our application and uses HTML5 Boilerplate as a base. You can find the Models, Views and Collections for this section within public/js/scripts.js (however, this can of course be expanded into sub-directories for each component if desired).

scripts.js contains the following Backbone component definitions:

--Models
----Todo

--Collections
----TodoList

--Views
---TodoView
---AppView

app.rb is the small Sinatra application that powers our backend API.

Lastly, the views directory hosts the Haml source files for our application's index and templates, both of which are compiled to standard HTML markup at runtime.

These can be viewed along with other note-worthy snippets of code from the application below.

Backbone

Views

In our main application view (AppView), we want to load any previously stored Todo items in our Mongo database when the view initializes. This is done below with the line Todos.fetch() in the initialize() method where we also bind to the relevant events on the Todos collection for when items are added or changed.

// Our overall AppView is the top-level piece of UI.
var AppView = Backbone.View.extend({

 

// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $("#todoapp"),

// Our template for the line of statistics at the bottom of the app.
statsTemplate: _.template($('#stats-template').html()),

// Delegated events for creating new items, and clearing completed ones.
events: {
  "keypress #new-todo":  "createOnEnter",
  "keyup #new-todo":     "showTooltip",
  "click .todo-clear a": "clearCompleted"
},

// At initialization
initialize: function() {
  this.input    = this.$("#new-todo");

  Todos.on('add',   this.addOne, this);
  Todos.on('reset', this.addAll, this);
  Todos.on('all',   this.render, this);

  Todos.fetch();
},

// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function() {
  this.$('#todo-stats').html(this.statsTemplate({
    total:      Todos.length,
    done:

...

Collections

In the TodoList collection below, we've set the url property to point to /api/todos to reference the collection's location on the server. When we attempt to access this from our Sinatra-backed API, it should return a list of all the Todo items that have been previously stored in Mongo.

For the sake of thoroughness, our API will also support returning the data for a specific Todo item via /api/todos/itemID. We'll take a look at this again when writing the Ruby code powering our backend.

// Todo Collection

  var TodoList = Backbone.Collection.extend({

  // Reference to this collection's model.
  model: Todo,

  // Save all of the todo items under the `"todos"` namespace.
  // localStorage: new Store("todos"),
  url: '/api/todos',

  // Filter down the list of all todo items that are finished.
  done: function() {
    return this.filter(function(todo){ return todo.get('done'); });
  },

  // Filter down the list to only todo items that are still not finished.
  remaining: function() {
    return this.without.apply(this, this.done());
  },

  // We keep the Todos in sequential order, despite being saved by unordered
  // GUID in the database. This generates the next order number for new items.
  nextOrder: function() {
    if (!this.length) return 1;
    return this.last().get('order') + 1;
  },

  // Todos are sorted by their original insertion order.
  comparator: function(todo) {
    return todo.get('order');
  }

});

Model

The model for our Todo application remains largely unchanged from the versions previously covered in this book. It is however worth noting that calling the function model.url() within the below would return the relative URL where a specific Todo item could be located on the server.

// Our basic Todo model has text, order, and done attributes.
  var Todo = Backbone.Model.extend({
    idAttribute: "_id",

  // Default attributes for a todo item.
  defaults: function() {
    return {
      done:  false,
      order: Todos.nextOrder()
    };
  },

  // Toggle the `done` state of this todo item.
  toggle: function() {
    this.save({done: !this.get("done")});
  }

});

Ruby/Sinatra

Now that we've defined our main models, views and collections let's get the CRUD operations required by our Backbone application supported in our Sinatra API.

We want to make sure that for any operations changing underlying data (create, update, delete) that our Mongo data store correctly reflects these.

app.rb

For app.rb, we first define the dependencies required by our application. These include Sinatra, Ruby Gems, the MongoDB Ruby driver and the JSON gem.

require 'rubygems'
require 'sinatra'
require 'mongo'
require 'json'

Next, we create a new connection to Mongo, specifying any custom configuration desired. If running a multi-threaded application, setting the 'pool_size' allows us to specify a maximum pool size and 'timeout' a maximum timeout for waiting for old connections to be released to the pool.

DB = Mongo::Connection.new.db("mydb", :pool_size => 5,
  :timeout => 5)

Finally we define the routes to be supported by our API. Note that in the first two blocks - one for our application root (/ and the other for our todo items route /todo - we're using Haml for template rendering.

class TodoApp < Sinatra::Base

get '/' do
  haml :index, :attr_wrapper => '"', :locals =>
  {:title => 'hello'}
end

get '/todo' do
  haml :todo, :attr_wrapper => '"', :locals =>
  {:title => 'Our Sinatra Todo app'}
end

haml :index instructs Sinatra to use the views/index.haml for the application index, whilst `attr_wrapper is simply defining the values to be used for any local variables defined inside the template. This similarly applies Todo items with the template `views/todo.haml'.

The rest of our routes make use of the params hash and a number of useful helper methods included with the MongoDB Ruby driver. For more details on these, please read the comments I've made inline below:

get '/api/:thing' do
  # query a collection :thing, convert the output to an array, map the id
  # to a string representation of the object's _id and finally output to JSON
  DB.collection(params[:thing]).find.toa.map{|t| frombsonid(t)}.to_json
end

get '/api/:thing/:id' do
  # get the first document with the id :id in the collection :thing as a single document (rather
  # than a Cursor, the standard output) using findone(). Our bson utilities assist with
  # ID conversion and the final output returned is also JSON
  frombsonid(DB.collection(params[:thing]).findone(tobsonid(params[:id]))).to_json
end

post '/api/:thing' do
  # parse the post body of the content being posted, convert to a string, insert into
  # the collection #thing and return the ObjectId as a string for reference
  oid = DB.collection(params[:thing]).insert(JSON.parse(request.body.read.tos))
  "{
quot;id
quot;: 
quot;#{oid.to_s}
quot;}"
end

delete '/api/:thing/:id' do
  # remove the item with id :id from the collection :thing, based on the bson
  # representation of the object id
  DB.collection(params[:thing]).remove('id' => tobson_id(params[:id]))
end

put '/api/:thing/:id' do
  # collection.update() when used with $set (as covered earlier) allows us to set single values
  # in this case, the put request body is converted to a string, rejecting keys with the name 'id' for security purposes
  DB.collection(params[:thing]).update({'id' => tobsonid(params[:id])}, {'$set' => JSON.parse(request.body.read.tos).reject{|k,v| k == 'id'}})
end


# utilities for generating/converting MongoDB ObjectIds

def tobsonid(id) BSON::ObjectId.fromstring(id) end
def frombsonid(obj) obj.merge({'id' => obj['id'].tos}) end

end

That's it. The above is extremely lean for an entire API, but does allow us to read and write data to support the functionality required by our client-side application.

For more on what MongoDB and the MongoDB Ruby driver are capable of, please do feel free to read their documentation for more information.

If you're a developer wishing to take this example further, why not try to add some additional capabilities to the service:

  • Validation: improved validation of data in the API. What more could be done to ensure data sanitization?
  • Search: search or filter down Todo items based on a set of keywords or within a certain date range
  • Pagination: only return the Nth number of Todo items or items from a start and end-point

Haml/Templates

Finally, we move on to the Haml files that define our application index (layout.haml) and the template for a specific Todo item (todo.haml). Both of these are largely self-explanatory, but it's useful to see the differences between the Jade approach we reviewed in the last post vs. using Haml for this implementation.

Note: In our Haml snippets below, the forward slash character is used to indicate a comment. When this character is placed at the beginning of a line, it wraps all of the text after it into a HTML comment. e.g

/ These are templates

compiles to:

<!-- These are templates -->

index.haml

%head
  %meta{'charset' => 'utf-8'}/
  %title=title
  %meta{'name' => 'description', 'content' => ''}/
  %meta{'name' => 'author', 'content' => ''}/
  %meta{'name' => 'viewport', 'content' => 'width=device-width,initial-scale=1'}/

 

/ CSS concatenated and minified via ant build script
  %link{'rel' => 'stylesheet', 'href' => 'css/style.css'}/
  / end CSS

%script{'src' => 'js/libs/modernizr.min.js'}
%body
  %div#container
    %header
    %div#main
      = yield
    %footer
  /! end of #container

%script{'src' => 'http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js'}

/ scripts concatenated and minified via ant build script
  %script{'src' => 'js/mylibs/underscore.js'}
  %script{'src' => 'js/mylibs/backbone.js'}
  %script{'defer' => true, 'src' => 'js/plugins.js'}
  %script{'defer' => true, 'src' => 'js/script.js'}
  / end scripts

todo.haml

%div#todoapp
  %div.title
    %h1
      Todos
      %div.content
        %div#create-todo
          %input#new-todo{"placeholder" => "What needs to be done?", "type" => "text"}/
          %span.ui-tooltip-top{"style" => "display:none;"} Press Enter to save this task
        %div#todos
          %ul#todo-list
        %div#todo-stats

/ Templates

%script#item-template{"type" => "text/template"}
  <div class="todo <%= done ? 'done' : '' %>">
  %div.display
    <input class="check" type="checkbox" <%= done ? 'checked="checked"' : '' %> />
    %div.todo-text
    %span#todo-destroy
  %div.edit
    %input.todo-input{"type" => "text", "value" =>""}/
  </div>

%script#stats-template{"type" => "text/template"}
  <% if (total) { %>
  %span.todo-count
    %span.number <%= remaining %>
    %span.word <%= remaining == 1 ? 'item' : 'items' %>
    left.
  <% } %>
  <% if (done) { %>
  %span.todo-clear
    %a{"href" => "#"}
      Clear
      %span.number-done <%= done %>
      completed
      %span.word-done <%= done == 1 ? 'item' : 'items' %>
  <% } %>

Conclusions

In this post, we looked at creating a Backbone application backed by an API powered by Ruby, Sinatra, Haml, MongoDB and the MongoDB driver. I personally found developing APIs with Sinatra a relatively painless experience and one which I felt was on-par with the effort required for the Node/Express implementation of the same application.

This section is by no means the most comprehensive guide on building complex apps using all of the items in this particular stack. I do however hope it was an introduction sufficient enough to help you decide on what stack to try out for your next project.

In the next post, we may look at developing real-time applications using Backbone and a Web Socket (socket.io) based framework such as SocketStream.

With thanks to @akahn, @KushalP, @paulbjensen and more for their feedback and technical reviews.