Apr 292012
 

Backbone in Baby Steps, part 2.5

In the first two part of this tutorial series we looked at the basic structure of a Backbone application and how to add and remove models. In the third part we will look at how to synchronize the models with the back end, but in order to do that we need to make a small detour and set up a server with a REST api. That is what we are going to do in this part.Since this is a JavaScript tutorial I will use JavaScript to create the server using node.js. If you are more comfortable in setting up a REST server in another language, this is the API you need to conform to:

url HTTP Method Operation
/api/books GET Get an array of all books
/api/books/:id GET Get the book with id of :id
/api/books POST Add a new book and return the book with an id attribute added
/api/books/:id PUT Update the book with id of :id
/api/books/:id DELETE Delete the book with id of :id

The outline for this tutorial looks like this:

  1. Install node.js, npm and MongoDB
  2. Install node modules
  3. Create directory structure
  4. Create a simple web server
  5. Connect to the database
  6. Create the REST API

Installing node, npm and MongoDB

Download and install node.js from nodejs.org. The node package manager (npm) will be installed as well.

Download and install MongoDB from mongodb.org. When installing MongoDB you get instructions on copying a config file in order for MongoDB to run. On OSX it can look something like this:

sudo cp /usr/local/Cellar/mongodb/2.0.4-x86_64/mongod.conf /usr/local/etc/mongod.conf

Once it is installed we can go ahead and start it. On my machine I did this:

mongod run --config /usr/local/Cellar/mongodb/2.0.4-x86_64/mongod.conf

Installing node modules

Start by creating a new folder for this project. Using a terminal, go to your project folder and run the following commands:

npm install express@2.5.9
npm install path
npm install mongoose

Note the @2.5.9 on express. If you don’t type that you will get express version 3  which is at the time of writing in the unstable branch and has a different API.

Create directory structure

In your project folder root, create a file server.js – this is where our server code will go. Then create a folder public. Anything within the public folder will be served by the express web server as static content to the client. Now go ahead and copy everything from Part 2 into the public folder. When you are done your folder structure should look something like this:

node_modules/
  .bin/
  express/
  mongoose/
  path/
public/
  css/
  img/
  js/
  index.html
server.js
package.json

Create a simple web server

Open server.js and enter the following:

// Module dependencies.
var application_root = __dirname,
    express = require("express"), //Web framework
    path = require("path"), //Utilities for dealing with file paths
    mongoose = require('mongoose'); //MongoDB integration

//Create server
var app = express.createServer();

// Configure server
app.configure(function () {
    app.use(express.bodyParser()); //parses request body and populates req.body
    app.use(express.methodOverride()); //checks req.body for HTTP method overrides
    app.use(app.router); //perform route lookup based on url and HTTP method
    app.use(express.static(path.join(application_root, "public"))); //Where to serve static content
    app.use(express.errorHandler({ dumpExceptions:true, showStack:true })); //Show all errors in development
});

//Start server
app.listen(4711, function () {
    console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});

I start off by loading the modules required for this project: Express for creating the HTTP server, Path for dealing with file paths and mongoose for connecting with the database. We the create an express server and configure it using an anonymous function. This is a pretty standard configuration and for our application we don’t actually need the methodOverride part. It is used for issuing PUT and DELETE HTTP requests directly from a form, since forms normally only support GET and POST. Finally I start the server by running the listen function. The port number used, in this case 4711, could be any free port on your system. I simply used 4711 since it is the most random number. We are now ready to run our first server:

node server.js

If you open a browser on localhost:4711 you should see something like this:

This is where we left off in Part 2, but we are now running on a server instead of directly from the files. Great job! We can now start defining routes (URLs) that the server should react to. This will be our REST API. Routes are defined by using app followed by one of the HTTP verbs get, put, post and delete, which corresponds to Create, Read, Update and Delete. Let us go back to server.js and define a simple route:

// Routes
app.get('/api', function(req, res){
    res.send('Library API is running');
});

The get function will take the URL as first parameter and a function as second. The function will be called with request and response objects. Now you can restart node and go to our specified URL:

Connect to database

Fantastic. Now since we want to store our data in MongoDB we need to define a schema. Add this to server.js:

//Connect to database
mongoose.connect('mongodb://localhost/library_database');

//Schemas
var Book = new  mongoose.Schema({
    title:String,
    author:String,
    releaseDate: Date
});

//Models
var BookModel = mongoose.model('Book', Book);

As you can see, schema definitions are quite straight forward. They can be more advanced, but this will do for us. I also extracted a model (BookModel) from Mongo. This is what we will be working with. Next up we define a get operation for the rest API that will return all books:

//Get a list of all books
app.get('/api/books', function (req, res) {
    return BookModel.find(function (err, books) {
        if (!err) {
            return res.send(books);
        } else {
            return console.log(err);
        }
    });
});

The find function of Model is defined like this: function find (conditions, fields, options, callback) – but since we want a function that return all books we only need the callback parameter. The callback will be called with an error object and an array of found objects. If there was no error we return the array of objects to the client using the send function of the result object, otherwise we log the error to the console.

To test our API we need to do a little typing in a JavaScript console. Restart node and go to localhost:4711 in your browser. Open up the JavaScript console. If you are using Google Chrome, go to View->Developer->JavaScript Console. If you are using Firefox, install Firebug and go to View->Firebug. If you are on any other browser I’m sure you will find a console somewhere. In the console type the following:

jQuery.get("/api/books/", function (data, textStatus, jqXHR) {
    console.log("Get resposne:");
    console.dir(data);
    console.log(textStatus);
    console.dir(jqXHR);
});

…and press enter and you should get something like this:

Here I used jQuery to make the call to our REST API, since it was already loaded on the page. The returned array is obviously empty, since we have not put anything into the database yet. Lets go and create a POST route that enables this in server.js:

//Insert a new book
app.post('/api/books', function (req, res) {
    var book = new BookModel({
        title:req.body.title,
        author:req.body.author,
        releaseDate:req.body.releaseDate
    });
    book.save(function (err) {
        if (!err) {
            return console.log('created');
        } else {
            return console.log(err);
        }
    });
    return res.send(book);
});

We start by creating a new BookModel passing an object with title, author and releaseDate attributes. The data are collected from req.body. This means that anyone calling this operation in the API needs to supply a JSON object containing the title, author and releaseDate attributes. Actually, the caller can omit any or all attributes since we have not made any one mandatory. We the call the save function on the BookModel passing in an anonymous function for handling errors in the same way as with the previous get route. Finally we return the saved BookModel. The reason we return the BookModel and not just “success” or similar string is that when the BookModel is saved it will get an _id attribute from MongoDB, which the client need when updating or deleting a specific book. Lets try it out again, restart node and go back to the console and type:

jQuery.post("/api/books", {
  "title": "JavaScript the good parts",
  "author": "Douglas Crockford",
  "releaseDate": new Date(2008, 4, 1).getTime()
}, function(data, textStatus, jqXHR) {
    console.log("Post response:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
});

..and then

jQuery.get("/api/books/", function (data, textStatus, jqXHR) {
    console.log("Get response:");
    console.dir(data);
    console.log(textStatus);
    console.dir(jqXHR);

You should now get an array of size 1 back from our server. You may wonder about this line:

  "releaseDate": new Date(2008, 4, 1).getTime()

MongoDB expects dates in UNIX time format (milliseconds from Jan 1st 1970), so we have to convert dates before posting. The object we get back however, contains a JavaScript Date object. Also note the _id attribute of the returned object.

Lets move on to creating a get that retrieves a single book in server.js:

app.get('/api/books/:id', function(req, res){
    return BookModel.findById(req.params.id, function(err, book){
        if(!err){
            return res.send(book);
        } else {
            return console.log(err);
        }
    });
});

Here we use colon notation (:id) to tell express that this part of the route is dynamic. We also use the findById function on BookModel to get a single result. Now you can get a single book by adding its id to the URL like this:

jQuery.get("/api/books/4f95a8cb1baa9b8a1b000006", function (data, textStatus, jqXHR) {
    console.log("Get resposne:");
    console.dir(data);
    console.log(textStatus);
    console.dir(jqXHR);
});

Lets create the PUT (update) function next:

app.put('/api/books/:id', function(req, res){
    console.log('Updating book ' + req.body.title);
    return BookModel.findById(req.params.id, function(err, book){
        book.title = req.body.title;
        book.author = req.body.author;
        book.releaseDate = req.body.releaseDate;
        return book.save(function(err){
            if(!err){
                console.log('book updated');
            } else {
                console.log(err);
            }
            return res.send(book);
        });
    });
});

This is a little larger than previous ones, but should be pretty straight forward – we find a book by id, update its properties, save it and send it back to the client.

To test this we need to use the more general jQuery ajax function (replace the id with what you got from a GET request):

jQuery.ajax({
  url:"/api/books/4f95a8cb1baa9b8a1b000006",
  type:"PUT",
  data:{
    "title": "JavaScript The good parts",
    "author": "The Legendary Douglas Crockford",
    "releaseDate": new Date(2008, 4, 1).getTime()
  },
  success: function(data, textStatus, jqXHR) {
    console.log("Post resposne:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
  }});

Finally we create the delete route:

app.delete('/api/books/:id', function(req, res){
    console.log('Deleting book with id: ' + req.params.id);
    return BookModel.findById(req.params.id, function(err, book){
        return book.remove(function(err){
            if(!err){
                console.log('Book removed');
                return res.send('');
            } else {
                console.log(err);
            }
        });
    });
});

…and try it out:

jQuery.ajax({
  url:'/api/books/4f95a5251baa9b8a1b000001',
  type: 'DELETE',
  success:function(data, textStatus, jqXHR){
    console.log("Post resposne:");
    console.dir(data);
    console.log(textStatus);
    console.dir(jqXHR);
  }
});

So now our REST API is complete – we have support for all HTTP verbs. What next? Well, until now I have left out the keywords part of our books. This is a bit more complicated since a book could have several keywords and we don’t want to represent them as a string, but rather an array of strings. To do that we need another schema. Add a Keywords schema right above our Book schema:

//Schemas
var Keywords = new mongoose.Schema({
    keyword: String
});

To add a sub schema to an existing schema we use brackets notation like so:

var Book = new mongoose.Schema({
    title:String,
    author:String,
    releaseDate:Date,
    keywords: [Keywords]
});

Also update POST and PUT

app.post('/api/books', function (req, res) {
    var book = new BookModel({
        title:req.body.title,
        author:req.body.author,
        releaseDate:req.body.releaseDate,
        keywords: req.body.keywords
    });
    book.save(function (err) {
        if (!err) {
            return console.log('created');
        } else {
            return console.log(err);
        }
    });
    return res.send(book);
});

app.put('/api/books/:id', function(req, res){
    return BookModel.findById(req.params.id, function(err, book){
        book.title = req.body.title;
        book.author = req.body.author;
        book.releaseDate = req.body.releaseDate;
        book.keywords = req.body.keywords;
        return book.save(function(err){
            if(!err){
                console.log('book updated');
            } else {
                console.log(err);
            }
            return res.send(book);
        });
    });
});

There we are, that should be all we need, now we can try it out in the console:

jQuery.post("/api/books", {
  "title": "Secrets of the JavaScript Ninja",
  "author": "John Resig",
  "releaseDate": new Date(2008, 3, 12).getTime(),
  "keywords":[{
     "keyword":"JavaScript"
   },{
     "keyword": "Reference"
   }]
}, function(data, textStatus, jqXHR) {
    console.log("Post response:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
});

You should now have a fully functional REST server. We will use this in the next part of the tutorial to make our library application persistent.

  8 Responses to “Backbone in Baby Steps, part 2.5”

  1. Could you please let us know how to install MongoDB and the Express in Windows 7 and also how to verify ? Thank you.

    • Spit on the Windows.Try in Linux. It’s not so hard , believe me. With Windows you will have a lot of surprises with node and MongoDB.

  2. I don’t have access to any windows machine so I cannot verify, but the intructions on the mongodb web site seems quite explanatory: http://docs.mongodb.org/manual/tutorial/install-mongodb-on-windows/ . Regarding Express it should work the same “npm install express@2.5.9″.

  3. Noticed that even in the code it expects ‘_id’ instead of ‘id’. For example, the code wasnt working if I use ‘/api/books/:id’ but it did when I tried ‘/api/books/:_id’. Same goes for findById(req.params._id) instead of findById(req.params.id. Is this a valid argument or am I doing something wrong here.

  4. Superb. Excellent. Wunderbar.Vielen Dank.(on German)

  5. Hello, i am having a little problem with this. In the connect to database section, i also get an error at

    mongoose.connect( ‘mongodb://localhost/library_database’ );

    does anyone know why?? The error i get is this..

    Error: failed to connect to [localhost:27017]
    at null. (/Users/160076/Documents/_kofi/samples/javascript/backbone/Library/node_modules/mongoose/node_modules/mongodb/lib/mongodb/connection/server.js:604:74)
    at EventEmitter.emit (events.js:106:17)
    at null. (/Users/160076/Documents/_kofi/samples/javascript/backbone/Library/node_modules/mongoose/node_modules/mongodb/lib/mongodb/connection/connection_pool.js:139:15)
    at EventEmitter.emit (events.js:98:17)
    at Socket. (/Users/160076/Documents/_kofi/samples/javascript/backbone/Library/node_modules/mongoose/node_modules/mongodb/lib/mongodb/connection/connection.js:476:10)
    at Socket.EventEmitter.emit (events.js:95:17)
    at net.js:426:14
    at process._tickCallback (node.js:415:13)

  6. Second Jquery.get is missing last line with brackets:-
    });

  7. Everyone loves it when folks get together and share ideas.
    Great blog, stick with it!

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>