Node.js 43 🟢 Connecting to MongoDB using Mongoose ODM
MongoDB stores documents, and Node.js works with objects. The two are close cousins: a MongoDB document is a BSON object, and a Node.js application is full of JavaScript objects. But the similarity is deceptive. MongoDB accepts any valid BSON document, so a collection can contain documents with different fields, different types, and different structures. For some applications, that flexibility is the point. For applications that need consistency — a user always has an email, an order always has a total — the flexibility becomes a liability. Mongoose is the Object Data Modeling (ODM) library that adds structure to the unstructured. It defines a schema, compiles the schema into a model, and enforces the schema’s rules at the application level before any document is written to the database .
The ODM is the document-database counterpart of the ORM. An ORM maps tables to classes and rows to instances. An ODM maps collections to models and documents to instances. Mongoose is not the only ODM for MongoDB, but it is the most widely used in the Node.js ecosystem, and it is the library that the MongoDB documentation recommends for schema-based modeling . The schema defines the fields, the types, the defaults, and the validators. The model is the compiled constructor that the application uses to create, query, update, and delete documents. The schema is the contract, and the model is the interface.
The distinction between the schema and the model is the distinction between the definition and the usage. The schema is defined once, at application startup. The model is derived from the schema with mongoose.model(), and it is used throughout the application. The model’s name determines the collection’s name: the model User maps to the users collection, and the model Kitten maps to the kittens collection. Mongoose pluralizes and lowercases the model name automatically, so the developer does not need to specify the collection name unless the convention does not fit .
This chapter covers three areas. First, why Mongoose exists — the schema-enforcement problem and the ODM’s role. Second, how to define schemas and models — the mongoose.Schema constructor, the field types, the validation rules, and the mongoose.model() compilation. Third, how to perform CRUD operations — the create, find, findById, findOne, save, updateOne, and deleteOne methods, and the query API. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the schema and model flow.
Key point: Mongoose is an ODM for MongoDB. A schema defines the structure and validation rules; a model is compiled from the schema and used to interact with the collection. The model’s name determines the collection’s name (pluralized, lowercased). The schema is enforced at the application level, not by MongoDB itself. The CRUD operations are asynchronous and return promises.
Why Mongoose exists
The schema-enforcement problem. MongoDB is schema-less by design. A collection can hold documents with different fields and types. This flexibility is useful when the data model is evolving, but it becomes a problem when the application expects a consistent structure. A query that assumes every user has an email field will fail when it encounters a user without one. Mongoose addresses this by defining a schema at the application level and validating the documents against it before they are written. The schema is the contract, and the validation is the enforcement .
The application-level problem. The Mongoose schema is not enforced by the database. It exists only in the Node.js application code. Another application, a database tool, or a direct mongosh session can insert documents that violate the schema, and MongoDB will accept them. The schema is a consistency mechanism for the application, not a database constraint. This distinction matters: the developer cannot rely on the schema as a security boundary, and the schema must be enforced in every application that writes to the collection .
The type-casting problem. MongoDB stores data in BSON, and the types are specific: int, long, double, string, date, objectId, and others. A Node.js application works with JavaScript types: number, string, Date, and the ObjectId class from the driver. Mongoose casts the JavaScript values to the BSON types before writing. If a value cannot be cast — a string in a Number field — Mongoose raises a CastError before the query is sent .
The validation problem. Beyond the type, a field can have validation rules: required, minimum, maximum, string length, enum, and custom validators. Mongoose runs the validators before the document is saved. If a validator fails, the save is rejected with a ValidationError. The validation is the second line of defense after the casting, and it is the mechanism that keeps the data consistent with the application’s expectations .
The middleware problem. Mongoose supports middleware (also called hooks) that run before or after specific operations. A pre('save') hook can hash a password before the document is written. A pre('validate') hook can normalize an email to lowercase. A post('save') hook can send a welcome email. The middleware is the mechanism for cross-cutting concerns that do not belong in the route handlers or the model methods .
The trade-off. Mongoose adds a layer of abstraction between the application and the database. The schema is another artifact to maintain, and the abstraction can hide the underlying MongoDB operations. A query that is simple in the driver becomes a chain of Mongoose methods. The trade-off is between the consistency and the convenience of the ODM and the directness and the flexibility of the driver. For applications with a stable data model and a need for validation, Mongoose is the right choice. For applications that need the full power of the aggregation pipeline or the raw driver, the driver is the better choice.
a. Defining a schema
A schema is defined with the mongoose.Schema constructor. The constructor takes an object, and each key is a field name. The value is either a type constructor or an object with the type and the options .
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
maxlength: [50, 'Name cannot exceed 50 characters'],
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/\S+@\S+\.\S+/, 'Invalid email format'],
},
age: {
type: Number,
min: [18, 'Must be at least 18 years old'],
max: [120, 'Age cannot exceed 120'],
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user',
},
createdAt: {
type: Date,
default: Date.now,
},
});
The type is the JavaScript constructor: String, Number, Boolean, Date, Array, Object, Buffer, Map, Schema.Types.ObjectId, and Schema.Types.Mixed. The required option takes a boolean or an array with the boolean and a custom message. The default option provides a value when the field is absent. The unique option creates a unique index, but it is not a validator: the uniqueness is enforced by the database, and the error is raised on the save .
The unique option is a common source of confusion. It does not validate the value before the query is sent. It creates a unique index on the field, and the database rejects the duplicate insert with a duplicate-key error. The error is raised at the save, not at the validation. The unique constraint is not a validator, and it does not appear in the ValidationError .
The _id field. Every document has an _id field, and Mongoose adds it automatically. The default type is ObjectId, and the default value is generated by the driver. The developer can specify a different type, but the ObjectId is the convention.
The timestamps option. The timestamps: true option adds createdAt and updatedAt fields to the schema. The fields are managed by Mongoose: createdAt is set on the first save, and updatedAt is updated on every save.
const userSchema = new mongoose.Schema({
name: String,
}, {
timestamps: true,
});
The schema options. The schema constructor’s second argument is an options object. The collection option overrides the collection name. The strict option (default true) rejects fields that are not in the schema. The minimize option (default true) removes empty objects. The versionKey option controls the __v field, which Mongoose uses for versioning.
b. Compiling a model
A model is compiled from a schema with mongoose.model(). The first argument is the model name, and the second is the schema .
const User = mongoose.model('User', userSchema);
The model name is singular and capitalized. Mongoose pluralizes it and lowercases it to determine the collection name: User maps to the users collection. The model is a constructor, and an instance of the model is a document.
const user = new User({
name: 'Alice',
email: 'alice@example.com',
age: 30,
});
console.log(user.name); // 'Alice'
console.log(user._id); // an ObjectId
The model has static methods for the queries: find, findById, findOne, create, updateOne, deleteOne, and others. The document has instance methods: save, validate, toJSON, and the custom methods defined on the schema .
The connection. Every model is associated with a connection. When mongoose.model() is called, the model uses the default connection, which is the one created by mongoose.connect(). If the application uses multiple connections, the model is created on the specific connection with connection.model() .
const connection = mongoose.createConnection('mongodb://127.0.0.1:27017/test');
const Tank = connection.model('Tank', tankSchema);
The schema copy. The mongoose.model() function makes a copy of the schema. Any method, hook, or virtual added after the model() call is not included in the compiled model. The recommendation is to add everything to the schema before compiling the model .
c. Performing CRUD operations
The model’s methods are asynchronous and return promises. The async/await syntax is the standard way to use them.
Create. The create method creates one or more documents and saves them. The new Model() constructor creates a document, and the save() method persists it.
// Create and save in one step
const user = await User.create({
name: 'Alice',
email: 'alice@example.com',
age: 30,
});
// Create and save in two steps
const user = new User({ name: 'Bob', email: 'bob@example.com' });
await user.save();
// Insert many
await User.insertMany([
{ name: 'Carol', email: 'carol@example.com' },
{ name: 'Dave', email: 'dave@example.com' },
]);
Read. The find method returns an array of documents. The findOne method returns a single document or null. The findById method returns the document with the specified _id .
// Find all
const users = await User.find();
// Find with a filter
const admins = await User.find({ role: 'admin' });
// Find one
const user = await User.findOne({ email: 'alice@example.com' });
// Find by ID
const user = await User.findById('507f1f77bcf86cd799439011');
The query API is chainable. The where, gt, lt, sort, limit, skip, and select methods refine the query.
const users = await User
.find({ age: { $gte: 18 } })
.where('role').equals('user')
.sort({ createdAt: -1 })
.limit(10)
.select('name email');
Update. The save method on a document updates the changed fields. The updateOne and findByIdAndUpdate methods update without loading the document.
// Load, modify, save
const user = await User.findById(id);
user.age = 31;
await user.save();
// Update without loading
await User.updateOne({ _id: id }, { age: 31 });
// Update and return
const updated = await User.findByIdAndUpdate(id, { age: 31 }, { new: true });
The findByIdAndUpdate method’s third argument is the options. The new: true option returns the updated document instead of the original. The runValidators: true option runs the schema validators on the update, which is not the default for the update methods .
Delete. The deleteOne and deleteMany methods remove the documents. The findByIdAndDelete method removes the document with the specified _id.
await User.deleteOne({ _id: id });
await User.deleteMany({ role: 'guest' });
await User.findByIdAndDelete(id);
The save versus the query methods. The save method runs the full validation and the middleware. The updateOne, updateMany, and findOneAndUpdate methods do not run the save middleware by default, and they do not run the validators unless the runValidators: true option is set. The distinction matters when the schema has validators or the middleware has side effects .
Complete Example Session
// ============================================
// PART 1: THE CONNECTION
// ============================================
const mongoose = require('mongoose');
async function main() {
await mongoose.connect('mongodb://127.0.0.1:27017/myapp');
console.log('Connected to MongoDB');
}
main().catch((err) => console.error(err));
// ============================================
// PART 2: THE SCHEMA
// ============================================
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
maxlength: [50, 'Name cannot exceed 50 characters'],
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [/\S+@\S+\.\S+/, 'Invalid email format'],
},
age: {
type: Number,
min: [18, 'Must be at least 18 years old'],
max: [120, 'Age cannot exceed 120'],
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user',
},
}, {
timestamps: true,
});
// ============================================
// PART 3: THE MODEL
// ============================================
const User = mongoose.model('User', userSchema);
// ============================================
// PART 4: CREATE
// ============================================
async function createUser() {
const user = await User.create({
name: 'Alice',
email: 'alice@example.com',
age: 30,
});
console.log(user.name); // 'Alice'
console.log(user._id); // an ObjectId
return user;
}
// ============================================
// PART 5: READ
// ============================================
async function findUsers() {
// All users
const all = await User.find();
// With a filter
const admins = await User.find({ role: 'admin' });
// One user
const user = await User.findOne({ email: 'alice@example.com' });
// By ID
const byId = await User.findById('507f1f77bcf86cd799439011');
// With a chain
const sorted = await User
.find({ age: { $gte: 18 } })
.sort({ createdAt: -1 })
.limit(10)
.select('name email');
return { all, admins, user, byId, sorted };
}
// ============================================
// PART 6: UPDATE
// ============================================
async function updateUser(id) {
// Load, modify, save
const user = await User.findById(id);
user.age = 31;
await user.save();
// Update without loading
await User.updateOne({ _id: id }, { age: 31 });
// Update and return
const updated = await User.findByIdAndUpdate(
id,
{ age: 31 },
{ new: true, runValidators: true }
);
return updated;
}
// ============================================
// PART 7: DELETE
// ============================================
async function deleteUser(id) {
await User.deleteOne({ _id: id });
// or
await User.findByIdAndDelete(id);
}
// ============================================
// PART 8: THE VALIDATION ERROR
// ============================================
async function validationError() {
try {
await User.create({
name: 'Bob',
email: 'not-an-email',
age: 17,
});
} catch (error) {
if (error.name === 'ValidationError') {
console.log(error.errors['email'].message); // 'Invalid email format'
console.log(error.errors['age'].message); // 'Must be at least 18 years old'
}
}
}
// ============================================
// PART 9: THE MIDDLEWARE
// ============================================
userSchema.pre('save', async function (next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 12);
}
next();
});
userSchema.post('save', function (doc) {
console.log(`User ${doc._id} saved`);
});
// ============================================
// PART 10: THE COMPLETE APPLICATION
// ============================================
const express = require('express');
const app = express();
app.use(express.json());
app.post('/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (error) {
if (error.name === 'ValidationError') {
return res.status(400).json({ errors: error.errors });
}
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/users', async (req, res) => {
const users = await User.find().select('name email role');
res.json(users);
});
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.patch('/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.delete('/users/:id', async (req, res) => {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(204).send();
});
app.listen(3000);
The ten parts show the connection, the schema, the model, the create, the read, the update, the delete, the validation error, the middleware, and the complete application.
Quick Reference
The Schema Constructor
| Option | Purpose |
|---|---|
type | The JavaScript constructor |
required | The field is required |
default | The default value |
unique | A unique index (not a validator) |
lowercase | Convert to lowercase |
trim | Trim the whitespace |
min / max | The numeric bounds |
maxlength / minlength | The string bounds |
enum | The allowed values |
match | The regex pattern |
The Model Methods
| Method | Purpose |
|---|---|
create(data) | Create and save |
insertMany(array) | Insert multiple |
find(filter) | Find all matching |
findOne(filter) | Find one matching |
findById(id) | Find by _id |
updateOne(filter, update) | Update one |
findByIdAndUpdate(id, update) | Update and return |
deleteOne(filter) | Delete one |
deleteMany(filter) | Delete multiple |
findByIdAndDelete(id) | Delete by _id |
The Document Methods
| Method | Purpose |
|---|---|
save() | Persist the changes |
validate() | Run the validators |
toJSON() | Convert to JSON |
toObject() | Convert to a plain object |
isModified(path) | Check if a field changed |
The Query API
| Method | Purpose |
|---|---|
.where(path) | Add a condition |
.equals(value) | Equality |
.gt(value) / .lt(value) | Comparison |
.sort(object) | Sort |
.limit(n) | Limit the results |
.skip(n) | Skip the results |
.select(fields) | Select the fields |
.populate(path) | Join a reference |
The Error Types
| Error | Cause |
|---|---|
ValidationError | A validator failed |
CastError | A value could not be cast |
DocumentNotFoundError | The document was not found |
MongoServerError (11000) | A unique index violation |
Best Practices
✅ Do This:
// Define the schema before the model
const userSchema = new mongoose.Schema({ ... }); // ✅
// Use the required option with a custom message
name: { type: String, required: [true, 'Name is required'] } // ✅
// Use the enum option for the constrained values
role: { type: String, enum: ['user', 'admin'] } // ✅
// Use timestamps: true for the created/updated fields
const schema = new Schema({ ... }, { timestamps: true }); // ✅
// Use findByIdAndUpdate with new: true and runValidators: true
await User.findByIdAndUpdate(id, data, { new: true, runValidators: true }); // ✅
// Use save() for the full validation and the middleware
await user.save(); // ✅
// Handle the ValidationError in the route
if (error.name === 'ValidationError') { ... } // ✅
❌ Don’t Do This:
// Don't rely on the unique option as a validator
email: { type: String, unique: true } // the error is at save, not validate // ❌
// Don't expect the update methods to run the validators by default
await User.updateOne({ _id: id }, { age: 'bad' }); // no validation // ❌
// Don't use the string concatenation in the queries
User.find({ email: req.body.email }); // use the object form // ❌
// Don't forget to handle the CastError
const user = new User({ age: 'not a number' }); // CastError // ❌
// Don't add the middleware after the model is compiled
mongoose.model('User', userSchema); userSchema.pre('save', ...); // too late // ❌
// Don't use the driver methods and the Mongoose methods interchangeably
User.collection.insertOne(...); // bypasses the Mongoose validation // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
ValidationError not caught | No try/catch | Wrap the save in try/catch |
CastError on a numeric field | The value is not a number | Check the input type |
| The unique constraint not enforced | The index is not created | Wait for the index or use syncIndexes |
| The update validators not run | The default is off | Use runValidators: true |
| The middleware not run | The model was compiled first | Add the middleware before model() |
| The document not found on save | The document was deleted | Handle DocumentNotFoundError |
The _id is not an ObjectId | The schema uses a different type | Use Schema.Types.ObjectId |
Real-World Examples
1. The Connection
await mongoose.connect('mongodb://127.0.0.1:27017/myapp');
2. The Schema
const userSchema = new mongoose.Schema({ name: String, email: String });
3. The Model
const User = mongoose.model('User', userSchema);
4. The Create
const user = await User.create({ name: 'Alice', email: 'a@b.com' });
5. The Find
const users = await User.find({ role: 'admin' });
6. The Find by ID
const user = await User.findById(id);
7. The Update
await User.findByIdAndUpdate(id, { age: 31 }, { new: true });
8. The Delete
await User.findByIdAndDelete(id);
9. The Validation Error
if (error.name === 'ValidationError') { ... }
10. The Middleware
userSchema.pre('save', async function (next) { ... });
Visual
The Schema and Model Flow
┌──────────────────────────────────────────────────────────────┐
│ THE SCHEMA AND MODEL FLOW │
│ │
│ const userSchema = new mongoose.Schema({ │
│ name: { type: String, required: true }, │
│ email: { type: String, unique: true }, │
│ }); │
│ │ │
│ │ The schema defines the structure. │
│ ▼ │
│ const User = mongoose.model('User', userSchema); │
│ │ │
│ │ The model is compiled from the schema. │
│ │ The model name 'User' maps to the 'users' collection. │
│ ▼ │
│ const user = new User({ name: 'Alice' }); │
│ │ │
│ │ The document is an instance of the model. │
│ ▼ │
│ await user.save(); │
│ │ │
│ │ Mongoose casts and validates before saving. │
│ │ The document is written to the 'users' collection. │
│ ▼ │
│ MongoDB │
│ │
└──────────────────────────────────────────────────────────────┘
The CRUD Operations
┌──────────────────────────────────────────────────────────────┐
│ THE CRUD OPERATIONS │
│ │
│ CREATE: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ await User.create({ name: 'Alice' }); │ │
│ │ await new User({ name: 'Bob' }).save(); │ │
│ │ await User.insertMany([{ ... }, { ... }]); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ READ: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ await User.find({ role: 'admin' }); │ │
│ │ await User.findOne({ email: 'a@b.com' }); │ │
│ │ await User.findById(id); │ │
│ │ await User.find().sort({ name: 1 }).limit(10); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ UPDATE: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const user = await User.findById(id); │ │
│ │ user.age = 31; │ │
│ │ await user.save(); │ │
│ │ │ │
│ │ await User.updateOne({ _id: id }, { age: 31 }); │ │
│ │ await User.findByIdAndUpdate(id, data, { new: true });│ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ DELETE: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ await User.deleteOne({ _id: id }); │ │
│ │ await User.deleteMany({ role: 'guest' }); │ │
│ │ await User.findByIdAndDelete(id); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Validation and Casting
┌──────────────────────────────────────────────────────────────┐
│ THE VALIDATION AND CASTING │
│ │
│ Input: { name: 'Alice', age: 'not a number' } │
│ │ │
│ ▼ │
│ CASTING: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Mongoose tries to cast 'not a number' to Number. │ │
│ │ The cast fails. │ │
│ │ A CastError is raised. │ │
│ │ │ │
│ │ Validation does NOT run after a cast error. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ VALIDATION (if the cast succeeds): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ required: the field is present │ │
│ │ min/max: the value is in the range │ │
│ │ match: the value matches the regex │ │
│ │ enum: the value is in the list │ │
│ │ custom: the function returns true │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ SAVE (if the validation passes) │
│ │
└──────────────────────────────────────────────────────────────┘
The Middleware Hooks
┌──────────────────────────────────────────────────────────────┐
│ THE MIDDLEWARE HOOKS │
│ │
│ userSchema.pre('validate', function (next) { │
│ if (this.email) this.email = this.email.toLowerCase(); │
│ next(); │
│ }); │
│ │ │
│ │ Normalize before validation. │
│ ▼ │
│ userSchema.pre('save', async function (next) { │
│ if (this.isModified('password')) { │
│ this.password = await bcrypt.hash(this.password, 12); │
│ } │
│ next(); │
│ }); │
│ │ │
│ │ Hash the password before saving. │
│ ▼ │
│ userSchema.post('save', function (doc) { │
│ sendWelcomeEmail(doc.email); │
│ }); │
│ │ │
│ │ Side effect after saving. │
│ ▼ │
│ MongoDB │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Mongoose | ODM for MongoDB |
| Schema | Defines the structure and validation |
| Model | Compiled from the schema |
| Collection name | Pluralized, lowercased model name |
create() | Create and save |
find() | Find all matching |
findById() | Find by _id |
save() | Persist the changes |
findByIdAndUpdate() | Update and return |
deleteOne() | Delete one |
ValidationError | A validator failed |
CastError | A value could not be cast |
Key takeaways:
- Mongoose is an ODM for MongoDB. It defines a schema at the application level and enforces the schema’s rules before the documents are written. The schema is not enforced by the database; it exists only in the Node.js application code .
- The schema defines the structure and the validation. The
mongoose.Schemaconstructor takes an object with the field names and the options. The options includetype,required,default,unique,min,max,enum,match, and the custom validators . - The model is compiled from the schema with
mongoose.model(). The model name determines the collection name:Usermaps to theuserscollection. The model is the constructor for the documents, and it has the static methods for the queries . - The CRUD operations are asynchronous. The
create,find,findById,findOne,save,updateOne,findByIdAndUpdate,deleteOne, andfindByIdAndDeletemethods return promises and are used withasync/await. - The
savemethod runs the full validation and the middleware. TheupdateOneandfindByIdAndUpdatemethods do not run the validators unless therunValidators: trueoption is set, and they do not run thesavemiddleware . - The
uniqueoption is not a validator. It creates a unique index, and the duplicate-key error is raised at the save, not at the validation. The error is aMongoServerErrorwith the code11000, not aValidationError. - The
CastErroris raised before the validation. When a value cannot be cast to the field’s type, Mongoose raises aCastError, and the validators do not run. The error is in theerror.errorsobject with thename: 'CastError'. - The middleware hooks run before or after the operations. The
pre('save')hook runs before the document is saved, thepre('validate')hook runs before the validation, and thepost('save')hook runs after the save. The middleware is the mechanism for the cross-cutting concerns .
Remember: Mongoose is the ODM that adds structure to MongoDB. The schema defines the fields, the types, and the validation rules. The model is compiled from the schema and used to interact with the collection. The CRUD operations are asynchronous, and the save method runs the full validation and the middleware. The unique option is not a validator, and the CastError is raised before the validation. The middleware hooks are the mechanism for the password hashing, the data normalization, and the side effects. The result is a Node.js application where the documents are validated, cast, and consistent with the application’s expectations before they reach the database.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!