MongoDB is one of the most popular NoSQL databases in the world. While it is easy to use and understand compared to SQL databases, it is a schemaless database.

What is a schema?

A "Schema" for a database can be compared with the "Class" in object-oriented programming. Just like a class that provides a blueprint for creating objects in a program, a schema provides a blueprint for creating objects (called documents in MongoDB) in a database.

In a Node.js application, we can use the Mongoose object data modeling (ODM) tool to define a schema for a MongoDB collection. A Mongoose schema defines the document structure, document properties, default values, validators, static methods, virtuals, etc.

Installing Mongoose

Before we get started with Mongoose schemas, make sure that you have already installed and configured both Node.js and MongoDB (instructions for Mac and Ubuntu).

You can then install Mongoose in your Node.js project with the following command:

$ npm install mongoose --save

Now you can require Mongoose in your application:

const mongoose = require('mongoose')

Take a look at this article to learn more about getting started with Mongoose in Node.js.

Defining a schema

Everything in Mongoose starts with defining a schema. It maps directly to a MongoDB collection and defines the document structure within that collection.

Let us look at the below example that defines a schema for the blog post:

const mongoose = require('mongoose')
const { Schema } = mongoose

const postSchema = new Schema({
  title: String,
  author: String,
  body: String,
  comments: [{ body: String, createdAt: Date }],
  published: {
    type: Boolean,
    default: false
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  meta: {
    upvotes: Number,
    bookmarks: Number
  }
})

Each property defined in the above blogSchema is cast to its associated schema type before storing it in the document. For example, the property author will be cast to the String data type, and the property createdAt will be cast to a Date schema type.

Notice above that only the schema type is required for each property. It can be defined using a shorthand notation (without the type attribute) or an object notation. If you choose to use the object notation, you must define the type attribute.

Mongoose allows the following schema types:

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array
  • Decimal128
  • Map

A key in a Mongoose schema can also be assigned a nested schema containing its own keys and types, like the meta property above. It can even be an array of a nested schema like the comments property.

Creating a model

A Mongoose model is a compiled version of the schema definition that maps directly to a single document in the collection.

To create a model in Mongoose, you use the mongoose.model() method with the model name as the first parameter and the schema definition as the second parameter:

const Post = mongoose.model('Post', postSchema)

Now the Post model is ready for querying, creating, updating, and removing documents from the posts collection in MongoDB:

const post = new Post({
  title: 'Mongoose Introduction',
  author: 'Atta'
})

post.title      // Mongoose Introduction
post.author     // Atta
post.published  // false

Overriding the _id property

By default, Mongoose automatically adds the _id property to every schema definition:

const user = new Schema({ name: String })

user.path('_id') // ObjectId { path: '_id', instance: 'ObjectID', ...}

When you create a new document, a unique value is auto-generated by Mongoose and saved in the _id property.

However, you can override Mongoose's default _id with your own _id property:

const birdSchema = new Schema({ _id: Number, name: String })
const Bird = mongoose.model('Bird', birdSchema)

Make sure you assign a value to _id when saving the document. Otherwise, Mongoose will refuse to save the document and throw an error:

const bird = new Bird({ name: 'Sparrow' })
await bird.save() // Throws "document must have an _id before saving"

// Set `_id` field before calling `save()`
bird._id = 45
await bird.save()

Validation

Mongoose allows you to define validation constraints in your schema. For example, let us say you want to make sure that every employee must have a name property and a unique ssn property in the employees collection. You can make the name field required, and the ssn filed unique as shown below:

const empSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  ssn: {
    type: String,
    unique: true
  },
  age: Number
})

const Employee = mongoose.model('Employee', empSchema)

const emp = new Employee({ age: 45 })

await emp.save() // Throws "Path `name` is required."

Instance methods

Each instance of a Mongoose model is called a document. Documents have their own built-in instance methods. You can also define your own custom instance methods:

const foodSchema = new Schema({
  name: String,
  type: String
})

// Define custom method
foodSchema.methods.similarFoods = function (cb) {
  return mongoose.model('Food').find({ type: this.type }, cb)
}

const Food = mongoose.model('Food', foodSchema)

Now each instance of Food will have access to the similarFoods() method:

const food = new Food({ name: 'Pizza', type: 'Fast Food' })

food.similarFoods((err, foods) => {
  console.log(foods)
})

Static methods

You can also add static methods to your model by using the statics field:

// Define static method
foodSchema.statics.findByName = function (name) {
  return this.find({ name: new RegExp(name, 'i') })
}

const Food = mongoose.model('Food', foodSchema)

const foods = await Food.findByName('Pizza')

Query helpers

Query helpers are like instance methods but only for Mongoose queries. You can define query helper functions to extend the Mongoose query building API:

// Define query helper
foodSchema.query.byName = function (name) {
  return this.where({ name: new RegExp(name, 'i') })
}

const Food = mongoose.model('Food', foodSchema)

Food.find()
  .byName('burger')
  .exec((err, foods) => {
    console.log(foods)
  })

Indexes

With Mongoose, you can define indexes within your schema at the path level or the schema level. Schema-level indexes are usually required when you create compound indexes:

const postSchema = new Schema({
  title: String,
  author: String,
  body: String,
  tags: {
    type: [String],
    index: true // Path level index
  }
})

// Schema level compound index
postSchema.index({ name: 1, author: -1 })

Virtuals

Virtuals are document properties that you can get and set, but they do not get persisted to MongoDB. These properties are commonly used for formatting or combining fields as well as for splitting a single value into multiple values for storage.

Let us look at the following user's schema:

const userSchema = new Schema({
  name: {
    first: String,
    last: String
  },
  age: Number
})

const User = mongoose.model('User', userSchema)

const john = new User({
  name: { first: 'John', last: 'Doe' },
  age: 32
})

console.log(`${john.name.first} ${john.name.last}`) // John Doe

Instead of concatenating first and last names every time, we can want to create a virtual property called fullName that returns the full name:

userSchema.virtual('fullName').get(function () {
  return `${this.name.first} ${this.name.last}`
})

Now whenever you access the fullName property, Mongoose will call your virtual method:

console.log(john.fullName)    // John Doe

Take a look at this article to learn more about Mongoose virtuals.

Aliases

Mongoose uses aliases to get and set another property value while saving the network bandwidth. It allows you to convert a short name of the property stored in MongoDB into a longer name for code readability:

const userSchema = new Schema({
  n: {
    type: String,
    alias: 'name'
  }
})

const User = mongoose.model('User', userSchema)

const user = new User({ name: 'Alex' })
console.log(user) // { n: 'Alex' }
console.log(user.n) // Alex

user.name = 'John'
console.log(user.n) // John

Options

The Schema constructor takes a second options parameter that you can use to configure the schema. For example, we can disable the _id attribute completely using the { _id: false } schema option:

const userSchema = new Schema(
  {
    name: String
  },
  { _id: false }
)

const User = mongoose.model('User', userSchema)

const user = new User({ name: 'Atta' })
console.log(user) // { name: 'Atta' }

You can find a complete list of schema options on the Mongoose documentation.

✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.