cheat sheet

mongoose

Package-level reference for mongoose on npm — schemas, models, transactions, populate, indexes, and 7-to-8 migration.

mongoose

What it is

mongoose is the dominant Object-Document Mapper for MongoDB in Node. It layers schemas, validation, middleware (hooks), virtuals, and query builders on top of the official mongodb driver. Documents become typed JavaScript objects with methods and computed fields; queries return either lean POJOs or full Mongoose Documents depending on the call.

Reach for mongoose when you want enforced shape, application-level validation, hooks (pre/post), and convenience helpers like populate for cross-collection joins. Reach for the raw mongodb driver when you want maximum performance, or prisma (with its Mongo adapter) when you want generated TypeScript types from a single schema.

Install

bash
npm install mongoose

Output: added mongoose to dependencies

bash
pnpm add mongoose

Output: added 1 package, linked from store

bash
yarn add mongoose

Output: added mongoose

bash
bun add mongoose

Output: installed mongoose

Mongoose bundles its own TypeScript types — no @types/mongoose package needed (and the historical one is deprecated).

Versioning & Node support

Current line is mongoose@8.x.

  • mongoose@8 — Node 16.20+. Requires mongodb@6 driver. Refined types (notably HydratedDocument, InferSchemaType). Strict-populate by default.
  • mongoose@7 — Node 14.20+. Driver mongodb@5. Removed several deprecated paths.
  • mongoose@6 — Node 12+. The last line with the older callback API. Still in wide production use.
  • mongoose@5 — historical.

Mongoose follows semver. Pin minor in production ("mongoose": "8.x") and audit mongodb driver version on each upgrade — it controls wire-protocol behaviour.

Package metadata

  • Maintainer: Valeri Karpov + Automattic (Automattic/mongoose)
  • Project home: github.com/Automattic/mongoose
  • Docs: mongoosejs.com
  • npm: npmjs.com/package/mongoose
  • License: MIT
  • First released: 2010
  • Downloads: ~2 million+ weekly downloads — by far the most popular MongoDB abstraction in Node.

Peer dependencies & extras

mongoose bundles mongodb (the official driver) as a transitive dependency — no peer-dep install. Commonly-paired packages:

  • mongoose-paginate-v2 — cursor + offset paginators
  • mongoose-autopopulate — auto-populate configured per-schema
  • mongoose-aggregate-paginate-v2 — pagination over aggregation pipelines
  • mongoose-delete — soft deletes
  • mongoose-unique-validator — friendlier unique error messages
  • mongoose-lean-virtuals — virtuals on .lean() queries
  • connect-mongo — Express session store backed by Mongo
  • agenda / bullmq — job queues that pair with Mongo or Redis storage

Alternatives

ApproachTrade-off
mongodb (driver)Lower-level, no schema/validation/hooks. Best for high-throughput services and CLIs.
prisma (Mongo provider)TypeScript-first schema, generated client. Less mature than the Postgres provider.
typegooseTypeScript-class-decorator wrapper around mongoose. Same engine, classier ergonomics.
mikro-ormIdentity-map ORM; supports Mongo. Strong for hybrid SQL+Mongo apps.
mongolass / monkThin promise wrappers. Niche.

Real-world recipes

Schema + Model

A Mongoose model is a constructor over a schema. The schema defines fields, types, validation, and indexes. The model exposes CRUD methods scoped to a collection.

typescript
import mongoose, { Schema, InferSchemaType, model } from "mongoose";

const userSchema = new Schema({
  email: { type: String, required: true, unique: true, lowercase: true },
  name: { type: String, trim: true },
  createdAt: { type: Date, default: Date.now },
}, { timestamps: true });

type User = InferSchemaType<typeof userSchema>;
const User = model<User>("User", userSchema);

await mongoose.connect(process.env.MONGO_URL!);

Output: Mongoose connects; User is a typed model targeting the users collection. timestamps: true adds createdAt / updatedAt automatically.

CRUD operations

The model exposes create, find, findOne, findOneAndUpdate, updateOne, deleteOne, plus chainable query builders.

typescript
const u = await User.create({ email: "alice@example.com", name: "Alice" });

const fetched = await User.findOne({ email: "alice@example.com" }).lean();

await User.updateOne({ _id: u._id }, { $set: { name: "Alice Dev" } });

await User.deleteOne({ _id: u._id });

Output: create returns the saved document; .lean() returns a plain JS object skipping the Mongoose wrapper (faster, but no .save() / virtuals); update + delete return acknowledgement objects.

Use .lean() for read-mostly endpoints — 2-5× faster and less memory.

Populate (cross-collection join)

populate replaces a foreign-key field (ObjectId) with the referenced document. It's an extra round-trip, not a join — Mongoose issues a second query.

typescript
import { Schema, model, Types } from "mongoose";

const postSchema = new Schema({
  title: String,
  author: { type: Schema.Types.ObjectId, ref: "User" },
});
const Post = model("Post", postSchema);

const posts = await Post.find().populate("author", "email name").lean();
console.log(posts[0].author);

Output: each post's author field is the referenced User document (selected fields only). Mongoose runs Post.find() then a single User.find({ _id: { $in: [...] } }).

For aggregation-style joins use $lookup in an aggregation pipeline — same effect, server-side, faster on large fan-outs.

Multi-document transaction

MongoDB transactions require a replica set or a sharded cluster (Atlas is, by default). Open a session, run operations inside withTransaction, commit/abort automatically.

typescript
import mongoose from "mongoose";

async function transfer(fromId: string, toId: string, amount: number) {
  const session = await mongoose.startSession();
  try {
    await session.withTransaction(async () => {
      await Account.updateOne({ _id: fromId }, { $inc: { balance: -amount } }, { session });
      await Account.updateOne({ _id: toId },   { $inc: { balance:  amount } }, { session });
    });
  } finally {
    await session.endSession();
  }
}

Output: both updates commit or neither does. withTransaction retries on transient TransientTransactionError errors automatically.

Standalone (non-replica-set) mongod rejects transactions. Run mongod --replSet rs0 locally for tests, or mongodb-memory-server with replSet: { count: 1 }.

Index management

Mongoose declares indexes in the schema. The driver creates them on mongoose.connect() (or when the model is first used) — in production, prefer running migrations explicitly.

typescript
const productSchema = new Schema({
  sku: { type: String, index: { unique: true } },
  category: String,
  price: Number,
});
productSchema.index({ category: 1, price: -1 }); // compound, descending price
productSchema.index({ name: "text" });            // full-text

const Product = model("Product", productSchema);

await Product.syncIndexes(); // drop+recreate to match schema

Output: indexes match the schema declarations exactly. syncIndexes is destructive — gates it behind a migration step.

Avoid autoIndex: true in production — schema changes silently rebuild indexes on hot start. Set mongoose.set("autoIndex", false) and run syncIndexes() from a deploy hook.

Production deployment

Connection pool

Configure the underlying driver pool via the connection options.

typescript
await mongoose.connect(process.env.MONGO_URL!, {
  maxPoolSize: 10,
  minPoolSize: 1,
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000,
  family: 4, // IPv4 only; speeds up DNS on hosts with broken AAAA
});

Output: at most 10 sockets per replica; fast failure if the cluster is unreachable.

Replica set / Atlas

Mongo connection strings encode the topology. Atlas / replica-set strings include replicaSet= and multiple hosts. Mongoose's driver handles failover transparently.

bash
mongodb+srv://user:pass@cluster.mongodb.net/dbname?retryWrites=true&w=majority

The +srv scheme triggers SRV/TXT lookups to discover replica members. w=majority writes wait for the majority of the replica set to acknowledge — the right durability default.

Graceful shutdown

typescript
for (const sig of ["SIGTERM", "SIGINT"] as const) {
  process.on(sig, async () => {
    await mongoose.disconnect();
    process.exit(0);
  });
}

Output: open queries finish; new ones fail; clean exit.

Logging slow queries

Set a slow-query threshold in Mongo; mongoose can log all queries via mongoose.set("debug", true) (dev) or hook mongoose.set("debug", (collection, method, query, doc) => …) to a structured logger.

Performance tuning

  • .lean() aggressively. Skip the Mongoose wrapper for read endpoints. Halves CPU on hot reads.
  • Project fields. find({}, "email name") returns only those columns; saves bandwidth and parsing.
  • Index every query path. db.collection.explain() (or Model.find().explain()) shows whether an index is used.
  • Avoid populate in tight loops. Each call is a separate query; for fan-out >100, switch to $lookup in an aggregation.
  • Limit cursor sizes. await Model.find({}) materializes everything; for >10k results, use .cursor() or .stream().
  • bulkWrite for batch operations. Combines inserts/updates/deletes into one round trip.
  • Disable validators on bulk loads. Model.insertMany(docs, { validate: false, lean: true }) for trusted data.
  • Aggregation allowDiskUse: true on large pipelines to allow Mongo to spill to disk instead of erroring at the 100 MB sort limit.
  • maxTimeMS per query. Bounds runaway scans. Model.find().maxTimeMS(2000).

Version migration guide

From mongoose@7 to mongoose@8

  1. Node 16.20+ required.
  2. Underlying driver bumped to mongodb@6 — review any direct driver usage.
  3. Strict populate is default — populate("missingField") throws instead of silently returning unpopulated. Pass { strictPopulate: false } to keep the old behaviour.
  4. findOneAndUpdate returns the updated document by default (was the original under v6).
  5. Type signatures tightened — HydratedDocument<T> is the canonical document type. Document<T> still exists but HydratedDocument is now preferred.
  6. Removed deprecated Model.update() (use updateOne / updateMany).

Migration sequence:

bash
npm install mongoose@8
npx tsc --noEmit
npm test

Output: TypeScript catches most issues; runtime regressions usually surface in populate-heavy code.

From mongoose@6 to mongoose@7

  1. Model.remove() removed — use deleteOne / deleteMany.
  2. Callback API removed everywhere — convert remaining .exec(cb) calls to await.
  3. mongoose.connect() returns the connection promise, not undefined.

Security considerations

  • Always validate inputs at the schema. Mongoose's required, match, min, max, enum etc. are your first line. Don't store user input untyped.
  • Avoid $where and JavaScript-eval queries. They allow arbitrary code execution on the server.
  • Object-injection in queries. A user-controlled {$ne: null} bypasses equality filters. Sanitize: use mongo-sanitize middleware or validate the shape of query objects.
  • Connection string in env. MONGO_URL in secret management — never commit credentials.
  • TLS required. Atlas requires TLS by default; self-hosted Mongo must be configured (--tlsMode requireTLS). Pass tls: true to mongoose.connect.
  • Least-privilege users. App role with readWrite on its database, separate dbAdmin for migrations.
  • Don't return raw documents. Mongoose's toJSON / toObject lets you strip sensitive fields (password, secret).
  • Bound find() cardinality. Always pair find() with .limit() or pagination; an unbounded scan denial-of-services your DB.
  • Audit mongoose-paginate-v2 for unbounded limit. Reject ?limit=100000.

Testing & CI integration

mongodb-memory-server spins up a temporary mongod for tests — fast, no Docker, full feature parity (including replica sets in MongoMemoryReplSet).

typescript
import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
import { beforeAll, afterAll } from "vitest";

let mongod: MongoMemoryServer;

beforeAll(async () => {
  mongod = await MongoMemoryServer.create();
  await mongoose.connect(mongod.getUri());
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongod.stop();
});

Output: each test suite runs against a fresh mongod; tears down at the end. Sub-second startup on modern machines.

For transactions, use MongoMemoryReplSet:

typescript
import { MongoMemoryReplSet } from "mongodb-memory-server";

const repl = await MongoMemoryReplSet.create({ replSet: { count: 1 } });
await mongoose.connect(repl.getUri());

Output: single-node replica set supporting transactions.

CI workflow:

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm test

Ecosystem integrations

ToolRole
typegooseDecorator-based TypeScript wrapper around mongoose.
nestjs/mongooseNestJS integration module.
connect-mongoExpress session store.
mongoose-paginate-v2Pagination helpers.
mongoose-autopopulateAuto-populate fields.
mongoose-deleteSoft deletes.
agendaJob scheduling on top of Mongo.
bullmq (with mongo-store)Job queues.
OpenTelemetry mongodb instrumentationSpans for queries.

Troubleshooting common errors

MongooseServerSelectionError — can't reach the cluster. Verify DNS / SRV records, firewall, and IP allow-list (Atlas). The error message usually shows which hosts were tried.

ValidationError — schema validation failed. Inspect err.errors for field-by-field details.

E11000 duplicate key error — unique index violation. Catch and translate to 409 Conflict.

StrictPopulateError — populating a path not declared in the schema. Either declare the ref, or pass { strictPopulate: false }.

MongoExpiredSessionError — session leak. Always endSession() in finally, or use withTransaction which manages it.

Buffering timed out — Mongoose buffered an operation but the connection never opened. Connect before using models, or set bufferCommands: false to fail fast.

**Cannot overwrite \User` model** — calling model("User", schema)twice. Hot-reload duplicates the model; gate withmongoose.models.User || mongoose.model("User", schema)`.

Transaction numbers are only allowed on a replica set — running transactions against standalone mongod. Run a replica set or use Atlas.

Slow populate — N+1 fan-out. Switch to $lookup aggregation, or populate selectively with select.

When NOT to use this

  • Edge runtimes. Mongoose / mongodb driver uses Node sockets — won't run on Cloudflare Workers. Use Atlas Data API or HTTP-fronted alternatives.
  • Relational data with joins. MongoDB joins via $lookup are slower than SQL joins and you lose foreign-key integrity. Choose Postgres if the domain is relational.
  • You need typed end-to-end without runtime overhead. Prisma (Mongo provider) generates static types; Mongoose's runtime schemas add cost.
  • High-throughput writes. Mongoose's pre/post hooks add per-document overhead. For >50k writes/s, use the raw driver with insertMany.
  • Embedded analytics. MongoDB aggregation is powerful but not OLAP-fast. Reach for ClickHouse / Postgres for analytical workloads.
  • You want the smallest possible dep tree. Mongoose pulls a handful of transitive deps. The raw driver is leaner.

See also