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
npm install mongoose
Output: added mongoose to dependencies
pnpm add mongoose
Output: added 1 package, linked from store
yarn add mongoose
Output: added mongoose
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+. Requiresmongodb@6driver. Refined types (notablyHydratedDocument,InferSchemaType). Strict-populate by default.mongoose@7— Node 14.20+. Drivermongodb@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 paginatorsmongoose-autopopulate— auto-populateconfigured per-schemamongoose-aggregate-paginate-v2— pagination over aggregation pipelinesmongoose-delete— soft deletesmongoose-unique-validator— friendlier unique error messagesmongoose-lean-virtuals— virtuals on.lean()queriesconnect-mongo— Express session store backed by Mongoagenda/bullmq— job queues that pair with Mongo or Redis storage
Alternatives
| Approach | Trade-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. |
typegoose | TypeScript-class-decorator wrapper around mongoose. Same engine, classier ergonomics. |
mikro-orm | Identity-map ORM; supports Mongo. Strong for hybrid SQL+Mongo apps. |
mongolass / monk | Thin 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.
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.
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.
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.
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.
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.
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.
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
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()(orModel.find().explain()) shows whether an index is used. - Avoid
populatein tight loops. Each call is a separate query; for fan-out >100, switch to$lookupin an aggregation. - Limit cursor sizes.
await Model.find({})materializes everything; for >10k results, use.cursor()or.stream(). bulkWritefor 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: trueon large pipelines to allow Mongo to spill to disk instead of erroring at the 100 MB sort limit. maxTimeMSper query. Bounds runaway scans.Model.find().maxTimeMS(2000).
Version migration guide
From mongoose@7 to mongoose@8
- Node 16.20+ required.
- Underlying driver bumped to
mongodb@6— review any direct driver usage. - Strict populate is default —
populate("missingField")throws instead of silently returning unpopulated. Pass{ strictPopulate: false }to keep the old behaviour. findOneAndUpdatereturns the updated document by default (was the original under v6).- Type signatures tightened —
HydratedDocument<T>is the canonical document type.Document<T>still exists butHydratedDocumentis now preferred. - Removed deprecated
Model.update()(useupdateOne/updateMany).
Migration sequence:
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
Model.remove()removed — usedeleteOne/deleteMany.- Callback API removed everywhere — convert remaining
.exec(cb)calls toawait. mongoose.connect()returns the connection promise, notundefined.
Security considerations
- Always validate inputs at the schema. Mongoose's
required,match,min,max,enumetc. are your first line. Don't store user input untyped. - Avoid
$whereand JavaScript-eval queries. They allow arbitrary code execution on the server. - Object-injection in queries. A user-controlled
{$ne: null}bypasses equality filters. Sanitize: usemongo-sanitizemiddleware or validate the shape of query objects. - Connection string in env.
MONGO_URLin secret management — never commit credentials. - TLS required. Atlas requires TLS by default; self-hosted Mongo must be configured (
--tlsMode requireTLS). Passtls: truetomongoose.connect. - Least-privilege users. App role with
readWriteon its database, separatedbAdminfor migrations. - Don't return raw documents. Mongoose's
toJSON/toObjectlets you strip sensitive fields (password,secret). - Bound
find()cardinality. Always pairfind()with.limit()or pagination; an unbounded scan denial-of-services your DB. - Audit
mongoose-paginate-v2for unboundedlimit. 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).
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:
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:
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
| Tool | Role |
|---|---|
typegoose | Decorator-based TypeScript wrapper around mongoose. |
nestjs/mongoose | NestJS integration module. |
connect-mongo | Express session store. |
mongoose-paginate-v2 | Pagination helpers. |
mongoose-autopopulate | Auto-populate fields. |
mongoose-delete | Soft deletes. |
agenda | Job scheduling on top of Mongo. |
bullmq (with mongo-store) | Job queues. |
OpenTelemetry mongodb instrumentation | Spans 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
$lookupare 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
- JavaScript: Node runtime — async I/O, process model
- Concept: async — promises, queues, sessions
- Concept: json — BSON / JSON serialization boundaries