Skip to content

Mongoose with Express

Mongoose is the most popular MongoDB ORM (technically an ODM, Object Document Mapper) for Node.js, adding schemas, validation, and a convenient model-based API on top of the native driver. This lesson covers its core workflow.

Schemas and Models

A Mongoose Schema defines the shape of a document, field types, required fields, defaults, validation rules, even though MongoDB itself doesn't enforce a schema at the database level. A Model, compiled from a schema, is the interface you actually use to query and manipulate that collection.

This gives you structure similar to a relational ORM, defined fields, types, validation, while still storing data in MongoDB's flexible document format.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  name: { type: String, required: true },
  age: { type: Number, min: 0 },
}, { timestamps: true });

const User = mongoose.model('User', userSchema);

module.exports = User;

{ timestamps: true } automatically adds and maintains createdAt/updatedAt fields on every document.

Connecting and Basic CRUD

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

await User.create({ email, name });
await User.find({});
await User.findById(id);
await User.findByIdAndUpdate(id, { name }, { new: true });
await User.findByIdAndDelete(id);
  • mongoose.connect() manages the underlying connection pool for you.
  • Model.create() both validates against the schema and inserts the document.
  • findByIdAndUpdate(id, data, { new: true }) returns the updated document instead of the original.
  • Mongoose automatically converts a valid ID string into an ObjectId for you in query methods.

Mongoose Cheatsheet

The Mongoose model methods you'll use for nearly every resource.

Operation Method Example
Create create() User.create({ email, name })
Read all find() User.find({ active: true })
Read one findById() User.findById(id)
Update findByIdAndUpdate() User.findByIdAndUpdate(id, data, { new: true })
Delete findByIdAndDelete() User.findByIdAndDelete(id)
Count countDocuments() User.countDocuments({ active: true })

Schema Validation and Error Handling

Mongoose validates data against the schema before saving, throwing a ValidationError you can catch and translate into a clean 400 response, this pairs naturally with the error-handling patterns from earlier lessons.

router.post('/', async (req, res, next) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    if (err.name === 'ValidationError') {
      return res.status(400).json({ error: err.message });
    }
    next(err);
  }
});

Relationships With populate()

Mongoose models relationships between documents with a ref field storing another document's ObjectId, and .populate() to automatically resolve that reference into the full related document when needed.

const orderSchema = new mongoose.Schema({
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  total: Number,
});

const Order = mongoose.model('Order', orderSchema);

const order = await Order.findById(id).populate('user');
console.log(order.user.email); // fully resolved User document

Common Mistakes

  • Defining the schema shape inconsistently across files instead of exporting one shared model.
  • Forgetting { new: true } on findByIdAndUpdate() and getting the pre-update document back.
  • Not catching Mongoose's ValidationError distinctly from other errors.
  • Overusing .populate() on large collections without considering the performance cost of the extra lookup.

Key Takeaways

  • Mongoose adds schemas, validation, and a model-based API on top of MongoDB's flexible documents.
  • Models compiled from a schema are your primary interface for querying and mutating data.
  • Catch ValidationError specifically to return clean, field-level error responses.
  • .populate() resolves ObjectId references into full related documents.

Pro Tip

Enable { timestamps: true } on every schema by default, createdAt/updatedAt fields are almost always useful later for sorting, auditing, or debugging, and cost nothing to add upfront.