# Introduction to Mongoose Schemas

By Atta Ur Rehman Shah (https://attacomsian.com/about). Published Dec 13, 2020, updated Oct 1, 2022. Topics: Node.js, Mongoose, MongoDB.
Canonical URL: https://attacomsian.com/blog/mongoose-schemas

> A detailed article to learn about Mongoose schemas and how to define a model, virtual and static methods, indexes, query helpers, aliases, and more.

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](https://attacomsian.com/blog/install-nodejs-npm-ubuntu) and MongoDB (instructions for [Mac](https://attacomsian.com/blog/install-mongodb-macos) and [Ubuntu](https://attacomsian.com/blog/install-mongodb-ubuntu-18-04)).

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

```bash
$ npm install mongoose --save
```

Now you can require Mongoose in your application:

```javascript
const mongoose = require('mongoose')
```

Take a look at [this article](https://attacomsian.com/blog/nodejs-getting-started-with-mongoose) 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:

```javascript
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](https://attacomsian.com/blog/mongoose-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](https://attacomsian.com/blog/mongoose-models) 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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
// 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](https://attacomsian.com/blog/nodejs-getting-started-with-mongoose#query-building) API:

```javascript
// 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:

```javascript
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:

```javascript
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:

```javascript
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:

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

Take a look at [this article](https://attacomsian.com/blog/mongoose-virtuals) 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:

```javascript
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:

```javascript
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](https://mongoosejs.com/docs/guide.html#options) on the Mongoose documentation.
