In Depth Guide on Building a REST API with Node.js, Restify & MongoDB

With the multitudes of JavaScript frameworks available these days, there are countless options for building APIs. This also means things can get cloudy and many users lose sight of the most important factors when it comes to building a REST API — speed, easability of use, and the overall presence in the JavaScript community. There have been dozens of performance benchmarks on various frameworks such as Hapi, Express and Koa; however, Restify seems to be the clear winner. For this tutorial, we’ve chosen to work with Restify.

Please note that this tutorial assumes that you have an understanding of git, the command line and node.js. For those of you who would like to see the boilerplate codebase, you can clone the repository from GitHub. If you happen to find any issues, please submit a pull request, as they are always accepted. You can follow the steps below in order, or deviate away as you’d like… just don’t get lost ;). With that said, let’s get started!

1. Create your directory
 Inside of a directory of your choice, create a new directory called API. For this tutorial, we’re going to build the API in the root directory.

$ cd ~
$ mkdir api && cd api

2. Install required dependencies

We’ll be using a couple of popular NPM modules in addition to Restify:

  • Mongoose (a driver for MongoDB)
  • Mongoose API Query (lightweight Mongoose plugin to help query your REST API)
  • Mongoose Created Modified (lightweight Mongoose plugin that adds support for created_at and modified_at timestamps)
  • Lodash (a toolkit of Javascript functions)
  • Winston (a multi-transport async logging library)
  • Bunyan Winston Adapter (allows the user of the winston logger in restify server without really using bunyan — the default logging library)
  • Restify Errors (HTTP and REST errors abstracted from Restify)

For the rest of the API, we’ll use native Node.js API’s so to keep things nice and clean.

First, let’s initialize our package.json file using the following command (please follow the prompts):

$ npm init

Woohoo! You should now have a package.json file located in your /api directory. This file serves as the manifest for your NPM modules.

Next up, let’s get our NPM modules installed into /node_modules and saved to our package.json file so they can be checked into version control.

$ npm install restify mongoose mongoose-api-query mongoose-createdmodified lodash winston bunyan-winston-adapter restify-errors --save

Once the installation is complete, you’ll notice that your package.json is now populated with your dependencies and a new directory called node_modules exists. You can easily check the contents of the package.json file with the following command:

$ cat package.json

3. Lay the foundation

Now that we’ve covered the basics, let’s start to lay the foundation for our application. First off, we’ll need to create a MongoDB database to house or data and connect our API to. You have a couple of options here:

  • Compose (A rock solid MongoDB deployment platform optimized for scale, speed and security.)
  • MongoDB Atlas (MongoDB as a Service offering available on Amazon Web Services (AWS) powered by the creators of the database.)
  • Self-host on Google, AWS, or another provider
  • Host locally and use a database manager such as Robomongo

For sake of simplicity and cost, we’re going to be using a local version of MongoDB and Robomongo to manage our database (option 4). I’m running on a macOS, so I’ll be using Homebrew to install MongoDB and Robomongo.

$ brew install mongodb
$ brew cask install robomongo

There are a few additional steps that you’ll need to take in order to get MongoDB up and running 100%. With that said, please have a look at this tutorial by Treehouse. Robomongo on the other hand will be installed in your /Applications directory on your computer.

One thing that isn’t covered in the tutorial by Treehouse is how to configure your database settings, so let’s go ahead and do that now:

$ mongo 
$ use api

Now that we have MongoDB setup to use the database api, let’s go ahead and get our config.js file created (this will house all of our configuration data for the REST API):

$ touch config.js

Open your config.js file in an editor or your choice (I’ll be using Atom), and add the following contents to your file:

'use strict'

module.exports = {
name: 'API',
version: '0.0.1',
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 3000,
base_url: process.env.BASE_URL || 'http://localhost:3000',
db: {
uri: 'mongodb://127.0.0.1:27017/api',
},
}

Next, let’s create our index.js file, the entry point for our REST API.

$ touch index.js

Open your index.js and add the following contents to your file:

'use strict'

/**
* Module Dependencies
*/
const config = require('./config'),
restify = require('restify'),
bunyan = require('bunyan'),
winston = require('winston'),
bunyanWinston = require('bunyan-winston-adapter'),
mongoose = require('mongoose')

/**
* Logging
*/
global.log = new winston.Logger({
transports: [
new winston.transports.Console({
level: 'info',
timestamp: () => {
return new Date().toString()
},
json: true
}),
]
})

/**
* Initialize Server
*/
global.server = restify.createServer({
name : config.name,
version : config.version,
log : bunyanWinston.createAdapter(log),
})

/**
* Middleware
*/
server.use(restify.jsonBodyParser({ mapParams: true }))
server.use(restify.acceptParser(server.acceptable))
server.use(restify.queryParser({ mapParams: true }))
server.use(restify.fullResponse())

/**
* Error Handling
*/
server.on('uncaughtException', (req, res, route, err) => {
log.error(err.stack)
res.send(err)
});

/**
* Lift Server, Connect to DB & Bind Routes
*/
server.listen(config.port, function() {

mongoose.connection.on('error', function(err) {
log.error('Mongoose default connection error: ' + err)
process.exit(1)
})

mongoose.connection.on('open', function(err) {

if (err) {
log.error('Mongoose default connection error: ' + err)
process.exit(1)
}

log.info(
'%s v%s ready to accept connections on port %s in %s environment.',
server.name,
config.version,
config.port,
config.env
)

require('./routes')

})

global.db = mongoose.connect(config.db.uri)

})

As you can see from the entry point code above, we are very specific in the order that we include resources.

4. Create your database models

Models are fancy constructors compiled from our Schema definitions. Basically, they allow us to tell the database what to store and instances of these models represent documents which can be saved and retrieved from our database via Mongoose. All document creation and retrieval from the database is handled by these models, so let’s go ahead and create our todo model:

$ mkdir models && cd models
$ touch todo.js

Next up, let’s define our schema:

'use strict'

const mongoose = require('mongoose'),
mongooseApiQuery = require('mongoose-api-query'),
createdModified = require('mongoose-createdmodified').createdModifiedPlugin

const TodoSchema = new mongoose.Schema({
task: {
type: String,
required: true,
trim: true,
},
status: {
enum: ['pending', 'complete', 'overdue']
},
}, { minimize: false });


TodoSchema.plugin(mongooseApiQuery)
TodoSchema.plugin(createdModified, { index: true })

const Todo = mongoose.model('Todo', TodoSchema)
module.exports = Todo

Note: Our createdModified module will add the created_at and modified_at timestamps for us (ISO-8601), and MongoDB will automatically generate a UUID called _id.

5. Generate API routes

Now that we have our models in place, it’s time to create our API routes. We’ll be creating a basic REST API that consists of Create, Read, Update and Delete methods (also known as CRUD operations).

Let’s start by backing out of the models directory, creating a new directory called routes, and finally creating an index file to hold our methods:

$ cd ../ && mkdir routes && cd routes && touch index.js

Next up, we’ll define the various routes that we will need to run CRUD operations:

  • POST /todos (Creates a todo item)
  • GET /todos (Lists all todos in the queue)
  • GET /todos/:todo_id (Gets a specific todo item in the queue)
  • PUT /todos/:todo_id (Updates a specific todo item in the queue)
  • DELETE /todos/:todo_id (Destroys a specific todo item in the queue)

First, let’s include our module dependencies so we have the necessary tools to work with:

'use strict'

/**
* Module Dependencies
*/
const _ = require('lodash'),
errors = require('restify-errors')

Second, we’ll include the todo.js model so that we have access to the schema:

/**
* Model Schema
*/
const Todo = require('../models/todo')

Let’s continue on by creating a route for each of the operations listed above:

/**
* POST
*/
server.post('/todos', function(req, res, next) {

let data = req.body || {}

let todo = new Todo(data)
todo.save(function(err) {

if (err) {
log.error(err)
return next(new errors.InternalError(err.message))
next()
}

res.send(201)
next()

})

})


/**
* LIST
*/
server.get('/todos', function(req, res, next) {

Todo.apiQuery(req.params, function(err, docs) {

if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
}

res.send(docs)
next()

})

})


/**
* GET
*/
server.get('/todos/:todo_id', function(req, res, next) {

Todo.findOne({ _id: req.params.todo_id }, function(err, doc) {

if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
}

res.send(doc)
next()

})

})


/**
* UPDATE
*/
server.put('/todos/:todo_id', function(req, res, next) {

let data = req.body || {}

if (!data._id) {
_.extend(data, {
_id: req.params.todo_id
})
}

Todo.findOne({ _id: req.params.todo_id }, function(err, doc) {

if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
} else if (!doc) {
return next(new errors.ResourceNotFoundError('The resource you requested could not be found.'))
}

Todo.update({ _id: data._id }, data, function(err) {


if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
}


res.send(200, data)
next()

})

})

})

/**
* DELETE
*/
server.del('/todos/:todo_id', function(req, res, next) {

Todo.remove({ _id: req.params.todo_id }, function(err) {

if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
}

res.send(204)
next()

})

})

Start the server

In order for our server to run, we’ll need to make sure that MongoDB is up and running locally (you can bypass this step if you decided to go with a hosted platform).

In a new terminal window, simply run the following command to start the process:

$ mongod

Now that MongoDB is started, we can run the following command from the main directory to start the server:

$ node index.js

Note: If you’re rapidly developing on an API, Nodemon is a great to have in your toolbox. Simple run npm instal -g nodemon to install Nodemon globally. You can then start the API by typing nodemon . in the main directory.

7. Familiarizing ourselves with Postman

Okay, we’ve taken the time to setup the necessary base infrastructure, setup the database, create our models and routes, etc. Let’s better understand how to use the API.

My favorite tool of choice for testing REST APIs is Postman, a free application which can be downloaded here. Postman hits the sweet spot between functionality and usability by providing all of the HTTP functionality needed for testing APIs via an intuitive UI.

Once downloaded, load up the application and head over to a new tab in the app and set the URL to http://localhost:3000. Since we’re developing a JSON API (it’s the modern thing to do), let’s go ahead and set the Content-Type header to application/json (under the Headers section of the dashboard).

With that in place, you can now hit the endpoints that you created! For example, we can send a POST to the /todos endpoint with the following payload to create a new todo item (notice that the response will be a 201 status code):

{
"task": "make moves",
"status": "pending"
}

We can then query the the API via the /todos endpoint and get back the created object:

[
{
"_id": "5853020b632a154aa6d5750d",
"task": "make moves",
"status": "pending",
"__v": 0,
"modified": "2016-12-15T20:50:19.027Z",
"created": "2016-12-15T20:50:19.027Z"
}
]

To update the status to “complete”, simply send a PUT to the /todos/:todo_id endpoint (in our case /todos/5853020b632a154aa6d5750d):

And finally, if you’d like to delete the object, you can hit the /todos/:todo_id endpoint with a POST request:

8. Querying the API via Mongoose API Query

Remember that awesome Mongoose plugin that we installed called mongoose-api-query? While we won’t be covering the queries here, a full rundown on the query options can be found on the GitHub repo. You can do everything from adding intense filters, to ordering in ascending order or descending order, to latitude and longitude lookups (should your todo list expand in functionality).

9. Wrapping Up

You now have five functional API endpoints to power a todo list in which you can run full CRUD operations. While we covered just the tip of the iceberg, you are well along your way to adding additional functionality using the API/structural patterns provided in this tutorial.

Next steps

One important aspect of API development that we didn’t cover are building out a test framework. Generally speaking, writing tests for an API is somewhat hard to do.

While great test frameworks/libraries like Mocha and Chai exist, I prefer to use Jetpacks by Postman to get the job done. By doing so, I can easily test on my local environment (primarily the database) and have enough confidence to push to production, assuming all tests pass.

Writing tests is entirely up to you; however, they are highly recommended and are totally worth it from a time perspective (trust me, tests have saved me from frustration and production-level bugs many times).

All done

Building a basic REST API is an incredibly valuable building block of being a respectable coder, which is why we felt it was necessary to provide a means through which to understand the basics of this task. We explained the importance of why we did things in the order we did, why we installed all of the tools we did and why, we feel, it is infinitely important to TEST things before you throw them into production. If you have any thoughts on REST APIs, this tutorial, or anything else, please comment below or email nparsons08@gmail.com directly. I do my very best to respond to all inquiries.