| |

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

OptionPurpose
typeThe JavaScript constructor
requiredThe field is required
defaultThe default value
uniqueA unique index (not a validator)
lowercaseConvert to lowercase
trimTrim the whitespace
min / maxThe numeric bounds
maxlength / minlengthThe string bounds
enumThe allowed values
matchThe regex pattern

The Model Methods

MethodPurpose
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

MethodPurpose
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

MethodPurpose
.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

ErrorCause
ValidationErrorA validator failed
CastErrorA value could not be cast
DocumentNotFoundErrorThe 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

PitfallWhy It HappensFix
ValidationError not caughtNo try/catchWrap the save in try/catch
CastError on a numeric fieldThe value is not a numberCheck the input type
The unique constraint not enforcedThe index is not createdWait for the index or use syncIndexes
The update validators not runThe default is offUse runValidators: true
The middleware not runThe model was compiled firstAdd the middleware before model()
The document not found on saveThe document was deletedHandle DocumentNotFoundError
The _id is not an ObjectIdThe schema uses a different typeUse 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

ItemValue
MongooseODM for MongoDB
SchemaDefines the structure and validation
ModelCompiled from the schema
Collection namePluralized, 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
ValidationErrorA validator failed
CastErrorA 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.Schema constructor takes an object with the field names and the options. The options include type, 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: User maps to the users collection. 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, and findByIdAndDelete methods return promises and are used with async/await .
  • The save method runs the full validation and the middleware. The updateOne and findByIdAndUpdate methods do not run the validators unless the runValidators: true option is set, and they do not run the save middleware .
  • The unique option 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 a MongoServerError with the code 11000, not a ValidationError .
  • The CastError is raised before the validation. When a value cannot be cast to the field’s type, Mongoose raises a CastError, and the validators do not run. The error is in the error.errors object with the name: 'CastError' .
  • The middleware hooks run before or after the operations. The pre('save') hook runs before the document is saved, the pre('validate') hook runs before the validation, and the post('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!