MongoDB With Mongoose. Go Schemas!

I've recent started a new nodejs project. I'm a person that likes to experiment a lot. And I heard a lot of buzz around using Mongoose with the MEAN stack. So I've decided to give it a try. I already shipped a project with mongodb using rails and mongoid and I loved it. What I loved the most, was the way that mongoid integrates really well with all the rails stack.

Well nodejs is not like rails, while rails is a fullstack framework nodejs is a platform. So some features will be unfair to compare. I also choose to write the code for this new project with coffeescript. Which is a very nice language that compiles to javascript code. So let's get started.

I always create a json file that has all my configurations of my server. So I can easily switch across environments. Using ports, and different styles.

/* config/config.json */
{
  "development": {
    "mongo": {
      "url": "mongodb://localhost/playapp",
      "options": { 
        "server": { "poolSize": 5 }
      }
    }
  }
  "test": {
    "mongo": {
      "url": "mongodb://localhost/playapptest",
      "options": {
        "server": { "poolSize": 5 }
      }
    }
  }
}

Now that I have a nice place to store some config files.

# config/db.coffee
path = require 'path'  
mongoose = require 'mongoose'  
config = require path.join(__dirname, 'config')[process.env.NODE_ENV || 'development']

mongoose.connect  config.mongo.url, config.mongo.options

module.exports = mongoose  

We've got our file set and up for mongoose now is time to use mongoose for his only and one propose. Be a simple Object Document Model or ODM. Which will handle persistence, validations and a schema definition. To illustrate it better let's first write some tests for our app. To write the tests I'm using mocha, so let's start.

# spec/models/blogPost_spec.coffee
assert = require 'assert'  
path = require 'path'  
BlogPost = require path.join(__dirname, '../../models/blogPost')

describe 'BlogPost', ->  
  it 'saves the blog post', (done) ->
    blogPost = new BlogPost 
      title: 'My Title',
      content: 'blog post content'
    blogPost.save (err) ->
      assert !err

What happened there? It's simple, We build a document after the documentation of mongoose and we saved it. The save method expect a function which the first parameter is an error if it happens. So we called assert inverting the err message type it returns. If we run our tests it will fail. So it's time to create the code to pass it.

For that mongoose relies on a schema that has to be defined for our model. With this schema we will provide kind of a contract that our model will have to write to our mongo database.

# models/blogPost.coffee
path = require 'path'  
mongoose = require(path.join(__dirname, '../config/db'))

blogPostSchema = mongoose.Schema  
  title: String,
  content: String,
  pubDate: { type: Date, default: Date.now }

BlogPost = mongoose.model 'BlogPost', blogPostSchema  
module.exports = BlogPost  

Now our model is created, if we run our tests it will pass nicely. This is because we declared the mongoose schema with the content that we needed to save it properly. There's something strange in our schema declaration. We declared a json with title and content fields to be a String. But when I declared pubdate field I didn't pass a type like String, but a object. It is the kind of the flexibility that mongoose give us. If you need to have some operations, such as validations or default value. We can just pass an object with type being the actually type of the field and add what we need to validates, or set default values for our field. More information about the SchemaTypes can be found here in the documentation.

If we try to a field that is not declared at our schemas when we receive a callback it will not be show at the object that is written at the database. And it will also not be available at our database.

blogPost = new BlogPost  
  title: 'Blog Title'
  content: 'Blog Content'
  foo: 'Bar'

blogPost.save (err, post) ->  
  throw err if err
  # { title: 'Blog Title', content: 'Blog Content'}
  console.log(post) 

On the example above if we call post.foo we will have undefined. This is a nice feature to have in our System. And is definitely the power of having a Schema mechanism on our hands. But if you got to have the flexibility of mongodb in a field, just need to use the type Schema.Types.Mixed. Within this type you can write anything at that field and mongoose will persist it, even nested objects or arrays.

Time to validate

Build schemas for our models in mongodb is a nice feature. But the best part of having a schema is the ability to handle validations. For example, let's say neither our content or title can be blank. To solve this problem with mongoose all we need to do is add an object to the key to the field name and a object with the type of the schema and with the validation option.

blogPostSchema = mongoose.Schema  
  title: { type: String, required: true },
  content: { type: String, required: true },
  pubDate: { type: Date, default: Date.now }

Just with that required true, mongoose handle the field and does not save the object if the title is not valid for this operations. Returning an error at the save callback function. One can also pass a string instead of true to the required validation. That way when the error is returned it will return the string of your choice. Just be careful to pass the value PATH under brackets {PATH} to indicate the field name.

blogPostSchema = mongoose.Schema  
  title: { type: String, required: '{PATH} is required' },
  content: { type: String, required: required: '{PATH} is required' },
  pubDate: { type: Date, default: Date.now }

At the mongoose docs we can see others handful validations that are nice to use, such as match to restrict via Regex the field that will be saved and the min and max for restrict the range of a number.

To have those buildin validations are great. But the greatest thing is to have custom validations. One can Apply a validation just by declaring an Array in which the first parameter will be function that will handle the validation and the other parameter will be the text of the error message case this function returns false.

longerThanThree = (val) ->  
  val.length > 3

validatesLongerThanThree = [longerThanThree, '{PATH} is shorter than three']

blogPostSchema = mongoose.Schema  
  title: { type: String, validates: longerThenThree },
  content: { type: String, required: required: '{PATH} is required' },
  pubDate: { type: Date, default: Date.now }

This is nice way to handle error before acually writing at our database.

And finaly another good thing that I like about mongoose is the ability to create nested schemas. Everyone knows that mongodb has no support for relation between documents the same way that SQL databases does. But It's possible to embed a document inside another. And we can do that with mongoose just declare our nested schema inside another schema. To show it let's first create a Comment schema for our BlogPost.

# models/comment.coffee
path = require 'path'  
mongoose = require(path.join(__dirname, '../config/db'))

CommentSchema = mongoose.Schema  
  content: { type: String, required: true},
  pubDate: { type: Date, default: Date.now }

Comment = mongoose.model 'Comment', CommentSchema  
module.exports = Comment  

Now all we need to do is to embed this schema inside BlogPost like an array of comments.

# models/blogPost.coffee
path = require 'path'  
Comment = require path.join(__dirname, 'comment')

blogPostSchema = mongoose.Schema  
  title: { type: String, required: true },
  content: { type: String, required: true },
  pubDate: { type: Date, default: Date.now },
  comments: [Comment]

BlogPost = mongoose.model 'BlogPost', blogPostSchema  
module.exports = BlogPost  

Now we can create a test to see the embedded comment created.

# spec/models/blogPost_spec.coffee
assert = require 'assert'  
path = require 'path'  
BlogPost = require path.join(__dirname, '../../models/blogPost')

describe 'BlogPost', ->  
  it 'saves the blog post', (done) ->
      comments = [
      new Comment(content: 'Foo'),
      new Comment(content: 'Bar')
    ]
    blogPost = new BlogPost 
      title: 'My Title',
      content: 'blog post content'
      comments: comments
    blogPost.save (err, post) ->
      assert post.comments.length == 2
      assert !err

And if we run our tests it passes!

Mongoose is not the only thing that can handle mongodb operations in node the native mongo driver is pretty good. But of course it lacks schemas definition, and of course model validations which in my opinion is the killer feature of mongoose.