In a “greenfield” project I contribute to, we use MongoDB. The core reason for this decision is future flexibility. A brand-new project like this contains a lot of unknowns! I’ll outline the problem we are solving.
Here, I assume you are familiar with databases and general coding concepts. I will not cover how to connect to and use MongoDB outside of the Subdocuments and Discriminators options defined below. If you need more of an introduction, these references from freeCodeCamp are both excellent: Mongoose 101 or Introduction to Mongoose for MongoDB.
This API proxy service will add a safety layer between the platform and a third-party vendor. Currently, we reach out directly to the vendor, causing multiple dependencies and a web of complicated actions which will need to be detangled and simplified.
Another long-term goal is to decouple our platform, allowing more vendor flexibility in the future. You never know when something will change with an API you don’t control. 😉
Why MongoDB - or why a NoSQL database - you might ask?

Although there is a level of flexibility in the various tables you can build and connect with relational databases and foreign keys, NoSQL databases are known for their flexible data structure capabilities. Ever-growing SQL tables leads to complicated queries and join tables to access scattered data. Those queries can become expensive.
I mentioned the possibility of future third-party vendor(s). Preliminary research highlighted that the data across other possible vendors is just different enough to need schema variation. Another win for MongoDB is that it can store multiple shapes of related data in the same collection! Don’t worry, we’ll cover this in more detail.
“Generally, in MongoDB, data that is accessed together should be stored together.” - Jesse Hall, MongoDB Developer Article
…and that’s the perfect segway to the crux of this topic.
In this post, we’ll cover:
❓ What is Mongoose?
📒 Mongoose Documents
📑 Mongoose Subdocuments
🙈🙉🙊 Mongoose Discriminators
🛠️ When and how to use these tools
❓ What is Mongoose?
To make MongoDB easier to work with, developers frequently use Mongoose. It’s not the only tool, and a tool is not required to work with MongoDB. But, tools provide structure and fluidity in my experience.
Mongoose is a popular third-party JavaScript library for Node.js. It helps to model, validate, and manipulate data along with plenty of other interesting capabilities. Mongoose is an ODM, or Object Data Modeling, library.

Mongoose provides more structure to developer interactions with MongoDB. The schema allows you to define the shape of your data and its expected types as well as additional options like default values, designated uniqueness, or indexing for example.
The model, on the other hand, applies your schema structure to each of the MongoDB documents. Models are then used for “CRUD” database actions on the records: creating, reading, updating, and deleting. — View the full list of Mongoose queries available.
To ensure our mental model is aligned, here’s an example of a base schema in JavaScript we will use to define a model we can work with:
import mongoose from 'mongoose';
const { Schema } = mongoose;
const foodBaseSchema = new Schema({
foodName: String,
foodColor: String
});
const foodBaseModel = mongoose.model('Food', foodBaseSchema);
📒 Mongoose Documents
A Mongoose document is a mapping of a model. Your model should match the pre-defined schema(s).
The Model class is a subclass of the Document class in the Mongoose implementation. When you use a Mongoose query, you are interacting with the Mongoose Document.
Let’s build on the above example as we go. Here’s the basic implementation of creating a Document with our above base model:
const foodDocument = new foodBaseModel();
/** Below is borrowed from the Mongoose documentation to aid in understanding how a Model is a subclass of a Document. **/
foodDocument instanceof foodBaseModel; // true
foodDocument instanceof mongoose.Model; // true
foodDocument instanceof mongoose.Document; // true
📑 Mongoose Subdocuments
A Subdocument represents a Document embedded inside another Document. In other words, a Subdocument can also be defined as a schema within another schema.
As we work with Subdocuments, you might notice that they look pretty similar to Documents. They are! The main difference is a Subdocument will be added to a “top-level” or “parent” schema and can only be accessed and interacted with alongside the parent schema. Additionally, if you use any of the built-in Mongoose middleware or validation options on the Subdocument, this will be performed before the Document to ensure everything is in order before proceeding at the top-level.
Meaning, Subdocuments are not stored in a separate table and accessed with a join table like when using a relational database. Subdocuments cannot exist without their parent — MongoDB stores all of this data in a single Document.
(That said, MongoDB does have the capability of a “join” view if you’re interested.)
Okay, great, now that we have a pretty solid understanding of Subdocuments, let’s expand the `foodBaseModel` with a couple of ways to add Subdocuments!
/** Let's define a schema for the ways we can cook said food item. **/
const cookSchema = new Schema({ type: String });
/** And we'll add a schema with data about how this item is grown. **/
const growSchema = new Schema({ detail: String });
const foodBaseModel = new Schema({
foodName: String,
foodColor: String,
// Array of Subdocuments to list the cooking types we could use
cook: [cookSchema],
// Single nested Subdocument to allow us to define a growth schema
grow: growSchema
});
Subdocuments and Nested Paths are Different
Before we move on, we should cover a common point of confusion. A nested path is not the same as a Subdocument, though they do look quite similar.
Try to keep an eye out for the subtle differences of this nested path example:
const nestedFoodSchema = new Schema({
foodName: String,
foodColor: String,
flavor: {
delicious: Boolean,
kidFriendly: Boolean
}
});
const nestedFoodModel = mongoose.model('Nested', nestedFoodSchema);
Although this might look close to what we did above, and they may look similar via MongoDB, Mongoose treats these differently.
A nested path like the above must be defined upon Document instantiation to be valid whereas in our earlier Subdocument example, we can set the `cook` field to `undefined` to start. We can then more easily alter the `cook` field when ready!
Subdocuments are nested Documents if you recall. Because of this, Mongoose gives each Subdocument an `_id` Document identifier making it searchable as a Subdocument or within it’s parent Document.
Although you can certainly use JavaScript methods on a subdocument or nested path object, nested paths do not allow you to take advantage of the built-in Mongoose methods to interact directly with a Document’s list of Subdocuments like our `cook` field.
If you need more clarification, I recommend referring to the Subdocument documentation.
🙈🙉🙊 Mongoose Discriminators
Discriminators essentially allow you to create schemas with varying object models to store within the same collection. This is an excellent option if you have a similar underlying schema structure, but you need slight differences.
When setting up a schema, an options object can be appended at the end as another parameter. In the case of Discriminators, a `discriminatorKey` in the options object is used with a value to define the Discriminator in the Documents. This key becomes searchable using the `__t` string path.
Once a Document is created with a Discriminator key, this key is not typically able to be updated by most methods. Though there are some update methods that use the `overwriteDiscriminatorKey` option to override this.
One other cool feature is that you can apply Discriminators to Subdocuments, too! Think about all the possibilities and flexibility your schema can have. 🤔
Let’s finish our example. First, we’ll revisit the base schema to add the `discriminatorKey` option:
/** Subdocument Schemas **/
const cookSchema = new Schema({ type: String });
const growSchema = new Schema({ detail: String });
const foodBaseModel = new Schema({
foodName: String,
foodColor: String,
cook: [cookSchema],
grow: growSchema
}, {
discriminatorKey: 'foodType'
});
const foodDocument = new foodBaseModel();
Then, we’ll create our separate schemas to add on top of our baseSchema with the `discriminatorKey`:
const vegetableModel = foodDocument.discriminator(
'vegetable',
new Schema({
isOrganic: Boolean,
colorVariations: [String]
}, {
discriminatorKey: 'foodType'
});
const grainModel = foodDocument.discriminator(
'grain',
new Schema({
isFresh: Boolean,
styleOptions: [String]
}, {
discriminatorKey: 'foodType'
});
And finally we create the new Documents (including the Subdocuments) for each Discriminator type:
const carrot = new vegetableModel({
foodName: 'carrot',
foodColor: 'orange',
cook: [
{ type: 'roast' },
{ type: 'bake' },
{ type: 'saute' }
],
grow: { detail: 'root' },
isOrganic: true,
colorVariations: [
'purple',
'white',
'red'
]
});
carrot.save();
// We now have a carrot Document!
const bread = new grainModel({
foodName: 'bread',
foodColor: 'brown',
cook: [
{ type: 'bake' },
{ type: 'fry' }
],
grow: { detail: 'cooked dough with a flour base' },
isFresh: true,
styleOptions: [
'croissant',
'italian',
'sourdough'
]
});
bread.save();
// We now have a bread Document!
🛠️ When and how to use these tools
Whether you use these options in a Mongoose project has the same answer heard frequently within the world of software engineering: “It depends.”
Should all Documents take advantage of Subdocuments if they have an associative object structure? No. Subdocuments are most useful for a specific schema to use and enforce for a nested object, the nested object should be searchable, or the option to leave the nested object structure off of the Document until a later time without having to define it up front could be used strategically.
And Discriminators? Yup - they’re not for every situation! I find Discriminators are best for unique situations. It’s not common to need slight variants on schemas. But, as I mentioned earlier, our case of slightly different data shapes for third-party vendor data is a great scenario for using the Discriminator option in Mongoose.
Personally, I’ve found that Mongoose has made development with MongoDB easy and enjoyable. The library is well-documented and both MongoDB and Mongoose have a great community with plenty of examples to draw from.
I hope you found the usage of Subdocuments and Discriminators by this non-relational database as interesting as I did!
Sources:
MongoDB Article: Getting Started with MongoDB and Mongoose
Mongoose Documentation: Documents | Subdocuments | Discriminators
Introduction to Mongoose for MongoDB and Mongoose 101 from freeCodeCamp
BONUS Recommendation: MongoDB Podcast
For more stuff from me, find me on LinkedIn / YouTube or catch what else I'm up to at mindi.omg.lol
Thanks for reading! Did I miss anything, or would you like to add anything? Let me know! I appreciate constructive feedback so we can all learn together. 🙌