Building Single Page Web Applications with Backbone.js, JaxRS, MongoDB, and OpenShift

Backbone.js is a mature, popular, and lightweight javascript library which aims to bring structure to your client-side code. The goal of backbone.js is to bring MVC, a software architecture pattern, to client side development.Actually, backbone.js is an MV framework where view is also responsible for controller logic as well. In this blog, we will build a simple social bookmarking application.

In brief, MVC is a programming design pattern for separating the different concerns of your application. The M corresponds to Model and is used to define domain object. The V corresponds to View and is responsible for display logic. Finally, the C corresponds to Controller and is responsible for user interactions and interactions between Views and Models.

You can find source code of the application here https://github.com/shekhargulati/getbookmarks. Backbone.js is used by Linkedin, Foursquare, Wunderkit, Groupon, etc to build complex applications. You can view the full list here.

Prerequisite

Before we can start building the application, you'll have a few setup tasks to do:

  1. Sign up for an OpenShift Account. It is completely free and Red Hat gives every user three free Gears on which to run your applications. At the time of this writing, the combined resources allocated for each user is 1.5 GB of memory and 3 GB of disk space.

  2. Install the rhc client tool on your machine. The rhc is a ruby gem so you need to have ruby 1.8.7 or above on your machine. To install rhc, just type

sudo gem install rhc

If you already have one, make sure it is the latest one. To update your rhc, execute the command shown below.

sudo gem update rhc

For additional assistance setting up the rhc command-line tool, see the following page: https://openshift.redhat.com/community/developers/rhc-client-tools-install

  1. Setup your OpenShift account using rhc setup command. This command will help you create a namespace and upload your ssh keys to OpenShift server.

Let's Get Started

After you have done all the prerequisites, its time to start building the application.

Step 1 : Create JBossEAP MongoDB application

We will start by creating a new application with a JBoss EAP and a MongoDB cartridge. The application that we will be creating is a social bookmarking site which can be used to bookmark the urls.

rhc app create getbookmarks jbosseap mongodb-2.2

This will create an application container for us, called a gear, and setup all of the required SELinux policies and cgroup configuration. OpenShift will also setup a private git repository for you and clone the repository to your local system. Finally, OpenShift will propagate the DNS to outside world. The application will be accessible at http://getbookmarks-domain-name.rhcloud.com/. Replace domain-name with your own unique OpenShift domain name (also sometimes called a namespace).

Step 2 : Delete template code

Next we will delete the template code created by OpenShift.

cd getbookmarks
git rm -rf src/main/webapp/*.jsp src/main/webapp/index.html
git commit -am "deleted template files"

Step 3 : Set up backbone.js and twitter bootstrap

The application will be built using Backbone.js and Twitter Bootstrap so we will need to add them to our application. I have created a template project which includes all the required css, js, and bare-bone index.html file. To get the source code for the template application, execute the commands shown below.

git remote add template -m master git://github.com/shekhargulati/openshift-backbonejs-blog-template.git
git pull -s recursive -X theirs template master

To confirm that template project works fine, open the local index.html in your favorite browser and you see page as shown below.

Step 4 : Show submit story form

The application needs a form to submit stories. The story will have a title, url, and tags(a comma-separated list of strings). We will start by making changes to app.js file. The app.js houses all the backbone.js related code. The code shown below creates an instance of backbone router providing it the root DOM as main div. Then when we hit the base url, router calls the index function which render then index view. The IndexView in its render function creates an instance of FormView and render the form. We also have a template function which compiles the mustache template (Mustache is a templating framework for JavaScript pages).

// app.js
(function($){
 
        var Bookmarks = {};
        window.Bookmarks = Bookmarks;
 
        var template = function(name) {
            return Mustache.compile($('#'+name+'-template').html());
        };
 
        Bookmarks.FormView = Backbone.View.extend({
            template : template('form'),
            render : function(){
                this.$el.html(this.template);
                return this;
            }
 
        });
        Bookmarks.IndexView = Backbone.View.extend({
 
            render : function(){
                var form = new Bookmarks.FormView();
                this.$el.append(form.render().el);
                return this;
            }
 
        });
 
 
        Bookmarks.Router = Backbone.Router.extend({
            initialize : function(options){
                this.el = options.el;
            },
            routes : {
                "" : "index",
            },
            index : function(){
                var indexView = new Bookmarks.IndexView();          
                this.el.empty();
                this.el.append(indexView.render().el);
            }
        });
 
        var router = new Bookmarks.Router({el : $('#main')});
        Backbone.history.start();
})(jQuery);

Next update the index.html to define form template as shown below.

<script type="text/x-mustache-template" id="form-template">
      <form class="form-horizontal" id="bookmarkForm">
          <div class="control-group">
            <label class="control-label" for="title">Title</label>
            <div class="controls">
              <input type="text" class="input-xlarge" id="title" placeholder="What is the title of story?" data-error-style="inline" required>
            </div>
          </div>
          <div class="control-group">
            <label class="control-label" for="author">Url</label>
            <div class="controls">
              <input type="text" class="input-xlarge" id="url" placeholder="What is the url of story" data-error-style="inline" required>
            </div>
          </div>
          <div class="control-group">
            <label class="control-label" for="tags">Tags</label>
            <div class="controls">
              <input type="text" class="input-xlarge" id="tags" placeholder="Comma seperated list of Tags" data-error-style="inline" required>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <button type="submit" class="btn btn-success">Submit Story</button>
            </div>
          </div>
      </form>
    </script>
 

Now if you refresh the index.html, you will see the form as shown below.

Step 5 : Persist data to local storage

Backbone.js provide support for html5 local storage through an extension. The backbone-localstorage.js enables local storage support. We have already added it in index.html. In the code shown below, we created a backbone collection called Stories and made it use local storage. In the IndexView view we added an event listener on stories collection such that if any thing happen on stories collection we will re-render the view. In FormView, we added an event for submit action, where in on submit we will invoke submitStory method which will first disable default action of refreshing the page and then add a new entry to collection. This will get persisted to local storage.

Bookmarks.Stories = Backbone.Collection.extend({
            localStorage : new Store('bookmarks')
        });
 
        Bookmarks.FormView = Backbone.View.extend({
            template : template('form'),
            events : {
                'submit' :'submitStory'
            },
 
            render : function(){
                this.$el.html(this.template);
                return this;
            },
 
            submitStory : function(event){
                event.preventDefault();
                this.collection.create({
                    title : this.$('#title').val(),
                    url : this.$('#url').val(),
                    tags : this.$('#tags').val()
                });
            }
 
        });
 
        Bookmarks.IndexView = Backbone.View.extend({
            initialize : function(){
                this.stories = new Bookmarks.Stories();
                this.stories.on('all',this.render,this);
                this.stories.fetch();
            },
 
            render : function(){
                this.$el.html('');
                var form = new Bookmarks.FormView({collection : this.stories});
                this.$el.append(form.render().el);
                return this;
            }
 
        });

Now if you refresh the index.html and submit a story, you will see a corresponding entry in browser local storage. In chrome, you can go to developer tools, then in Resources tab, under Local Storage you can see the data.

Step 6 : View and delete stories

Now that we have added the capability to store stories, the next logical step is to start viewing and deleting stories. To achieve that we will create StoryView to render the story. In render function of IndexView, after showing the form we will iterate over all the stories in the collection, and then render each story. The code is shown below. For delete functionality, I have added an event listener to listen for 'click button' and then call the removeStory function which will destroy the model.

Bookmarks.StoryView = Backbone.View.extend({
            template : template('story'),
            events : {
                'click button' : 'removeStory'
            },
            render : function(){
                this.$el.html(this.template(this));
                return this;
            },
            removeStory : function(){
                this.model.destroy();
                return false;
            },
 
            title : function(){
                return this.model.get('title');
            },
            url : function(){
                return this.model.get('url');
            },
            tags : function(){
                return this.model.get('tags');
            }
 
        });
 
        Bookmarks.IndexView = Backbone.View.extend({
            initialize : function(){
                this.stories = new Bookmarks.Stories();
                this.stories.on('all',this.render,this);
                this.stories.fetch();
            },
 
            render : function(){
                this.$el.html('');
                var form = new Bookmarks.FormView({collection : this.stories});
                this.$el.append(form.render().el);
                this.stories.each(this.renderStory,this);
                return this;
            },
 
            renderStory : function(story){
                var storyView = new Bookmarks.StoryView({model : story});
                this.$el.append(storyView.render().el);
            }
 
 
        });

Also update the index.html to have story template as shown below.

<script type="text/x-mustache-template" id="story-template">
 
 
    <div class="story">
      <h3> {{title}} </h3>
      <p> <a href={{url}} target="_blank">{{url}}</a> </p>
      <p> {{tags}} </p>
     <button type="submit" class="btn btn-danger">Remove Story</button>
    </div>
    </script>

Now if you refresh the index.html, you will see stories as shown below.

Step 7 : Write RESTful backend

Now that we have written front end of the application, we will move to writing RESTful backend for our application using JAX-RS. We will start with adding MongoDB java driver dependency in pom.xml

     <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongo-java-driver</artifactId>
            <version>2.9.1</version>
        </dependency>

Then we will activate JAX-RS by creating a class which extends javax.ws.rs.ApplicationPath. You need to specify the base url under which your web service will be available. This is done by annotating the class with ApplicatioPath annotation. In the code shown below, I have used "/rest" as base url.

package com.bookmarks.rest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
 
@ApplicationPath("/rest")
public class JaxRsActivator extends Application {
 
}

Next we will create an application scoped named bean to create the MongoDB database connection. The connection class works on both the local system as well as on OpenShift.

package com.bookmarks.mongo;
 
import java.net.UnknownHostException;
 
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
 
import com.mongodb.DB;
import com.mongodb.Mongo;
 
@Named
@ApplicationScoped
public class DBConnection {
 
    private DB mongoDB;
 
    @PostConstruct
    public void afterCreate() {
 
        System.out.println("just see if we can say anything");
 
        String host = System.getenv("OPENSHIFT_MONGODB_DB_HOST");
 
        if (host == null || "".equals(host)) {
            Mongo mongo = null;
            try {
                mongo = new Mongo("localhost", 27017);
                mongoDB = mongo.getDB("bookmarks");
            } catch (UnknownHostException e) {
                System.out.println("Could not connect to Mongo on Localhost: "
                        + e.getMessage());
            }
 
        } else {
 
            // on openshift
            String mongoport = System.getenv("OPENSHIFT_MONGODB_DB_PORT");
            String user = System.getenv("OPENSHIFT_MONGODB_DB_USERNAME");
            String password = System.getenv("OPENSHIFT_MONGODB_DB_PASSWORD");
            String db = System.getenv("OPENSHIFT_APP_NAME");
            int port = Integer.decode(mongoport);
 
            Mongo mongo = null;
            try {
                mongo = new Mongo(host, port);
            } catch (UnknownHostException e) {
                System.out.println("Couldn't connect to Mongo: "
                        + e.getMessage() + " :: " + e.getClass());
            }
 
            mongoDB = mongo.getDB(db);
 
            if (mongoDB.authenticate(user, password.toCharArray()) == false) {
                System.out.println("Failed to authenticate DB ");
            }
        }
 
    }
 
    public DB getDB() {
        return mongoDB;
    }
 
}

Finally we will write our REST service class which will expose CRUD methods.

package com.bookmarks.rest;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import org.bson.types.ObjectId;
 
import com.bookmarks.mongo.DBConnection;
import com.bookmarks.mongo.Story;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
 
@Path("/bookmarks")
public class BookmarkRestService {
 
    @Inject
    DBConnection dbConnection;
 
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response submitStory(Story story){
        DB db = dbConnection.getDB();
        DBCollection bookmarks = db.getCollection("bookmarks");
        BasicDBObject doc = new BasicDBObject("title",story.getTitle()).append("url", story.getUrl()).append("tags", story.getTags());
        bookmarks.insert(doc);
        return Response.created(null).build();
    }
 
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Story> allStories(){
        DB db = dbConnection.getDB();
        DBCollection bookmarks = db.getCollection("bookmarks");
        DBCursor cursor = bookmarks.find();
        List<Story> stories = new ArrayList<Story>();
 
        while(cursor.hasNext()){
 
            DBObject dbObject = cursor.next();
            stories.add(new Story(dbObject));
        }
        return stories;
    }
 
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Story lookupTodoById(@PathParam("id") String id) {
        DB db = dbConnection.getDB();
        DBCollection bookmarks = db.getCollection("bookmarks");
        DBObject doc = bookmarks.findOne(new BasicDBObject("_id",new ObjectId(id)));
        return new Story(doc);
    }
 
    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") String id){
        DB db = dbConnection.getDB();
        DBCollection bookmarks = db.getCollection("bookmarks");
        bookmarks.remove(new BasicDBObject("_id",new ObjectId(id)));
        return Response.noContent().build();
    }
 
}

Step 8 : Update client to use RESTful backend

The last step before we push the application to cloud is to update the client to use RESTful backend instead of local storage. To do that first remove backbone-localstorage.js dependency from index.html. Then in app.js, update the Stories collection to use RESTful backend as shown below.

Bookmarks.Stories = Backbone.Collection.extend({
    url : 'rest/bookmarks'
});

Step 9 : Push the code

Now you can push the code to OpenShift and see your application running in cloud.

git add .
git commit -am "bookmarks app"
git push

The application will be running at https://bookmarks-domain-name.rhcloud.com/. Please replace domain-name with your own namespace.

Conclusion

In this blog, we looked at how you can build single page web application using backbone.js and consume REST services from backbone.js. In the next blog of this series, we will extend this application to add edit and search functionality.

What's Next?

"Then we will activate JAX-RS by creating a class which extends javax.ws.rs.ApplicationPath" package com.bookmarks.rest; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("/rest") public class JaxRsActivator extends Application { } What is the name of the file (class) that you wrote in this article? and where I should place the file?

And please tell me the name of the file (class) and the other where I have to put the file, for example .. package com.bookmarks.mongo; import java.net.UnknownHostException; import javax.annotation.PostConstruct; ... Thank you

PS: I don't know java yet :)

author provided a git respository, please find the complete source, if you want you can work with eclipse ide which will help you with imports (and push your app with openshift plugin), alternatively there is always a api documentation to refere to