Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Introducing Mongoose to Your Node.js and Restify API

DZone's Guide to

Introducing Mongoose to Your Node.js and Restify API

Learn how using a tool such as Mongoose can help you simplify writing MongoDB functionality as a layer on top of your API.

· Database Zone
Free Resource

Traditional relational databases weren’t designed for today’s customers. Learn about the world’s first NoSQL Engagement Database purpose-built for the new era of customer experience.

This post is a sequel to Getting Started With MongoDB, Node.js, and Restify. We’ll now guide you through the steps needed to modify your API by introducing Mongoose. If you have not yet created the base application, please head back and read the original tutorial.

In this post, we’ll do a deep dive into how to integrate Mongoose, a popular ODM (Object -Document Mapper) for MongoDB, into a simple Restify API. Mongoose is similar to an ORM (Object-Relational Mapper) you would use with a relational database. Both ODMs and ORMs can make your life easier with built-in structure and methods. The structure of an ODM or ORM will contain business logic that helps you organize data. The built-in methods of an ODM or ORM automate common tasks that help you communicate with the native drivers, which helps you work more quickly and efficiently.

All of that said, the beauty of a tool like MongoDB is that ODMs are more of a convenience, as compared to how ORMs are essential for an RDBMS. MongoDB has many built-in features for helping you organize, analyze and keep track of your data. In order to harness the added structure and logic that an ODM like Mongoose offers, we are going to show you how to incorporate it into your API.

Mongoose is an ODM that provides a straightforward and schema-based solution for modeling your application data on top of MongoDB’s native drivers. It includes built-in type casting, validation (which enhances MongoDB’s native document validation), query building, hooks, and more.

Note: If you’d like to jump ahead without following the detailed steps below, the complete git repo for this tutorial can be found on GitHub.

Prerequisites

To get up to speed, let’s make sure that you are all set with the following prerequisites:

  • An understanding of the original API.
  • The latest version of Node.js (currently at v8.1.4).
  • A Mac (OSX, macOS, etc. as this tutorial does not cover Windows or Linux).
  • git (installed by default on macOS).

Getting Started 

This post assumes that you have the original codebase from the previous blog post. Please follow the instructions below to get up and running. I’ve included commands to pull in the example directory from the first post.

$ git clone git@github.com:nparsons08/mongodb-node-restify-api-part-1.git
$ cp -R mongodb-node-restify-api-part-1 mongodb-node-restify-api-part-2
$ cd mongodb-node-restify-api-part-2 && npm install

With the third command above, you have successfully copied the initial codebase into its own directory, which enables us to start the migration. To view the directories on your system, use $ls. You should see the following output:

$ mongodb-node-restify-api-part-1 mongodb-node-api-restify-part-2

Move into the new directory with the cd command and let’s begin the migration from the raw MongoDB driver to Mongoose:

$ cd mongodb-node-restify-api-part-2

New Dependencies

We’ll need to install additional dependencies in order to add the necessary functionality. Specifically, we’ll be adding the mongoose and mongoose-timestamp plugins to generate/store createdAt and updatedAt timestamps (we’ll touch more on Mongoose plugins later in the post).

$ npm install --save mongoose mongoose-timestamp

Since we’re moving away from the native MongoDB driver over to Mongoose, let’s go ahead and remove the dependency on the MongoDB driver using the npm command $ npm uninstall mongodb.

Now, if you view your package.json file, you will see the following JSON:

{
  "name": "rest-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Nick Parsons <nick.parsons@mongodb.com>",
  "license": "ISC",
  "dependencies": {
    "mongoose": "^4.11.1",
    "mongoose-timestamp": "^0.6.0",
    "restify": "^4.3.1"
  }
}

Mongoose Schemas and Models

When you’re developing an application backend using Mongoose, your document design starts with what is called a schema. Each schema in Mongoose maps to a specific MongoDB collection. With Mongoose schemas come models, constructors compiled from the schema definition. Instances of models represent a MongoDB document, which can be saved and retrieved from your database. All document creation and retrieval from MongoDB is handled by a specific model. It’s important to know that schemas are extremely flexible and allow for the same nested structure as the native MongoDB driver would support. Furthermore, schemas support business logic such as validation, pre/post hooks, plugins, and more — all of which is outlined in the official Mongoose guide.

In the following steps, we’ll be adding two schema definitions to our codebase and, in turn, we will import them into our API routes for querying and document creation. The first model will be used to store all user data and the second will be used to store all associated todo items. This will create a functional and flexible structure for our API.

Schema/Model Creation

Assuming you’re inside of the root directory, create a new directory called models with a user.js and todo.js file:

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

Next, let’s go ahead and modify our models/user.js and models/todo.js models. The model files should have the following contents:

models/user.js:

const mongoose   = require('mongoose'),
      timestamps = require('mongoose-timestamp')

const UserSchema = new mongoose.Schema({
email: {
type: String,
trim: true,
lowercase: true,
unique: true,
required: true,
},
name: {
first: {
type: String,
trim: true,
required: true,
},
last: {
type: String,
trim: true,
required: true,
},
},
}, { collection: 'users' })

UserSchema.plugin(timestamps)

module.exports = exports = mongoose.model('User', UserSchema)

models/todo.js:

const mongoose   = require('mongoose'),
      timestamps = require('mongoose-timestamp')

const TodoSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        index: true,
        required: true,
},
todo: {
type: String,
trim: true,
required: true,
},
status: {
        type: String,
        enum: [
            'pending',
'in progress',
            'complete',
        ],
        default: 'pending',
    },
}, { collection: 'todos' })

TodoSchema.plugin(timestamps)

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

Note: We’re using the mongoose-timestamp plugin by calling SchemaName.plugin(timestamps). This allows us to automatically generate createdAt and updatedAt timestamps and indexes without having to add additional code to our schema files. A full breakdown on schema plugins can be found here.

Route Creation

The /routes  directory will hold our user.js and todo.js files. For the sake of simplicity, you can copy and paste the following file contents into your todo.js file and overwrite the previous code. If you compare the two files, you’ll notice there is a slight change in the way that we call MongoDB using Mongoose. Specifically, Mongoose is playing as a role of abstraction over our database model, piping operations to the native MongoDB driver with validation in between. Lastly, we’ll need to create a new file called user.js$ cd ../routes.

Then create the routes/user.js file: $ touch user.js.

routes/user.js:

const User = require('../models/user'),
  Todo = require('../models/todo')

module.exports = function(server) {

/**
 * Create
 */
server.post('/users', (req, res, next) => {

let data = req.body || {}

User.create(data)
.then(task => {
res.send(200, task)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * List
 */
server.get('/users', (req, res, next) => {

let limit = parseInt(req.query.limit, 10) || 10, // default limit to 10 docs
            skip  = parseInt(req.query.skip, 10) || 0, // default skip to 0 docs
            query = req.query || {}

        // remove skip and limit from query to avoid false querying
        delete query.skip
        delete query.limit

    User.find(query).skip(skip).limit(limit)
.then(users => {
res.send(200, users)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * Read
 */
server.get('/users/:userId', (req, res, next) => {

User.findById(req.params.userId)
.then(user => {
res.send(200, user)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * Update
 */
server.put('/users/:userId', (req, res, next) => {

let data = req.body || {},
opts = {
                new: true
}

User.findByIdAndUpdate({ _id: req.params.userId }, data, opts)
.then(user => {
res.send(200, user)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * Delete
 */
server.del('/users/:userId', (req, res, next) => {

const userId = req.params.userId

User.findOneAndRemove({ _id: userId })
.then(() => {

// remove associated todos to avoid orphaned data
Todo.deleteMany({ _id: userId })
.then(() => {
res.send(204)
next()
})
.catch(err => {
res.send(500, err)
})
})
.catch(err => {
res.send(500, err)
})

})

}

routes/todo.js:

const Todo = require('../models/todo')

module.exports = function(server) {

/**
 * Create
 */
server.post('/users/:userId/todos', (req, res, next) => {

let data = Object.assign({}, { userId: req.params.userId }, req.body) || {}

Todo.create(data)
.then(task => {
res.send(200, task)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * List
 */
server.get('/users/:userId/todos', (req, res, next) => {

let limit = parseInt(req.query.limit, 10) || 10, // default limit to 10 docs
            skip  = parseInt(req.query.skip, 10) || 0, // default skip to 0 docs
query = req.params || {}

        // remove skip and limit from data to avoid false querying
        delete query.skip
        delete query.limit

    Todo.find(query).skip(skip).limit(limit)
.then(tasks => {
res.send(200, tasks)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * Get
 */
server.get('/users/:userId/todos/:todoId', (req, res, next) => {

Todo.findOne({ userId: req.params.userId, _id: req.params.todoId })
.then(todo => {
res.send(200, todo)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * Update
 */
server.put('/users/:userId/todos/:todoId', (req, res, next) => {

let data = req.body || {},
opts = {
                new: true
}

Todo.update({ userId: req.params.userId, _id: req.params.todoId }, data, opts)
.then(user => {
res.send(200, user)
next()
})
.catch(err => {
res.send(500, err)
})

})

/**
 * Delete
 */
server.del('/users/:userId/todos/:todoId', (req, res, next) => {

Todo.findOneAndRemove({ userId: req.params.userId, _id: req.params.todoId })
.then(() => {
res.send(204)
next()
})
.catch(err => {
res.send(500, err)
})

})

}

Entry Point

Our updated entry point for this API is in /index.js. Your /index.js file should mirror the following:

/**
 * Module Dependencies
 */
const restify = require('restify'),
      mongoose = require('mongoose')

/**
 * Config
 */
const config = require('./config')

/**
 * Initialize Server
 */
const server = restify.createServer({
    name    : config.name,
    version : config.version
})

/**
 * Bundled Plugins (http://restify.com/#bundled-plugins)
 */
server.use(restify.jsonBodyParser({ mapParams: true }))
server.use(restify.acceptParser(server.acceptable))
server.use(restify.queryParser({ mapParams: true }))
server.use(restify.fullResponse())

/**
 * Start Server, Connect to DB & Require Route Files
 */
server.listen(config.port, () => {

/**
 * Connect to MongoDB via Mongoose
 */
const opts = {
    promiseLibrary: global.Promise,
    server: {
        auto_reconnect: true,
        reconnectTries: Number.MAX_VALUE,
        reconnectInterval: 1000,
    },
    config: {
        autoIndex: true,
    },
}

mongoose.Promise = opts.promiseLibrary
mongoose.connect(config.db.uri, opts)

const db = mongoose.connection

db.on('error', (err) => {
    if (err.message.code === 'ETIMEDOUT') {
        console.log(err)
        mongoose.connect(config.db.uri, opts)
    }
})

db.once('open', () => {

require('./routes/user')(server)
require('./routes/todo')(server)

console.log(`Server is listening on port ${config.port}`)

})

})

Starting the Server

Now that we’ve modified the code to use Mongoose, let’s go ahead and run the $ npm start command from your terminal.

Assuming all went well, you should see the following output:

Server is listening on port 3000

Using the API 

The API is almost identical to the API written in the "getting started" post. However, in this version, we have introduced the concept of “users” who are owners of todo items. I encourage you to experiment with the new API endpoints using Postman to better understand the API endpoint structure. For your convenience, below are the available calls (cURL) for your updated API endpoints: 

User Endpoints

CREATE:

curl -i -X POST http://localhost:3000/users -H 'content-type: application/json' -d '{ "email": "nick.parsons@mongodb.com", "name": { "first": "Nick", "last": "Parsons" }}'

LIST:

curl -i -X GET http://localhost:3000/users -H 'content-type: application/json'

READ:

curl -i -X GET http://localhost:3000/users/$USER_ID -H 'content-type: application/json'

UPDATE:

curl -i -X PUT http://localhost:3000/users/$USER_ID -H 'content-type: application/json' -d '{ "email": "nick.parsons@10gen.com" }'

DELETE:

curl -i -X DELETE http://localhost:3000/users/$USER_ID -H 'content-type: application/json'

Todo Endpoints

CREATE:

curl -i -X POST http://localhost:3000/users/$USER_ID/todos -H 'content-type: application/json' -d '{ "todo": "Make a pizza!" }'

LIST:

curl -i -X GET http://localhost:3000/users/$USER_ID/todos -H 'content-type: application/json'

READ:

curl -i -X GET http://localhost:3000/users/$USER_ID/todos/$TODO_ID -H 'content-type: application/json'

UPDATE:

curl -i -X PUT http://localhost:3000/users/$USER_ID/todos/$TODO_ID -H 'content-type: application/json' -d '{ "status": "in progress" }'

DELETE:

curl -i -X DELETE http://localhost:3000/users/$USER_ID/todos/$TODO_ID -H 'content-type: application/json'

Note: The $PARAM_ID requirement in the URL denotes that the URL parameter should be replaced with a value. In our case, it will likely be a MongoDB ObjectId.

Final Thoughts

I hope this short tutorial on adding Mongoose to your API was helpful for future development. Hopefully, you noticed how using a tool like Mongoose can simplify writing MongoDB functionality as a layer on top of your API.

As Mongoose is only a single addition to keep in mind as you develop and hone your API development skills, we’ll continue to release more posts with other examples and look forward to hearing your feedback. If you have any questions or run into issues, please comment below.

In my next post, I’ll show you how to create a similar application from start to finish using MongoDB Stitch, our new Backend as a Service. You'll get to see how abstracting away this API in favor of using Stitch will make it easier to add additional functionality such as database communication, authentication, and authorization, so you can focus on what matters: The user experience on top of your API.

Learn how the world’s first NoSQL Engagement Database delivers unparalleled performance at any scale for customer experience innovation that never ends.

Topics:
database ,tutorial ,mongodb ,restify ,api ,node.js ,mongoose

Published at DZone with permission of Nick Parsons, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}