When building a new application an important consideration is often the platform you'll be releasing it on. Back in the old days these would largely have to be built and managed separately, but in the last half-decade or so a number of better options have become available to us. One of the most common techniques is the use of a JSON API (Application Programming Interface). Using the web to serve up JSON documents rather than HTML documents allows us to directly deliver the data that we need to build the components of our applications, or to allow other developers to build applications using our services.  

Today we're going to take a look at how to build an API of your own using Node.js, Express and MongoDB. For the database in the examples I'll be using mLab, but you can use any instance of a Mongo database you like, in fact with some edits you can really use any data storage method that you like. API's are as flexible as any code and largely agnostic about where and how we store our data, instead being primarily concerned with how to respond to the requests that we make of it, we'll be using MongoDB & mLab largely out of convenience for style and formatting. 

On a final note, testing an API, especially if you haven't done so before, is a little different than what you may be used to. Since our requests are typically being sent through form data or through the header, having a convenient way to test that they are working as intended without extra code and legwork often requires specialized tools. The most thorough of these is probably the Postman web extension, which I highly recommend for work with API's and web services. 

Alright lets dive in, we're going to be creating a project structure like so:

So an app directory with two sub-directories models and routes, and four files; our user.js model, the routing file route.js, our package.json file and the server.js file that we will use to launch the application.

Let's set up the package.json first, as it's really simple in this instnace.

package.json

{
  "name": "simple-api",
  "main": "server.js",
    
  "dependencies": {
    "express": "~4.0.0",
    "mongoose": "^4.4.6",
    "body-parser": "~1.0.1"
  }
  
}

We're going to be using three packages: 

  • The express framework to handle our server, routing and requests / responses.
  • Mongoose, which will help us with our MongoDB object modelling.
  • body-parser, which we will use to parse and handle our JSON.

The versions used here aren't that big of a deal, they were simply the most recent at the time of writing, you can feel free to npm install express mongoose body-parser --save

Let's take a look at our user model next.

user.js

// Set up mongoose and mongoose.Schema
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

// Export our mongoose model, with a user name and friends list
module.exports = mongoose.model('User', new Schema({
    name: String,
    friends: [String]
}));

This is also as straightforward as you can get, we're creating a basic user schema that gives each user a name of type String and a list of friends, which we've defined as an Array of Strings.

The route.js file is where the bulk of the application is going to be. We're going to build a simple RESTful service using standard verbs that lets us GET users, DELETE users, create new users through POST requests and update users through PUT requests. We're also going to take a look at some of the hiccups that can occur if you don't spend a little time understanding the architecture of your API beforehand. 

I'm going to heavily comment the route.js file to help explain everything that's going on in there.

route.js

// Set up express and include our User model
var express = require('express');
var User = require('../models/user');

// Get an instance of the express router
var router = express.Router();



// Our first set of routes, those that end with /users
router.route('/users')

/*
// When we POST to the /users route we want to create 
// a new user from the data sent in the request. We're 
// going to assume that our new users come packaged with 
// a friend (a la MySpace Tom), though we could easily 
// initialize this to an empty array as well.
*/
.post(function(req,res){

    var user = new User();
  
    // Set the user name, and add our friend to the friends array 
    user.name = req.body.name; 
    user.friends.push(req.body.friends); 

    // Save the user to the database
    // If we don't get any errors respond with a success message
    user.save(function(err){
        if (err) { res.send(err); }

        res.json({ message: 'We have created a new user!' });
    });
})

/* 
// When we make a GET request to /users we want 
// to return all of our users in the response as 
// a JSON object.
//
// We'll use mongoose to find our user documents, 
// and if there are no errors, we'll send a response 
// containing our users as JSON
*/
.get(function(req,res){

    User.find(function(err, users){
        if (err){ res.send(err); }

        res.json(users);
    });

})



// Routes for a specific user, ending in /users/:user_id
router.route('/users/:user_id')

/*
// When we make a GET request for a specific user,
// we want to return that user as a JSON object.

// We'll use mongoose to find our user by their id, 
// and if there are no errors, we'll send a response 
// containing our user data as JSON

// As a side note, you may have noticed that we didn't
// create a user_id parameter in our schema, this parameter
// is automatically uniquely assigned by MongoDB. We can use 
// our own unique keys if we so choose.
*/
.get(function (req,res){

    User.findById(req.params.user_id, function(err, user){
        if (err){ res.send(err); }

        res.json(user);
    });
})

/*
// When we make a PUT request we want to update 
// the user with the specified id using the data 
// in the request, if there are no errors we'll 
// respond with a success message.
*/
.put(function(req,res){
    User.findById(req.params.user_id, function(err, user){
        if (err){ res.send(err); }

        user.name = req.body.name;
        user.friends.push(req.body.friends);

        user.save(function(err){
            if (err) { res.send(err); }

            res.json({ message: 'User Updated!' });
        });
    });
})

/*
// When we make a DELETE request we want to 
// remove the user with the specified id, if 
// there are no errors we'll again respond 
// with a success message
*/
.delete(function(req, res){
    User.remove({_id:req.params.user_id}, function(err, user){
        if (err){ res.send(err); }

        res.json({ message: 'Successfully removed!' });
    });
})


module.exports = router;

In essence, this file is really just handling what we do with traffic that we receive to the given routes. Based on the requests we send we may make changes to our database, serve up the specific data being requested, or send standardized messages that can be processed by event handlers to tell other parts of the application what is going on. It's the core of our API. 

Finally let's take a look at the set up for a basic server.js file to launch our application.

server.js

// Set up our packages
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var apiRoute = require('./app/routes/route');

// Connect to our database
mongoose.connect('mongodb://USERNAME:PASSWORD@ds019638.mlab.com:19638/DBNAME');

// Configure body-parser
app.use(bodyParser.urlencoded({ extended:true }));
app.use(bodyParser.json());

// Set our port
var port = 8080;

// Prefix our routes with with /simple-api
app.use('/simple-api', apiRoute);

// START THE SERVER
app.listen(port);

So everything here is looking pretty good. We're making use of the standard GET, POST, PUT and DELETE, handling our incoming requests and responses, and taking care of data storage and persistence with MongoDB and mLab. However, this example highlights a problem that slipped in, what happens if we want to remove a friend from our friend list? Our PUT request, as it stands, is poorly structured. On submission it seems that it takes a user name and a friend (or a user name and an empty friend) and then updates our object, but this is extremely limited.

We could adjust the way that we are assigning values to the friends property. Instead of pushing individual friends to the array as we receive them, we could add a logic layer that builds a new array from our current friend list and the data in the request and subsequently have our PUT method reassign the entire array each time. This would likely be best for this simple situation, but we still potentially have a broader issue. In short, what do we do if we want to do a partial or specific update to some of the data in an object, rather than all of it at once?

We could solve this in a few different ways, and this is where the individual architecture of different API's comes into play, depending on what is required for the service that you are building. A common technique is to extend the actual API endpoint, so rather than having our request go through /users/:user_id, we go down to the individual property, /users/:user_id/friends, and build unique GET, PUT and DELETE methods specifically for that property and route. This allows us to do partial updates on our documents for any properties that we find may be frequently updated, but it comes at a cost. As the application grows you can potentially have hundreds of these fine grain elements, all with their own methods and handlers, and without some kind of code generation or scaffolding this can get out of hand quick. There are a few other techniques that can alleviate the partial update issue, I would recommend this blog post for a good succinct look at their pros and cons. 

Other than that, putting together an API really is just a matter of proper route handling and building the basic functionality to respond to requests that those routes receive. Whether that means adjusting something in the model, serving up some data to be consumed elsewhere in your app, on a different platform, or by a different developer. It is a very flexible means to build unique, useful and valuable applications across all media.

-Brad