Skip to content

Mongoose

What is Mongoose?

Mongoose is a Node.js-based Object Data Modeling (ODM) library on top of the MongoDB native driver for Node.js projects. It manages relationships between data, provides schema validation and it is used to translate between objects in code and the representation of those objects in MongoDB.

You can install Mongoose to your Node.js project with npm:

1
npm i mongoose

And remember use require() in your JavaScript codes:

1
const mongoose = require('mongoose');

Connection to MongoDB

Mongoose requires a connection to a MongoDB database. You can find your MongoDB connection string from MongoDB Atlas. Select Connect Your Application and find your connection string.

!Image 04

In a below you can see one example connection string from MongoDB Compass to MongoDB Atlas Cloud (Persons database).

1
mongodb+srv://pasi:PASSWD@democluster.brlob.mongodb.net/Persons

You should use dotenv module to store your connection string to .env file. Add your MongoDB connection string to your .env file.

.env
1
MONGODB = mongodb+srv://pasi:PASSWD@democluster.brlob.mongodb.net/Persons

You can connect your app to MongoDB Atlas database with mongoose.connect() command. By pass the deprecation warnings with useNewUrlParser and useUnifiedTopology. Read more here: Deprecation Warnings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// import dotenv and mongoose modules
require('dotenv').config()
const mongoose = require("mongoose")

// set up default mongoose connection
mongoose.connect(process.env.MONGODB, 
  { useNewUrlParser: true, useUnifiedTopology: true })

// get the default connection
const db = mongoose.connection

// bind connection to error event to get notification of connection errors
db.on("error", console.error.bind(console, "MongoDB connection error:"))
// connnection made successfully
db.once('open', function() {
  console.log("Database successfully connected")
})

Schema and Model

Models are defined using the Schema interface. The Schema allows you to define the fields stored in each document along with their validation requirements and default values. Schemas are compiled into models using the mongoose.model(). Once you have a model you can use it to find, create, update, and delete objects of the given type to your database.

If you want to create for example a persons collection to MongoDB with a following data (what we have used earlier):

example person document
1
2
3
4
5
{
  "name": "Kirsi Kernel",
  "email": "kirsi@kernel.com",
  "age": 55
}

You can define a new PersonSchema which will define needed data fields.

schema (basic)
1
2
3
4
5
const personSchema = new mongoose.Schema({
  name: String, 
  email: String,
  age: Number
})

You should check Mongoose validation syntax to learn more how to create powerful schemas. You can modify above schema for example like this.

schema (more details)
1
2
3
4
5
const personSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: String,
  age: { type: Number, min: 0 }
})

Now person name is required and age need to be zero or more. When data is inserted to database, mongoose will use build-in-validators to check that data is correct. If is not, data saving operation will throw an exception.

A new model PersonModel can be created with above personSchema.

model
1
const Person = mongoose.model('Person', personSchema)

Later the model is used to create a person object.

person
1
2
3
4
5
const person = new Person({
  name: 'Kirsi Kernel',
  email: 'kirsi@kernel.com',
  age: 55,
})

Modifying MongoDB (CRUD)

Creating

A new document can be done by using save() method. This method will return a promise and in the callback function we do a basic logging and close the connection.

1
2
3
4
person.save().then(response => {
  console.log('person saved!')
  mongoose.connection.close()
})

Reading

Documents can be found with a find() method. Now above created Person model can be used to find persons from the Persons collecions. Use find() method with different search criterias inside {}.

1
2
3
4
5
6
Person.find({}).then(result => {
  result.forEach(person => {
    console.log(person)
  })
  mongoose.connection.close()
})

Updating

You can use for example findByIdAndUpdate method to find person and update it's fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// create a person with new data
const person = {
  name: newName,
  age: newAge,
  email: newEmail
}
// find and update, create a new one if id not found
Person.findByIdAndUpdate(id, person, { new: true }).then(response => {
  console.log('person updated!')
  mongoose.connection.close()
})

Deleting

You can use for example findByIdAndRemove method to find person and delete it.

1
2
3
4
Person.findByIdAndRemove(id).then(response => {
  console.log('person deleted!')
  mongoose.connection.close()
})

Mongoose Validation

Mongoose Validation is a customisable middleware that gets defined inside the SchemaType of mongoose schema. It is automatically used before a document is saved in the MongoDB. Validation can also be run manually using validate(callback) or validateSync() functions.

Example:

Person Schema
1
2
3
4
5
const personSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: String,
  age: { type: Number, min: 0 }
})
1
2
3
const Person = mongoose.model('Person', personSchema);
const person = new Person({});
await person.save();

Now person object doesn't have any fields and this will give a following error:

1
Error: Person validation failed: name: Path `name` is required.

You can also modify to add a custom error messages:

Person Schema
1
2
3
4
5
const personSchema = new mongoose.Schema({
  name: { type: String, required: [true, 'Name is required!'] },
  email: String,
  age: { type: Number, min: 0 }
})
1
Error: Person validation failed: name: Name is required!

Try to user correct name value and set incorrect agevalue:

1
2
3
const Person = mongoose.model('Person', personSchema);
const person = new Person({ "name": "Kirsi Kernel", "age":-100 });
await person.save();
1
Error: Person validation failed: age: Path `age` (-100) is less than minimum allowed value (0).

You can also use enumerations in schema:

Person Schema
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const personSchema = new mongoose.Schema({
  name: { type: String, required: [true, 'Name is required!'] },
  email: String,
  age: { type: Number, min: 0 },
  company: {
    type: String,
    required: true,
    enum: {
      values: ['JAMK', 'JYU'],
      message: '{VALUE} not available',
    }
})
1
2
3
const Person = mongoose.model('Person', personSchema);
const person = new Person({ "name": "Kirsi Kernel", "age":00, "company":"Nokia" });
await person.save();
1
Error: Person validation failed: company: Nokia not available

Look more validation examples from Mongoose Validation materials.

Populate

Mongoose has a more powerful alternative called populate(), which lets you reference documents in other collections. Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s). We may populate a single document, multiple documents, a plain object, multiple plain objects, or all objects returned from a query.

Let's look at some examples.

  • Todo schema/model with text
schema/model - todo
1
2
3
4
const todoSchema = new mongoose.Schema({
  text: { type: String, required: true }
})
const Todo = mongoose.model('Todo', todoSchema, 'populate_todos');
  • Person schema/Model with name and array
  • Array contains mongoose.Schema.Types.ObjectId which ref is Todo.

So, now Person can have multiple todos.

schema/model - person
1
2
3
4
5
const personSchema = new mongoose.Schema({
  name: { type: String, required: true },
  todos: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Todo' } ]
})
const Person = mongoose.model('Person', personSchema, 'populate_todos');

Let's create a person and a few todos. Person and Todo models are created with a some default values. Todos are added to Person's todos with findByIdAndUpdate function.

index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const createData = async () => {
  try {
    // create person 
    var person = new Person({ "name":"Kirsi Kernel"});
    // save person to get _id
    person = await person.save();
    id = person._id.valueOf();
    console.log(id);
    // create an todos
    const todo1 = new Todo({ "text":"Learn JavaScript!" });
    await todo1.save();
    const todo2 = new Todo({ "text":"Learn Mongoose!" });
    await todo2.save();
    // find a person and add a new todo
    await Person.findByIdAndUpdate(
      person._id,
      { $push: { todos: todo1._id } },
      { new: true  }
    );
    // find a person and add a new todo
    await Person.findByIdAndUpdate(
      id,
      { $push: { todos: todo2._id } },
      { new: true  }
    );
  } catch (error) {
    console.log(error);
  }
}

createData();

A following person data will be visible in MongoDB.

populate 01

A following todo data will be visible in MongoDB.

populate 01

If you now try to find a person without populate() function:

1
2
const person = await Person.findById(id);
console.log('person', person);

You will get todo's only like a MongoDB ObjectId's.

1
2
3
4
5
6
7
8
9
{
  _id: new ObjectId("63e0c6f3328a03cbf2e2b8aa"),
  name: 'Kirsi Kernel',
  todos: [
    new ObjectId("63e0c6f4328a03cbf2e2b8ad"),
    new ObjectId("63e0c6f4328a03cbf2e2b8af")
  ],
  __v: 0
}

But, if you use populate() in your query, then those todos are populated to returned object:

1
2
const person = await Person.findById(id).populate('todos');
console.log('person', person);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  _id: new ObjectId("63e0c6f3328a03cbf2e2b8aa"),
  name: 'Kirsi Kernel',
  todos: [
    {
      _id: new ObjectId("63e0c6f4328a03cbf2e2b8ad"),
      text: 'Learn JavaScript!',
      __v: 0
    },
    {
      _id: new ObjectId("63e0c6f4328a03cbf2e2b8af"),
      text: 'Learn Mongoose!',
      __v: 0
    }
  ],
  __v: 0
}

Read More

Goals of this topic

Understand

  • Basics of the Mongoose.