Skip to content

Mongoose ODM for MongoDB

Mongoose adds structure on top of MongoDB's flexible documents: schemas, validation, and a model-based API that feels natural in JavaScript. This lesson covers defining schemas and using models to query data.

What Is Mongoose?

Mongoose is an Object Document Mapper (ODM) for MongoDB, it lets you define a Schema describing a document's expected shape and types, then compile that schema into a Model, a class you use to create, query, update, and delete documents with built-in validation.

Unlike the raw mongodb driver, Mongoose enforces structure at the application level (types, required fields, defaults) even though MongoDB itself doesn't require a fixed schema, giving you many of the safety benefits of a relational schema with the flexibility of documents.

import mongoose from 'mongoose';

await mongoose.connect(process.env.MONGODB_URI);

const productSchema = new mongoose.Schema({
  name: { type: String, required: true },
  price: { type: Number, required: true, min: 0 },
  inStock: { type: Boolean, default: true },
});

const Product = mongoose.model('Product', productSchema);

const product = await Product.create({ name: 'Keyboard', price: 49.99 });
console.log(product._id, product.name);

Product.create() validates the input against the schema automatically, if price were negative or name were missing, it would throw a validation error before hitting the database.

Core Mongoose API

Model.create(data)
Model.find(filter)
Model.findById(id)
Model.findByIdAndUpdate(id, updates)
Model.findByIdAndDelete(id)
  • Model.create() validates and inserts a new document in one call.
  • Model.find() returns a query object; await it directly or chain further methods first.
  • findById() handles the ObjectId conversion for you automatically, unlike the raw driver.
  • Schema-level validation (required, min, custom validators) runs before any database write.

Mongoose Cheatsheet

The Mongoose methods you'll use in nearly every model.

Task Code
Create await Product.create(data)
Find all await Product.find({})
Find by id await Product.findById(id)
Find one by filter await Product.findOne({ name })
Update by id await Product.findByIdAndUpdate(id, updates)
Delete by id await Product.findByIdAndDelete(id)

Schema Validation in Depth

Mongoose schemas support a rich set of built-in validators, required, min/max, enum, string length limits, plus custom validator functions for anything more specific to your domain.

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    match: /.+@.+\..+/,
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user',
  },
});

unique: true relies on a database-level index and only fully enforces uniqueness once that index exists, it's a helpful hint, not a substitute for handling duplicate-key errors.

Mongoose Middleware (Hooks)

Mongoose supports "pre" and "post" hooks that run automatically around operations like save, validate, or remove, useful for tasks like hashing a password before saving a user document.

userSchema.pre('save', async function (next) {
  if (this.isModified('password')) {
    this.password = await hashPassword(this.password);
  }
  next();
});

Common Mistakes

  • Skipping schema validators and validating everything manually in route handlers instead.
  • Forgetting that unique: true requires an actual database index to be fully effective.
  • Not handling Mongoose validation errors distinctly from other database errors when responding to clients.
  • Mutating a document without calling .save() afterward, so changes never persist.

Key Takeaways

  • Mongoose is an ODM that adds schemas, validation, and a model-based API on top of MongoDB.
  • Schemas define field types, required fields, defaults, and custom validation rules.
  • Models provide convenient methods like findById, findByIdAndUpdate, and create.
  • Pre/post hooks let you run logic automatically around document lifecycle events.

Pro Tip

Even though MongoDB itself is schema-flexible, define a strict Mongoose schema anyway for anything user-facing. Catching a validation error at the application layer, with a clear message, is far better for both debugging and API consumers than letting malformed data reach the database.