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.