Part 2: Introducing Mongoose to Your Node.js and Restify API
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 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 to model 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
In order 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 the following command:
$ 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
mongoose
and the
mongoose-timestamp
plugin 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 following 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
",
"license": "ISC",
"dependencies": {
"mongoose": "^4.11.1",
"mongoose-timestamp": "^0.6.0",
"restify": "^4.3.1"
}
}
Mongoose Schemas & 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
, a constructor 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:
$ npm start
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.
July 21, 2017