mrkeyoor.com_
Wed 05 Aug 05:04 UTC
npmDataupdated 05 Aug 2026

mongoose

The standard MongoDB object modeling layer (ODM) for Node.js, built on top of the official MongoDB driver. You define schemas that add structure to schemaless collections: types, validators, defaults, getters and setters, indexes, middleware hooks, instance methods, and populate for pseudo-joins between collections. Documents you load become full model instances with save(), change tracking, and validation. It supports Node.js with alpha-level Deno support, and Mongoose 9.0 shipped in November 2025.

Verdict

Still the default ODM for MongoDB plus Node and dependable at that job, with very active maintenance. If your project is TypeScript-heavy or your database choice is not settled, evaluate Prisma before committing.

API stability3/5The core schema-model-query API has been recognizable for a decade, but majors land roughly yearly (9.0 in November 2025) and each ships a real backwards-breaking-changes list; callback APIs were removed entirely in v7.
Docs4/5mongoosejs.com pairs topic guides with generated API docs and a migration guide per major; middleware ordering and connection edge cases are documented but still routinely send people to Stack Overflow.
Maintenance5/5Pushed to within hours of this review, 400+ contributors, backed by Automattic with commercial support via Tidelift; issues get triaged fast.
Ecosystem4/5Hundreds of community plugins on the official plugin search site plus session stores, seeders, and test runners like mongodb-memory-server; smaller than the SQL ORM world but deep for MongoDB.

Use it if

  • You are on MongoDB with Node.js and want schema validation, casting, and defaults enforced at the application layer
  • You use lifecycle hooks (pre-save, pre-find), virtuals, and populate rather than hand-writing that plumbing over the raw driver
  • Your team benefits from the plugin ecosystem (pagination, soft delete, autopopulate) and a well-trodden path with years of Stack Overflow answers
  • You want one place where document shape, indexes, and business rules for a collection live together
Skip it if

Setup reality

npm install mongoose, no native builds, works everywhere Node does. The friction is conceptual, not install-time. The default connection is a singleton: models registered via mongoose.model() are bound to it, and if you use createConnection() you must register models on that connection or saves silently target a connection that was never opened (the README calls this out explicitly). Command buffering means your app boots fine with a wrong URI and only fails later with timeouts. Collection names are auto-pluralized (model Ticket writes to tickets). Majors arrive regularly, 9.0 in November 2025, each with a documented list of backwards breaking changes you actually need to read before upgrading.

Patterns

Connect to MongoDBconnect-database

const mongoose = require("mongoose");

await mongoose.connect("mongodb://127.0.0.1:27017/my_database");

Commands are buffered until the connection opens, so a bad URI does not fail at startup; it fails later as operation timeouts. Use 127.0.0.1 instead of localhost if local connects hang.

Define a schema and modeldefine-schema-model

const { Schema, model } = require("mongoose");

const postSchema = new Schema({
  title: { type: String, required: true },
  author: { type: Schema.Types.ObjectId, ref: "User" },
  views: { type: Number, default: 0, min: 0 },
  createdAt: { type: Date, default: Date.now },
});

const BlogPost = model("BlogPost", postSchema);

The model name is singular; Mongoose pluralizes it for the collection, so model("Ticket", ...) reads and writes the tickets collection.

Create and save a documentcreate-document

const post = await BlogPost.create({ title: "Hello" });

// or the two-step form
const draft = new BlogPost({ title: "Draft" });
draft.views = 1;
await draft.save();

Validation runs on save/create by default; a failed validator rejects the promise with a ValidationError listing every bad path.

Find with filters, sort, and limitquery-documents

const recent = await BlogPost.find({ views: { $gte: 10 } })
  .sort({ createdAt: -1 })
  .limit(20)
  .select("title views");

const one = await BlogPost.findById(id);

Queries are thenables, not promises; awaiting works, but calling .then() twice re-executes the query.

Fast read-only queries with lean()lean-reads

const rows = await BlogPost.find({ published: true }).lean();

lean() returns plain JS objects instead of Mongoose documents: much faster and lighter, but no getters, virtuals, defaults, or save().

Atomic find-and-updateupdate-document

const updated = await BlogPost.findOneAndUpdate(
  { _id: id },
  { $inc: { views: 1 } },
  { new: true, runValidators: true }
);

Without new: true you get the pre-update document back; update validators only run when you pass runValidators, and they see the update, not the whole doc.

Populate referenced documentspopulate-references

const post = await BlogPost.findById(id)
  .populate("author", "name email");

console.log(post.author.name);

populate is extra queries under the hood, not a server-side join; populating in a loop over N docs is a classic N+1 problem.

Pre and post middlewaremiddleware-hooks

postSchema.pre("save", function (next) {
  this.slug = slugify(this.title);
  next();
});

postSchema.pre("deleteOne", { document: true }, async function () {
  await Comment.deleteMany({ post: this._id });
});

In document middleware `this` is the document; in query middleware (find, updateOne by default) `this` is the query object. Getting that wrong is the top middleware bug.

Embedded subdocumentsembedded-subdocuments

const post = await BlogPost.findById(myId);
post.comments.push({ title: "My comment" });
await post.save();

// remove one
post.comments[0].deleteOne();
await post.save();

Subdocuments get their own validators, defaults, and middleware, but they only persist when the parent document is saved.

Multi-document transactiontransactions

const session = await mongoose.startSession();
await session.withTransaction(async () => {
  await Account.updateOne({ _id: from }, { $inc: { balance: -100 } }, { session });
  await Account.updateOne({ _id: to }, { $inc: { balance: 100 } }, { session });
});
await session.endSession();

Transactions require a replica set or mongos; they throw on a plain standalone mongod, which is exactly what most local dev setups run.

Schema field literally named typenested-type-field

new Schema({
  asset: {
    name: String,
    type: { type: String }, // works
  },
});

A bare `type: String` inside a nested object makes Mongoose treat the whole parent as a String path; wrap it as { type: String } to get a real nested field.

Models on a separate connectionmultiple-connections

const conn = mongoose.createConnection(process.env.ANALYTICS_URI);
const Event = conn.model("Event", eventSchema);
await new Event({ name: "pageview" }).save();

Register the model on the connection (conn.model), not mongoose.model; otherwise it binds to the default connection that may never have been opened.

Alternatives

PackageRegistryPick it when
mongodbnpmYou want the official driver with no ODM layer: full control, no schema magic, best performance
prismanpmYou want generated TypeScript types from one schema file and the option to move between databases later
@typegoose/typegoosenpmYou are committed to Mongoose but want class-based models so TypeScript types and schemas stay as one definition