Building an API with Node, Express, and Mongoose

August 14, 2014

The first step in creating the example application is to create an API for the application to interact with. Node, Express, and Mongoose are used to create a REST API.

In addition to the example code below, the full code for the API is available.

Environment setup

To support the API, Node and MongoDB need to be installed. To install Node, I’d recommend using NVM to allow switching between different versions of Node (in the same way that RVM does for Ruby).

The Node packages used for the API are defined in the package.json file. Once Node is installed, run “npm install” from the API directory to get install all of the dependencies.

Exporting modules

All of the functions added to the API are exposed as Node modules. Exposing functions as modules follows the Node convention and allows functions to be made available using the same require syntax used to import Node modules. Exporting functions in modules involves defining named functions and then adding them to the exports collection.

function initialize(connectionString) {
  mongoose.connect(connectionString);
}
exports.initialize = initialize;

When needed, the modules are imported by requiring them.

var database = require("./database");
database.initialize("");

The server

Express provides wrappers around the basic Node structures that allow a lot of functionality to be enabled and setup with very little code.

var app = express();
var router = express.Router();
app.use("/", router);
app.listen(port);

Routing requests

A separate module is used to handle the requests for each of the main API endpoints (policies and claims). The express router is used to send the requests to the appropriate request handlers.

var PolicyHandler = require("./handlers/policyHandler");
function setupHandlers(router) {
  router.get("/", function(request, response) {
    response.json({ message: "Welcome to the API" });
  });
  router.get("/policies", function(request, response) {
    PolicyHandler.getAll(request, response);
  });
}

Data model

The data model for the API includes policies, claims, and issues. Policies have 0..n claims and claims have 0..n issues. The data model is described using the schema syntax of Mongoose. Mongoose allows the use of MongoDB as a back-end while providing a bit more structure around the format of the data allowed into the collections.

var PolicySchema = new Schema({
  policyNumber: String,
  firstName: String,
  lastName: String,
  validDate: Date,
  expirationDate: Date
});
var ClaimIssueSchema = new Schema({
  title: String,
  description: String
});
var ClaimSchema = new Schema({
  claimNumber: String,
  description: String,
  issues: [ClaimIssueSchema],
  policyNumber: String
 });

The database

The data for the API is stored in MongoDB. The API interacts with the MongoDB layer using Mongoose.

In addition to the schema syntax listed above, the models are made available by exporting the model.

module.exports = mongoose.model("Claim", ClaimSchema);

The model is used to query for documents or to save new documents.

var Claim = require("../models/claim");
Claim.find(function(err, claims) { /* ... */ });
Claim.findOne({"claimNumber": claimNumber}, function(err, claim) { /* ... */ });
var newClaim = new Claim();
newClaim.save();

API data format

The endpoints expose the data in a similar format to the schemas used for the database. However, the Mongo specific fields (added by Mongoose) are stripped from the data. This provides a couple of advantages: allowing consumers of the API to use a natural key rather than needing to construct a proper BSON ID string; and hiding the use of Mongo as a data store.

Handling requests

Requests are handled by setting information on the response. For this API, successful responses are handled by writing JSON formatted objects to the response. Exceptions are handled by setting the error status and writing the exception message to the response.

function getAll(request, response) {
  Claim.find(function(err, claims) {
    if (err) {
      response.status(400).send(err);
    } else {
      response.json(Common.toBasicCollection(claims));
    }
  });
}

For operations that take input (such as a search or creating a new item), the input is available in the request body. By setting up bodyParser when creating the application, both URL and JSON formatted request body information is available.

app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
function search(request, response) {
  var searchObject = request.body;
  Claim.find(searchObject, function(err, claims) {
    if (err) {
      response.status(400).send(err);
    } else {
      response.json(Common.toBasicCollection(claims));
    }
  });
}

Tying the pieces together

With the various components specified in separate modules, the index.js file (the target of Node invocations) is fairly small and straightforward.

var server = require("./server");
var handlers = require("./handlers");
var database = require("./database");
var port = 9090;
var connectionString = "mongodb://localhost/example";
database.initialize(connectionString);
server.start(port, handlers.setupHandlers);

Running the server

Running “node index.js” from the api/src directory starts the Node server.

Interacting with the API

A Postman collection file is available in the api directory. You can import the file into Postman and used the defined methods to explore with the API by interacting with it.

Also, some basic data is available in the data folder. The base data can be loaded into MongoDB from the Mongo shell.

load('clear_data.js');
load('base_data.js');