From mongoose-consistency
Write, review, refactor, or debug Node.js MongoDB code with Mongoose (schemas, models, queries, populate, middleware, connections) using one canonical, modern async/await idiom set. Use this skill whenever code defines Mongoose schemas, queries or updates documents, or when the user hits "Model.find() no longer accepts a callback", findOneAndUpdate returning stale documents, validators not running on updates, duplicate key E11000 errors despite unique: true "validation", ObjectId === comparisons failing, or asks lean() vs documents or save vs update middleware. Trigger it even when the user just says "store this in MongoDB" in a Node project using Mongoose — without saying the words "Mongoose idioms."
How this skill is triggered — by the user, by Claude, or both
Slash command
/mongoose-consistency:mongoose-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Mongoose 7 **removed the callback API** that saturates training data — every
Mongoose 7 removed the callback API that saturates training data — every
Model.find({}, cb) now throws — and the update-path semantics (no save middleware, no
validators, old-doc returns) keep silently diverging from what schema authors expect.
This skill pins the canonical idiom set for Mongoose 7/8: async/await only, explicit
update options, deliberate lean/document choice.
| Always | Never | Why |
|---|---|---|
await User.findOne({ email }) | User.findOne({ email }, (err, user) => ...) | Callbacks were removed in 7 — they throw MongooseError, they don't degrade. |
one await mongoose.connect(uri) at startup; listen on connection.on("error"/"disconnected") | connecting per request / ignoring connection events | The connection is a pooled singleton; per-request connects exhaust pools. |
timestamps: true in schema options | manual createdAt: Date.now fields | Built-in, index-friendly, updated on save automatically. |
.lean() for read-only queries | hydrating full documents to serialize JSON | lean returns POJOs (~5× faster, less memory) — but no getters/virtuals/methods/save; choose knowingly. |
findOneAndUpdate(filter, update, { new: true, runValidators: true }) | default options | Default returns the pre-update doc and skips validators — both silent. |
know the middleware split: pre("save") runs on save()/create() only; update ops need pre("findOneAndUpdate") | assuming save hooks cover updates | Password-hash hooks on save famously miss findOneAndUpdate password changes. |
id1.equals(id2) (or compare .toString()) | id1 === id2 for ObjectIds | ObjectIds are objects; === compares references — always false across queries. |
handle uniqueness at write time: catch err.code === 11000 (and create the index!) | treating unique: true as a validator | unique is an index definition — no index built (or race) means duplicates; it never produces a ValidationError. |
projections for reads: .select("name email") / .select("-passwordHash") | shipping whole documents to clients | Allowlists prevent the leaked-hash class of bug. |
Model.deleteOne/deleteMany/updateOne/updateMany/countDocuments | removed remove(), update(), count() | Removed/deprecated across 6–8; the modern names are explicit about cardinality. |
House style:
import mongoose from "mongoose";
const userSchema = new mongoose.Schema(
{
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
name: { type: String, required: true },
role: { type: String, enum: ["user", "admin"], default: "user" },
passwordHash: { type: String, required: true, select: false },
},
{ timestamps: true },
);
userSchema.pre("save", async function () {
if (this.isModified("passwordHash")) {
this.passwordHash = await argon2.hash(this.passwordHash);
}
});
export const User = mongoose.model("User", userSchema);
// usage
const users = await User.find({ role: "user" })
.select("name email createdAt")
.sort({ createdAt: -1 })
.limit(20)
.lean();
const updated = await User.findOneAndUpdate(
{ _id: id },
{ $set: { name } },
{ new: true, runValidators: true },
);
runValidators: true, update validation
only checks paths in the update, context: "query" is needed for some custom
validators, and required can't catch $unset tricks — schema-critical invariants
belong in code or save-path flows..then() twice executes
twice; passing queries around un-awaited re-executes them. .exec() returns a true
promise (and better stack traces).Model.create(doc) vs new Model(doc).save() are equivalent — but
create([docs], { session }) array form is what supports transactions.strictQuery filtering changed defaults across 6→7→8: an unknown filter field can
match everything. Pin mongoose.set("strictQuery", ...) explicitly.$lookup (aggregate) or restructure. And populated
fields on .lean() docs are plain objects — no .equals, no methods.bufferCommands: false in serverless.Schema.Types.Mixed/plain {} fields: changes aren't tracked —
markModified(path) or the change never saves.items.0.qty) bypass validators
silently; prefer $ positional operators with arrayFilters, or load-modify-save.maxPoolSize tuned down.Target Mongoose 7/8. The 7.0 line removed callbacks and remove(); 8.0 mostly
refines (e.g. findOneAndUpdate includeResultMetadata replacing rawResult,
strictQuery default back to false). Removed/renamed family to purge from generated
code: update() → updateOne/Many, count() → countDocuments/estimatedDocumentCount,
findOneAndRemove → findOneAndDelete, callback everything → await.
select: false on secrets.schema.index(...) and verify they exist in prod)?new/runValidators and matching query middleware) vs
transaction (sessions) for multi-document invariants.ValidationError → 400 with per-path messages; code === 11000 → 409;
CastError (bad ObjectId) → 404/400.{ new: true, runValidators },
save-hook assumptions on updates, === on ObjectIds, unique-as-validation, full-doc
responses, per-request connects.For the removed-API table, middleware matrix, populate/$lookup decision, transaction
recipes, and index management, read references/mongoose-patterns.md.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub guidogl/mongoose-consistency --plugin mongoose-consistency