From reading the past tutorials you should be starting to get a hang of API development, so we’re going to take a big leap forward and persist some data to MongoDB. We’ll create a very simple guestbook API, which records guestbook entries, reads them, updates them and deletes them. Install the latest version of MongoDB and let’s get started.
Let’s create a separate folder for this part of the tutorial and call it “part-5”. We’ll want to start MongoDB and keep it running, but beforehand we have to create a configuration file. In your part-5 folder, create a file called mongodb.conf and add the following
dbpath = db/
bind_ip = 127.0.0.1
noauth = true # use 'true' for options that don't take an argument
The first line instructs MongoDB to place it’s files in the “db” folder, but it won’t create that folder for us, so go ahead and do that now. The second line mentions which IP address to listen on. Throughout this tutorial we’ve seen 127.0.0.1. If you haven’t guessed by now, 127.0.0.1 is the IP address which DNS maps to from “localhost”, and it’s a loopback address; it always points to your own computer / device. Since we’re only listening on the local IP address, we’ll set noauth = true to avoid having to authenticate.
With our db folder created and the mongodb.conf file ready, let’s start mongodb with the following:
mongod --config mongodb.conf
You should now see a bunch of output on the screen, and mongo will be waiting for connections on port 27017. Leave that running for the rest of this tutorial.
Next up, we’re going to need to talk to our DB, and we’ll do that with a driver called Mongoose. It’s a third-party node package, so we’ll install it with npm:
npm install mongoose
If you’re in a different folder than you used for the last tutorial, remember to install restify, too:
npm install restify
In the last tutorial we separated our request handlers out from our main index.js file, to keep things tidy. Our index.js file for the Guestbook API will look similar. Copy and paste the following into a file saved as index.js:
var restify = require("restify");
var mongoose = require("mongoose");
var guestbookHandler = require("./guestbookHandler");
mongoose.connect("mongodb://localhost/guestbook");
var server = restify.createServer();
server.use(restify.bodyParser());
server.post( "/guestbook/:name", guestbookHandler.handleCreate );
server.get( "/guestbook/:id", guestbookHandler.handleRead );
server.put( "/guestbook/:id/", guestbookHandler.handleUpdate );
server.del( "/guestbook/:id", guestbookHandler.handleDelete );
server.listen( 8765 );
console.log( "server is running" );
We start out by including our restify and mongoose packages. “guestbookHandler” is a file we’ll get to a bit later on. You can see here we connect to mongodb using a special URL. Note that the last part of the URL, “/guestbook”, is arbitrary: we can call it anything we like, just remember it for accessing mongo in the shell, which we’ll do later on.
The next line creates our restify server, and then we have something new:
server.use(restify.bodyParser());
Here we’re instructing restify to parse data sent up with our request (in the request body), so that we can access it easily through the req.params object. We haven’t sent request bodies yet with curl, but you’ll see how this works later on.
The next 4 lines should look familiar: we’re defining our routes, and the functions that will handle them. We have a handler for HTTP POST, GET, PUT and DELETE, which map to Create, Read, Update and Delete (CRUD). We name some parameters in the URLs for easy access with req.params.
Finally, we tell our server to start on port 8765 and then output a log message to the console.
We’ve kept our index.js file clean by separating the request handlers into their own file, so let’s see what that file looks like. Copy and paste the following code into a new file named guestbookHandler.js:
var restify = require("restify");
var guestbook = require("./guestbook" );
function handleCreate(req, res, next)
{
var name = req.params.name;
return guestbook.createEntry( name, createCallback );
function createCallback(err, id)
{
if ( err )
return next( new restify.InternalError( err ) );
res.send( { id : id } );
return next();
}
}
function handleRead(req, res, next)
{
var id = req.params.id;
return guestbook.readEntry( id, readCallback );
function readCallback(err, guestbookDoc)
{
if ( err )
return next( new restify.InternalError( err ) );
res.send( guestbookDoc );
return next();
}
}
function handleUpdate(req, res, next)
{
var id = req.params.id;
var name = req.params.name;
return guestbook.updateEntry(id, name, updateCallback );
function updateCallback(err, guestbookDoc)
{
if ( err )
return next( new restify.InternalError( err ) );
res.send( guestbookDoc );
return next();
}
}
function handleDelete(req, res, next)
{
var id = req.params.id;
return guestbook.deleteEntry( id, deleteCallback );
function deleteCallback(err)
{
if ( err )
return next( new restify.InternalError( err ) );
res.end();
return next();
}
}
exports.handleCreate = handleCreate;
exports.handleRead = handleRead;
exports.handleUpdate = handleUpdate;
exports.handleDelete = handleDelete;
There’s a lot here, so let’s walk through it.
var restify = require("restify");
var guestbook = require("./guestbook" );
The second line is including a local file called “guestbook.js”, which we’ll get to in a bit.
function handleCreate(req, res, next)
{
var name = req.params.name;
return guestbook.createEntry( name, createCallback );
function createCallback(err, id)
{
if ( err )
return next( new restify.InternalError( err ) );
res.send( { id : id } );
return next();
}
}
In handleCreate, we get the parameter called ‘name’ from the URL route we specified in index.js (remember the ‘/:name’ part). This should seem familiar: we’ll be expecting a URL like /guestbook/Richard, and req.params.name will contain ‘Richard’. Next we have a bit of asynchronous code, so read it carefully. We’re returning guestbook.createEntry on the next line, and passing along the name of a callback, in this case our createCallback function. So even though we’re returning here, our ‘createCallback’ will be invoked later on when guestbook.createEntry is finished. While this ‘return’ statement is not required, it’s best practice to put a return on your last line, both for readability and to avoid errors. Note that your ‘last line’ often comes before your callbacks.
The createCallback function is fairly straight forward. It accepts two parameters: an error, and an ID for the newly created document. If an error is returned, we use the shorthand of returning next() with that error specified. The next() function will propagate that error back to the client, so for bonus points we could call next() with the appropriate HTTP status code for that error.
If there was no error, we want to send the ID back to the client. We pass the ID to res.send() in JSON format, then return next() which will end the response.
The remaining functions follow the same form: grab any input we expect from the request and pass it along to our ‘guestbook’ object, with a callback defined below. We end this file with some exports to make these functions visible to index.js.
Last but not least, we get to our CRUD file. This is the file that handles all of our data persistence. We’ve kept our guestbook MongoDB code in one file so that changing the way we talk to our database is simple in the future. Copy and paste the following code into a new file called guestbook.js:
var mongoose = require("mongoose");
var guestbookSchema = new mongoose.Schema(
{
name : String,
dateVisited : { type: Date, default: Date.now, index: true }
});
var Guestbook = mongoose.model( "guestbookEntry", guestbookSchema );
function createEntry(name, callback)
{
var guestbookEntry = new Guestbook( { name: name } );
return guestbookEntry.save( guestbookSaveCallback );
function guestbookSaveCallback(err, guestbookDoc)
{
return callback( err, guestbookDoc._id );
}
}
function readEntry(id, callback)
{
return Guestbook.findOne( {_id : id }, guestbookFindCallback );
function guestbookFindCallback(err, guestbookDoc)
{
return callback( err, guestbookDoc );
}
}
function updateEntry(id, name, callback)
{
return readEntry( id, readEntryCallback );
function readEntryCallback(err, guestbookDoc)
{
if ( err )
{
return callback( err, null );
}
guestbookDoc.name = name;
return guestbookDoc.save( guestbookDocSaveCallback );
}
function guestbookDocSaveCallback(err, updatedGuestbookDoc)
{
return callback( err, updatedGuestbookDoc );
}
}
function deleteEntry(id, callback)
{
return Guestbook.remove( { _id : id }, guestbookRemoveCallback );
function guestbookRemoveCallback( err )
{
return callback( err );
}
}
exports.createEntry = createEntry;
exports.readEntry = readEntry;
exports.updateEntry = updateEntry;
exports.deleteEntry = deleteEntry;
Again, there’s a lot here, so let’s talk through it:
var mongoose = require("mongoose");
var guestbookSchema = new mongoose.Schema(
{
name : String,
dateVisited : { type: Date, default: Date.now, index: true }
});
var Guestbook = mongoose.model( "guestbookEntry", guestbookSchema );
We include the mongoose node package, and then define our Schema. In this case, our schema has a name, which is a string, and dateVisited, for which we specify a few extra properties: it’s of type Date, and has a default value of Date.now, so we don’t have to manually specify the Date when we create our documents. We also ask MongoDB to index this property, so that looking up documents by their dateVisited field stays fast in large collections. Note also that the documents have a built-in “_id” property, which is indexed and unique, and created automatically with each document.
In the next line, we create a Model with this schema. Our Guestbook model will store it’s data in a collection called “guestbookEntry”. If we wanted to store a second collection separately, say a private guestbook, with the same desired schema, we could add a second line here replacing “guestbookEntry” with, say, “privateGuestbookEntry”.
One more note about schemas: MongoDB uses dynamic schemas, meaning we can add and remove properties as we like. This is handy, because later on if we want to, say, add a ‘comment’ field to our schema, we can just tack it on and start using it. We can also decide to start putting, say, a JSON object containing “lastName” and “firstName” into our “name” field. This flexibility can also lead to problems: if we accidentally stuff a large JSON object into “name” for example, we may suddenly find our database performance dropping without understanding why. To prevent these kinds of mistakes, you can enforce a “strict” option when creating your Mongoose Schema.
Continuing on:
function createEntry(name, callback)
{
var guestbookEntry = new Guestbook( { name: name } );
guestbookEntry.save( guestbookSaveCallback );
function guestbookSaveCallback(err, guestbookDoc)
{
return callback( err, guestbookDoc._id );
}
}
In createEntry, we start by creating a Guestbook Document. The Guestbook constructor accepts a JSON object, and note that we’re only specifying ‘name’, because dateVisited has a default value. We then call guestbookEntry.save() with a callback. That callback has 2 parameters: err, and guestbookDoc. If an error occurs, it’s passed in “err”, and guestbookDoc will likely be null. Otherwise, guestbookDoc contains our newly created Mongoose document, and so we invoke our callback with it’s _id property mentioned earlier.
function readEntry(id, callback)
{
return Guestbook.findOne( {_id : id }, guestbookFindCallback );
function guestbookFindCallback(err, guestbookDoc)
{
return callback( err, guestbookDoc );
}
}
This code should be readable by now. Our Guestbook model has a findOne method, and we pass in a query specifying that the _id property should match our ‘id’ variable.
If you’ve followed along this far, you should be able to read the remainder of the file: our Update and Delete methods.
After you’ve saved these 3 files, start our server with the usual command:
node index.js
Let’s test our server with some curl commands. First of all, let’s create a guestbook entry:
curl -X POST http://localhost:8765/guestbook/richard
We tell curl to issue an HTTP POST to our local server. If you’ve followed along carefully, you should get back the JSON we returned containing the ID:
{"id":"5176db93d8dab83d0c000001"}
Your ID will likely differ, so use your own value in the upcoming curl statements. OK, so let’s take a look at the full document by issuing a GET
curl http://localhost:8765/guestbook/5176db93d8dab83d0c000001
Note that we could have explicitly specified that this is a GET by issuing the -X GET command, but GET is the default HTTP method in curl. This should return the full JSON document:
{"name":"richard","_id":"5176db93d8dab83d0c000001","__v":0,"dateVisited":"2013-04-23T19:05:55.212Z"}
We see the name and dateVisited properties, along with the built-in _id property and the system field “__v” which is used for versioning.
We’ve created and read the document, let’s try updating it:
curl -X PUT http://localhost:8765/guestbook/5176db93d8dab83d0c000001 -d "name=penner"
You’ll notice something new here. We’ve added a “data” parameter to pass in “name”. You’ll remember that back in our index.js file we had restify “use” the bodyParser, so this “name” data will show up in our req.params object. This is a convenience; we could manually pull it out of the body if restify was not parsing this for us.
If that went OK, you’ll have your updated document returned:
{“name”:”penner”,”_id”:”5176db93d8dab83d0c000001″,”__v”:0,”dateVisited”:”2013-04-23T19:05:55.212Z”}
Before we get to the delete, which will remove our document, I want to touch on one more part of MongoDB: the mongo shell. If you’re curious where all of this data is going, open a new Terminal tab and type in:
mongo guestbook
That will connect you to our guestbook instance. From there, type in db.getCollectionNames() and start poking around. You’ll notice, for example, that while we instructed Mongoose to store our documents in “guestbookEntry”, it’s magically pluralized the collection for us:
> db.getCollectionNames()
[ "guestbookentries", "system.indexes" ]
To see our document, type:
> db.guestbookentries.find()
{ "__v" : 0, "_id" : ObjectId("5176db93d8dab83d0c000001"), "dateVisited" : ISODate("2013-04-23T19:05:55.212Z"), "name" : "penner" }
Hit Ctrl-C to exit the mongo shell. Finally, let’s test our delete handler with the following curl command:
curl -X DELETE http://localhost:8765/guestbook/5176db93d8dab83d0c000001
If you return to the mongo shell now, your collection will be empty.
If you’ve made it this far, you’re on the home stretch. You can now write APIs with cleanly-separated handlers, and you can persist your data to MongoDB. Best of all, you can fully test your API with curl.
To wrap things in part 6, we take a look at how to talk to this API in iOS.