Introduction to Mongoose Subdocuments

In Mongoose, subdocuments are documents that are embedded in other documents. This means that you can nest schemas in other schemas.

Mongoose has two distinct concepts of subdocuments: arrays of subdocuments and single nested subdocuments:

const childSchema = new Schema({
  firstName: String,
  lastName: String
})

const parentSchema = new Schema({
  // Array of subdocuments
  children: [childSchema],

  // Single subdocument
  child: childSchema
})

Just like a normal document, a subdocument can have middleware, custom validations, virtuals, and any other feature available to top-level schemas:

childSchema.virtual('fullName').get(function () {
  return this.firstName + ' ' + this.lastName
})

childSchema.pre('save', function (next) {
  if (this.firstName === 'john') {
    return next(new Error(`John is not allowed`))
  }
  next()
})

The main difference is that subdocuments are not saved separately. Instead, they are saved whenever their top-level parent document is saved.

Creating subdocuments

There are two ways to create documents that contain nested documents:

  1. Passing a nested object into the constructor of the parent document
  2. Adding a nested object into an already created document using array's methods like push and unshift

Let us assume that we have got the following order schema that contains an array of nested line item schemas:

const orderSchema = new Schema({
  ref: String,
  lineItems: [
    new Schema({
      name: String,
      price: Number,
      qty: Number,
      total: Number
    })
  ]
})

const Order = mongoose.model('Order', orderSchema)

1. Passing a nested object to the constructor

For this method, we create a nested object that contains both order's attributes and line items:

const orderObj = new Order({
  ref: 'QR34',
  lineItems: [
    {
      name: 'T-shirt',
      price: 6,
      qty: 2,
      total: 12
    },
    {
      name: 'Jeans',
      price: 15,
      qty: 1,
      total: 15
    }
  ]
})

const order = await orderObj.save()
console.log(order)

Here is what the complete parent document looks like:

{
  _id: '6277ddade65b3236b1eb65d5',
  ref: 'QR34',
  lineItems: [
    {
      _id: '6277ddade65b3236b1eb65d6',
      name: 'T-shirt',
      price: 6,
      qty: 2,
      total: 12
    },
    {
      _id: '6277ddade65b3236b1eb65d7',
      name: 'Jeans',
      price: 15,
      qty: 1,
      total: 15
    }
  ]
}

2. Adding subdocuments to an existing document

For this method, we first create the top-level parent document:

const orderObj = new Order({
  ref: 'QR34'
})

const order = await orderObj.save()
console.log(order)
// { _id: 6277dfd363adcf3d99012599, ref: 'QR34', lineItems: [] }

Then, we edit the order to add line items:

order.lineItems.push({
  name: 'T-shirt',
  price: 6,
  qty: 2,
  total: 12
})
order.lineItems.push({
  name: 'Jeans',
  price: 15,
  qty: 1,
  total: 15
})

await order.save()

You can also create a subdocument by using the create() method of the document array:

await order.lineItems.create({ name: 'Jeans', price: 15, qty: 1, total: 15 })

Querying subdocuments

By default, each subdocument has its own _id attribute. Mongoose provides a special id() method for scanning a document array to find a document with a given _id:

const doc = order.lineItems.id(`6277ddade65b3236b1eb65d6`)
console.log(doc)
// {
//     _id: 6277ddade65b3236b1eb65d6,
//     name: 'T-shirt',
//     price: 6,
//     qty: 2,
//     total: 12
// }

Updating subdocuments

The simplest way to update subdocuments is:

  1. Use the findOne() or findById() to retrieve the parent document
  2. Retrieve the subdocument array
  3. Modify the array
  4. Call the save() method on the parent document to persist changes
const order = await Order.findOne({ ref: 'QR34' })

// Change T-Shirts Qty
const id = `6277ddade65b3236b1eb65d6`
order.lineItems.id(id).qty = 3
order.lineItems.id(id).total = 18

const updated = await order.save()

Deleting subdocuments

In Mongoose, Each subdocument has its own remove() method. For an array of subdocuments, this is equivalent to calling the pull() method on the subdocument:

const order = await Order.findOne({ ref: 'QR34' })

order.lineItems.id(`6277ddade65b3236b1eb65d6`).remove()
// OR
order.lineItems.pull(`6277ddade65b3236b1eb65d6`)

const updated = await order.save()

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

You might also like...

Digital Ocean

The simplest cloud platform for developers & teams. Start with a $200 free credit.

Buy me a coffee ☕

If you enjoy reading my articles and want to help me out paying bills, please consider buying me a coffee ($5) or two ($10). I will be highly grateful to you ✌️

Enter the number of coffees below:

✨ Learn to build modern web applications using JavaScript and Spring Boot

I started this blog as a place to share everything I have learned in the last decade. I write about modern JavaScript, Node.js, Spring Boot, core Java, RESTful APIs, and all things web development.

The newsletter is sent every week and includes early access to clear, concise, and easy-to-follow tutorials, and other stuff I think you'd enjoy! No spam ever, unsubscribe at any time.