Claude Skill

api-database-mongoose

MongoDB ODM with schemas, validation, middleware, and TypeScript support

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-database-mongoose_skills_api-database-mongoose-3a51ef5.zip · 23 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-mongoose/skills/api-database-mongoose
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Mongoose ODM Patterns

Quick Guide: Use Mongoose as the ODM layer for MongoDB. Let TypeScript infer types from schema definitions instead of duplicating interfaces. Register all middleware before calling model() -- hooks added after compilation are silently ignored. Use .lean() for any read-only query. Pass { session } to every operation inside a transaction or enable transactionAsyncLocalStorage. Prefer session.withTransaction() over manual commit/abort. Use 127.0.0.1 instead of localhost in connection strings (Node.js 18+ IPv6 preference causes timeouts).


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST define all middleware (pre/post hooks) BEFORE calling model() -- hooks registered after model compilation are silently ignored with no error)

(You MUST pass { session } to EVERY operation inside a transaction -- missing session causes that operation to run outside the transaction silently)

(You MUST use .lean() for read-only queries returning API responses -- skipping lean wastes 3x memory on hydration overhead)

(You MUST use 127.0.0.1 instead of localhost in connection strings -- Node.js 18+ prefers IPv6 and localhost causes connection timeouts)

(You MUST NOT use findOneAndUpdate/updateOne and expect pre('save') to fire -- only save() and create() trigger document middleware)

(You MUST NOT use next() callbacks in pre hooks on Mongoose 9 -- use async/await instead; next() was removed in v9)

</critical_requirements>


Auto-detection: Mongoose, mongoose, mongoose.connect, Schema, model, ObjectId, populate, HydratedDocument, InferSchemaType, InferRawDocType, pre('save'), post('save'), lean, mongoose.startSession, withTransaction, discriminator, virtual, Schema.Types.ObjectId, Types.ObjectId

When to use:

  • Defining MongoDB schemas and models with Mongoose
  • TypeScript integration with schema type inference
  • Middleware hooks (pre/post save, validate, find, delete)
  • Population (resolving references between collections)
  • Transactions with session management
  • Virtuals and instance/static methods
  • Discriminators (single collection inheritance)
  • Connection management (single and multi-database)

Key patterns covered:

  • Schema definition with automatic TypeScript inference
  • TypeScript typing (HydratedDocument, InferSchemaType, methods/statics/virtuals generics)
  • CRUD operations (create, find, update, delete, lean vs hydrated)
  • Middleware hooks and their execution rules
  • Population with field selection and limits
  • Transactions (withTransaction, transactionAsyncLocalStorage)
  • Validation (built-in validators, custom validators, error messages)
  • Virtuals (computed, populate virtuals)
  • Discriminators (inheritance pattern)
  • Connection setup and multi-database

When NOT to use:

  • Raw MongoDB driver queries without schema enforcement (use the native driver)
  • Relational data with complex joins and foreign key constraints (use a relational database)
  • Simple key-value storage (use a dedicated key-value store)

Detailed Resources:

  • For decision frameworks, quick reference tables, and migration notes, see reference.md

Core Patterns:

  • examples/core.md -- Connection, schema definition, TypeScript typing, model creation, CRUD, validation

Middleware & Lifecycle:

Relationships & Population:

Transactions & Advanced:




<red_flags>

RED FLAGS

High Priority Issues:

  • Registering middleware after model() call -- hooks are silently ignored, no error thrown
  • Running operations in parallel inside a transaction (Promise.all()) -- MongoDB does not support parallel operations within a single transaction session
  • Missing { session } on any operation inside a transaction -- that operation runs outside the transaction silently
  • Using localhost in connection strings on Node.js 18+ -- IPv6 preference causes connection timeouts, use 127.0.0.1
  • Mutating a document fetched with .lean() and calling .save() -- lean returns plain objects without Mongoose methods

Medium Priority Issues:

  • Using findOneAndUpdate/updateOne and expecting pre('save') to fire -- only save() and create() trigger document middleware
  • Unbounded .populate() without limit or field selection -- can return thousands of documents per populate call, each is a separate DB round-trip
  • Not passing runValidators: true on findOneAndUpdate -- schema validation is skipped by default on direct updates
  • Using Schema.Types.ObjectId in TypeScript interfaces -- use Types.ObjectId for interfaces, Schema.Types.ObjectId for schema definitions only
  • Creating indexes in production application code instead of migration scripts -- index builds can lock the collection

Common Mistakes:

  • Forgetting { new: true } on findOneAndUpdate -- returns the old document by default, not the updated one
  • Using next() callbacks in pre hooks on Mongoose 9 -- next() was removed in v9, use async/await
  • Not handling duplicate key errors (error code 11000) from unique indexes
  • Using .lean() on write operations -- lean is for reads only
  • Checking doc.isNew in post('save') hooks -- always false after save; capture in pre('save') via this.$locals.wasNew
  • Defining the same middleware hook multiple times without realizing they stack (all run, not just the last one)
  • Using extends Document on interfaces -- deprecated pattern that breaks type inference for lean documents and query filters

Gotchas & Edge Cases:

  • MongoDB has a 16 MB document size limit -- deeply embedded arrays can silently hit this
  • Mongoose buffers all operations until connected -- queries queue silently if connection fails, which can mask connection issues in development
  • deleteOne/deleteMany on the Model do not trigger document pre('deleteOne') middleware -- they trigger query middleware instead; use doc.deleteOne() for document middleware
  • Virtual properties are excluded from toJSON()/toObject() by default -- set { toJSON: { virtuals: true } } in schema options or they disappear in API responses
  • insertMany() does not trigger save middleware -- it triggers insertMany model middleware only
  • Mongoose 9 renamed FilterQuery to QueryFilter -- update TypeScript imports if upgrading
  • Mongoose 9 disallows pipeline-style updates by default -- pass { updatePipeline: true } or they throw
  • create() with an array requires array syntax for { session }: Model.create([data], { session }) -- the non-array form Model.create(data, { session }) does not work in transactions

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST define all middleware (pre/post hooks) BEFORE calling model() -- hooks registered after model compilation are silently ignored with no error)

(You MUST pass { session } to EVERY operation inside a transaction -- missing session causes that operation to run outside the transaction silently)

(You MUST use .lean() for read-only queries returning API responses -- skipping lean wastes 3x memory on hydration overhead)

(You MUST use 127.0.0.1 instead of localhost in connection strings -- Node.js 18+ prefers IPv6 and localhost causes connection timeouts)

(You MUST NOT use findOneAndUpdate/updateOne and expect pre('save') to fire -- only save() and create() trigger document middleware)

(You MUST NOT use next() callbacks in pre hooks on Mongoose 9 -- use async/await instead; next() was removed in v9)

Failure to follow these rules will cause silent middleware bypass, transaction isolation failures, or connection timeouts.

</critical_reminders>

Files (skills)
  • examples
    • core.md 16.6 KB
      # Mongoose - Core Examples
      
      > Connection, schema definition, TypeScript typing, model creation, CRUD, and validation. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Middleware & lifecycle:** See [middleware.md](middleware.md). **Population & relationships:** See [population.md](population.md). **Transactions & advanced:** See [transactions.md](transactions.md).
      
      ---
      
      ## Pattern 1: Connection Setup
      
      ### Good Example -- Production Connection
      
      ```typescript
      import mongoose from "mongoose";
      
      const POOL_SIZE_MAX = 10;
      const POOL_SIZE_MIN = 2;
      const SERVER_SELECTION_TIMEOUT_MS = 5000;
      const SOCKET_TIMEOUT_MS = 45000;
      
      async function connectDatabase(): Promise<typeof mongoose> {
        const uri = process.env.MONGODB_URI;
        if (!uri) {
          throw new Error("MONGODB_URI environment variable is required");
        }
      
        try {
          const connection = await mongoose.connect(uri, {
            maxPoolSize: POOL_SIZE_MAX,
            minPoolSize: POOL_SIZE_MIN,
            serverSelectionTimeoutMS: SERVER_SELECTION_TIMEOUT_MS,
            socketTimeoutMS: SOCKET_TIMEOUT_MS,
            retryWrites: true,
            retryReads: true,
          });
      
          console.log(`Connected to MongoDB: ${mongoose.connection.name}`);
          return connection;
        } catch (error) {
          console.error("Failed to connect to MongoDB:", error);
          throw error;
        }
      }
      
      export { connectDatabase };
      ```
      
      **Why good:** Environment variable for URI, named constants for all numeric values, try/catch for initial connection, typed return
      
      ### Good Example -- Connection Events and Graceful Shutdown
      
      ```typescript
      function setupConnectionEvents(): void {
        mongoose.connection.on("connected", () => {
          console.log("MongoDB connected");
        });
      
        mongoose.connection.on("error", (err) => {
          console.error("MongoDB connection error:", err);
        });
      
        mongoose.connection.on("disconnected", () => {
          console.warn("MongoDB disconnected");
        });
      }
      
      async function disconnectDatabase(): Promise<void> {
        await mongoose.connection.close();
        console.log("MongoDB connection closed");
      }
      
      process.on("SIGINT", async () => {
        await disconnectDatabase();
        process.exit(0);
      });
      
      export { setupConnectionEvents, disconnectDatabase };
      ```
      
      **Why good:** Handles all critical lifecycle events, different log levels for severity, graceful shutdown on SIGINT
      
      ### Good Example -- Multiple Connections (Multi-Database)
      
      ```typescript
      import mongoose from "mongoose";
      
      // createConnection() returns a separate Connection object
      // Each connection has its own pool, models, and middleware
      const primaryDb = await mongoose
        .createConnection(process.env.PRIMARY_MONGODB_URI!)
        .asPromise();
      
      const analyticsDb = await mongoose
        .createConnection(process.env.ANALYTICS_MONGODB_URI!)
        .asPromise();
      
      // Models are bound to their specific connection
      const User = primaryDb.model("User", userSchema);
      const AnalyticsEvent = analyticsDb.model("AnalyticsEvent", eventSchema);
      
      export { User, AnalyticsEvent };
      ```
      
      **Why good:** Separate pools for different workloads, `.asPromise()` for await support, models explicitly bound to connections
      
      ### Bad Example -- Hardcoded Connection
      
      ```typescript
      // BAD: Everything wrong
      mongoose.connect("mongodb://admin:password123@localhost:27017/mydb");
      ```
      
      **Why bad:** Hardcoded credentials in source code, `localhost` fails on Node.js 18+ (IPv6 preference), no pool configuration, no error handling
      
      ---
      
      ## Pattern 2: Schema Definition with Validation
      
      ### Good Example -- Complete Schema
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      const MIN_NAME_LENGTH = 2;
      const MAX_NAME_LENGTH = 100;
      const MIN_PRICE = 0;
      const SKU_PATTERN = /^[A-Z]{2}-\d{6}$/;
      
      const productSchema = new Schema(
        {
          name: {
            type: String,
            required: [true, "Product name is required"],
            minlength: [
              MIN_NAME_LENGTH,
              `Name must be at least ${MIN_NAME_LENGTH} characters`,
            ],
            maxlength: [
              MAX_NAME_LENGTH,
              `Name must be at most ${MAX_NAME_LENGTH} characters`,
            ],
            trim: true,
            index: true,
          },
          sku: {
            type: String,
            required: true,
            unique: true,
            uppercase: true,
            match: [SKU_PATTERN, "SKU must match format XX-000000"],
          },
          price: {
            type: Number,
            required: true,
            min: [MIN_PRICE, "Price cannot be negative"],
            validate: {
              validator: (v: number) => Number.isFinite(v),
              message: "Price must be a finite number",
            },
          },
          category: {
            type: String,
            required: true,
            enum: {
              values: ["electronics", "clothing", "food", "books"] as const,
              message: "{VALUE} is not a valid category",
            },
          },
          tags: { type: [String], default: [] },
          specifications: { type: Map, of: Schema.Types.Mixed },
          isActive: { type: Boolean, default: true },
        },
        {
          timestamps: true,
          toJSON: { virtuals: true },
          toObject: { virtuals: true },
        },
      );
      
      const Product = model("Product", productSchema);
      export { Product, productSchema };
      ```
      
      **Why good:** Named constants for validation limits, custom error messages on every validator, `as const` preserves enum literal types, trim/uppercase transforms, Map for flexible key-value data, schema options for timestamps and virtual serialization
      
      ### Good Example -- Subdocument Schema
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      const addressSchema = new Schema(
        {
          street: { type: String, required: true },
          city: { type: String, required: true },
          state: { type: String, required: true },
          zipCode: { type: String, required: true, match: /^\d{5}(-\d{4})?$/ },
          country: { type: String, default: "US" },
        },
        { _id: false }, // No separate _id for embedded subdocuments
      );
      
      const customerSchema = new Schema(
        {
          name: { type: String, required: true },
          email: { type: String, required: true, unique: true, lowercase: true },
          shippingAddress: { type: addressSchema, required: true },
          billingAddress: { type: addressSchema },
          addresses: {
            type: [addressSchema],
            validate: {
              validator: (v: unknown[]) => v.length <= 10,
              message: "Maximum 10 addresses allowed",
            },
          },
        },
        { timestamps: true },
      );
      
      const Customer = model("Customer", customerSchema);
      export { Customer, customerSchema, addressSchema };
      ```
      
      **Why good:** Reusable subdocument schema, `{ _id: false }` avoids unnecessary ObjectIds on embedded documents, array-level validation to bound the array size, schema reused for both shipping and billing
      
      ### Bad Example -- No Validation
      
      ```typescript
      // BAD: No validation, no types, no constraints
      const userSchema = new Schema({
        name: String,
        email: String,
        age: Number,
        role: String,
      });
      ```
      
      **Why bad:** No `required` constraints (all fields optional), no validation rules, no enum for role, no custom error messages, no trim/lowercase transforms
      
      ---
      
      ## Pattern 3: TypeScript -- Automatic Inference
      
      ### Good Example -- Simple Model (Preferred)
      
      ```typescript
      import { Schema, model, type InferSchemaType } from "mongoose";
      
      const blogPostSchema = new Schema(
        {
          title: { type: String, required: true },
          slug: { type: String, required: true, unique: true },
          content: { type: String, required: true },
          authorId: { type: Schema.Types.ObjectId, ref: "User", required: true },
          tags: [{ type: String }],
          isPublished: { type: Boolean, default: false },
          viewCount: { type: Number, default: 0 },
        },
        { timestamps: true },
      );
      
      // TypeScript automatically infers the document type
      const BlogPost = model("BlogPost", blogPostSchema);
      
      // Extract the inferred type for use in other files
      type BlogPostDoc = InferSchemaType<typeof blogPostSchema>;
      
      export { BlogPost, blogPostSchema };
      export type { BlogPostDoc };
      ```
      
      **Why good:** No manual interface duplication, TypeScript infers types from schema, `InferSchemaType` exports the shape for consumers, named exports
      
      ### Good Example -- Full Typing with Methods, Statics, and Virtuals
      
      ```typescript
      import {
        Schema,
        model,
        type HydratedDocument,
        type Model,
        type Types,
      } from "mongoose";
      
      const SALT_ROUNDS = 12;
      
      // 1. Raw document interface (what's stored in MongoDB)
      interface IUser {
        email: string;
        passwordHash: string;
        firstName: string;
        lastName: string;
        role: "admin" | "user" | "moderator";
        lastLoginAt?: Date;
      }
      
      // 2. Instance methods interface
      interface IUserMethods {
        comparePassword(candidate: string): Promise<boolean>;
        updateLastLogin(): Promise<void>;
      }
      
      // 3. Virtuals interface
      interface IUserVirtuals {
        fullName: string;
      }
      
      // 4. Static methods interface
      interface IUserStatics {
        findByEmail(email: string): Promise<UserDocument | null>;
      }
      
      // 5. Composed model type = Model + Statics
      type UserModel = Model<IUser, {}, IUserMethods, IUserVirtuals> & IUserStatics;
      
      // 6. Hydrated document type for external consumers
      type UserDocument = HydratedDocument<IUser, IUserMethods & IUserVirtuals>;
      
      // 7. Schema with all generic parameters
      const userSchema = new Schema<
        IUser,
        UserModel,
        IUserMethods,
        {},
        IUserVirtuals
      >(
        {
          email: { type: String, required: true, unique: true, lowercase: true },
          passwordHash: { type: String, required: true },
          firstName: { type: String, required: true, trim: true },
          lastName: { type: String, required: true, trim: true },
          role: {
            type: String,
            enum: ["admin", "user", "moderator"],
            default: "user",
          },
          lastLoginAt: { type: Date },
        },
        {
          timestamps: true,
          toJSON: {
            virtuals: true,
            transform(_doc, ret) {
              delete ret.passwordHash; // Never expose password hash
              return ret;
            },
          },
        },
      );
      
      // Instance methods
      userSchema.methods.comparePassword = async function (
        candidate: string,
      ): Promise<boolean> {
        // Use bcrypt.compare(candidate, this.passwordHash) in production
        return candidate === this.passwordHash;
      };
      
      userSchema.methods.updateLastLogin = async function (): Promise<void> {
        this.lastLoginAt = new Date();
        await this.save();
      };
      
      // Virtuals
      userSchema.virtual("fullName").get(function () {
        return `${this.firstName} ${this.lastName}`;
      });
      
      // Static methods
      userSchema.statics.findByEmail = function (email: string) {
        return this.findOne({ email: email.toLowerCase() });
      };
      
      // Middleware -- MUST be defined BEFORE model()
      userSchema.pre("save", async function () {
        if (this.isModified("passwordHash")) {
          // Hash password here (e.g., bcrypt.hash(this.passwordHash, SALT_ROUNDS))
        }
      });
      
      // model() -- AFTER all middleware, methods, virtuals, and statics
      const User = model<IUser, UserModel>("User", userSchema);
      
      export { User, userSchema };
      export type { IUser, IUserMethods, UserDocument };
      ```
      
      **Why good:** Separate interfaces for document/methods/virtuals/statics, correct generic parameter order on Schema and model, `HydratedDocument` type exported for consumers, password excluded from JSON, middleware registered before `model()`, named constants
      
      ### Bad Example -- Type Duplication
      
      ```typescript
      // BAD: Interface duplicates schema -- they drift apart
      interface IProduct {
        name: string; // says required
        price: number; // says required
      }
      
      const productSchema = new Schema({
        name: String, // actually optional -- no 'required'
        price: Number, // no validation
      });
      
      // Interface says name is always string, but schema allows undefined
      const Product = model<IProduct>("Product", productSchema);
      ```
      
      **Why bad:** Interface and schema define different constraints, they will drift out of sync, TypeScript thinks `name` is always defined but MongoDB allows undefined
      
      ---
      
      ## Pattern 4: ObjectId References
      
      ### Good Example -- Typed References
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      const commentSchema = new Schema(
        {
          postId: {
            type: Schema.Types.ObjectId, // Schema.Types.ObjectId in schema definition
            ref: "BlogPost",
            required: true,
            index: true,
          },
          authorId: {
            type: Schema.Types.ObjectId,
            ref: "User",
            required: true,
            index: true,
          },
          parentId: {
            type: Schema.Types.ObjectId,
            ref: "Comment",
            default: null,
          }, // Self-reference for threads
          content: { type: String, required: true, maxlength: 5000 },
          likes: { type: Number, default: 0 },
        },
        { timestamps: true },
      );
      
      // Compound index for common query pattern
      commentSchema.index({ postId: 1, createdAt: -1 });
      
      const Comment = model("Comment", commentSchema);
      export { Comment, commentSchema };
      ```
      
      **Why good:** `Schema.Types.ObjectId` in schema (not `Types.ObjectId`), `ref` for populate support, indexes on foreign keys, compound index for common query
      
      ### Bad Example -- Wrong ObjectId Type
      
      ```typescript
      import { Types } from "mongoose";
      
      // BAD: Using Types.ObjectId in schema definition
      const schema = new Schema({
        userId: { type: Types.ObjectId, ref: "User" }, // WRONG type for schema
      });
      ```
      
      **Why bad:** `Types.ObjectId` is for TypeScript interfaces and runtime values. `Schema.Types.ObjectId` is the correct type for schema definitions.
      
      ---
      
      ## Pattern 5: CRUD Operations
      
      ### Good Example -- Create
      
      ```typescript
      // Single document -- triggers pre('save') middleware
      const user = await User.create({
        name: "Alice",
        email: "alice@example.com",
        role: "admin",
      });
      
      // Bulk insert -- triggers insertMany middleware, NOT save middleware
      const BATCH_SIZE = 1000;
      const users = generateUsers(BATCH_SIZE);
      await User.insertMany(users, { ordered: false });
      // ordered: false continues inserting after errors (skips duplicates)
      ```
      
      ### Good Example -- Read with Lean
      
      ```typescript
      const PAGE_SIZE = 20;
      
      // Read-only response -- use lean()
      const user = await User.findById(id).lean();
      
      // Paginated list
      const activeAdmins = await User.find({ role: "admin", isActive: true })
        .select("name email role")
        .sort({ name: 1 })
        .limit(PAGE_SIZE)
        .lean();
      ```
      
      **Why good:** `.lean()` for read-only responses (3x memory savings), `.select()` for projection, named constant for page size
      
      ### Good Example -- Update
      
      ```typescript
      // Update with save() -- triggers pre('save') middleware
      const user = await User.findById(id);
      if (!user) {
        throw new Error(`User not found: ${id}`);
      }
      user.name = "Updated Name";
      await user.save();
      
      // Direct update -- does NOT trigger save middleware
      await User.findByIdAndUpdate(
        id,
        { $set: { name: "Updated" } },
        { new: true, runValidators: true },
      );
      
      // Bulk update
      await User.updateMany(
        { isActive: false },
        { $set: { archivedAt: new Date() } },
      );
      ```
      
      **Why good:** `save()` when middleware matters, `{ new: true }` returns updated document, `{ runValidators: true }` enforces schema validation on direct updates
      
      ### Good Example -- Delete
      
      ```typescript
      await User.findByIdAndDelete(id);
      
      const DAYS_TO_KEEP = 30;
      const cutoffDate = new Date(Date.now() - DAYS_TO_KEEP * 24 * 60 * 60 * 1000);
      await User.deleteMany({ isActive: false, archivedAt: { $lt: cutoffDate } });
      ```
      
      ### Bad Example -- Lean Then Save
      
      ```typescript
      // BAD: lean() returns plain objects -- no Mongoose methods
      const user = await User.findById(id).lean();
      user.name = "Updated";
      await user.save(); // TypeError: user.save is not a function
      ```
      
      **Why bad:** `.lean()` returns plain JavaScript objects without Mongoose methods. Cannot call `.save()`, `.populate()`, or any instance method.
      
      ### Bad Example -- Missing runValidators
      
      ```typescript
      // BAD: Schema validation skipped on direct updates by default
      await User.findByIdAndUpdate(id, {
        $set: { email: "not-an-email" }, // No validation! Saves invalid data
      });
      ```
      
      **Why bad:** `findByIdAndUpdate` skips schema validation by default. Always pass `{ runValidators: true }` to enforce validation on direct updates.
      
      ---
      
      ## Pattern 6: Schema Options
      
      ### Good Example -- Comprehensive Options
      
      ```typescript
      const auditLogSchema = new Schema(
        {
          action: { type: String, required: true },
          userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
          resource: { type: String, required: true },
          details: { type: Schema.Types.Mixed },
        },
        {
          timestamps: true,
          // Include virtuals in JSON and object output
          toJSON: { virtuals: true, versionKey: false },
          toObject: { virtuals: true },
          // Optimistic concurrency control (uses __v)
          optimisticConcurrency: true,
          // Explicit collection name (default: lowercase plural of model name)
          collection: "audit_logs",
          // Disable automatic index creation in production
          autoIndex: process.env.NODE_ENV !== "production",
        },
      );
      
      export { auditLogSchema };
      ```
      
      **Why good:** Virtuals in serialization, version key hidden from JSON, optimistic concurrency for safe concurrent updates, explicit collection name, autoIndex disabled in production (create indexes via migration scripts instead)
      
      ---
      
      _For middleware patterns, see [middleware.md](middleware.md). For population, see [population.md](population.md). For transactions, see [transactions.md](transactions.md)._
      
    • middleware.md 7.9 KB
      # Mongoose - Middleware & Lifecycle Examples
      
      > Pre/post hooks, error handling middleware, query middleware, soft delete, and middleware ordering. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Prerequisites**: Understand schema definition and model creation from [core.md](core.md).
      
      **Population:** See [population.md](population.md). **Transactions:** See [transactions.md](transactions.md).
      
      ---
      
      ## Pattern 1: Document Middleware (Pre/Post Save)
      
      ### Good Example -- Pre-Save with Conditional Logic
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      const SALT_ROUNDS = 12;
      
      const userSchema = new Schema({
        email: { type: String, required: true, unique: true },
        password: { type: String, required: true },
        name: { type: String, required: true },
        slug: { type: String, unique: true },
      });
      
      // Hash password only when modified -- Mongoose 9: async, no next()
      userSchema.pre("save", async function () {
        if (this.isModified("password")) {
          // this.password = await bcrypt.hash(this.password, SALT_ROUNDS);
        }
      });
      
      // Generate slug from name when name changes
      userSchema.pre("save", function () {
        if (this.isModified("name")) {
          this.slug = this.name.toLowerCase().replace(/\s+/g, "-");
        }
      });
      
      // MUST register all middleware BEFORE model()
      const User = model("User", userSchema);
      export { User };
      ```
      
      **Why good:** `isModified()` check prevents re-hashing on every save, Mongoose 9 async pattern (no `next()` callback), middleware defined before `model()` call
      
      ### Good Example -- Capturing isNew in Post-Save
      
      ```typescript
      // isNew is ALWAYS false in post('save') hooks
      // Mongoose sets it to false after successful save
      // Capture it in pre('save') via $locals
      
      userSchema.pre("save", function () {
        this.$locals.wasNew = this.isNew;
      });
      
      userSchema.post("save", function (doc) {
        if (doc.$locals.wasNew) {
          // Send welcome email only for new users
          console.log(`New user created: ${doc.email}`);
        }
      });
      ```
      
      **Why good:** `$locals` persists data between pre and post hooks on the same operation, solves the `isNew` gotcha where it's always false in post-save
      
      ### Bad Example -- Middleware After model()
      
      ```typescript
      const User = model("User", userSchema);
      
      // BAD: Registered AFTER model() -- SILENTLY IGNORED
      userSchema.pre("save", function () {
        this.updatedAt = new Date();
      });
      // No error thrown -- this hook will never execute
      ```
      
      **Why bad:** Middleware registered after `model()` compilation is silently ignored. No error, no warning -- data integrity silently broken.
      
      ### Bad Example -- Using next() on Mongoose 9
      
      ```typescript
      // BAD: next() removed in Mongoose 9
      userSchema.pre("save", function (next) {
        if (this.isModified("password")) {
          // ... hash password
        }
        next(); // TypeError in Mongoose 9
      });
      ```
      
      **Why bad:** Mongoose 9 removed `next()` callback from pre hooks. Use async/await or return a Promise instead.
      
      ---
      
      ## Pattern 2: Error Handling Middleware
      
      ### Good Example -- Transform Duplicate Key Errors
      
      ```typescript
      const DUPLICATE_KEY_ERROR = 11000;
      
      userSchema.post("save", function (error: any, _doc: any, next: Function) {
        if (error.name === "MongoServerError" && error.code === DUPLICATE_KEY_ERROR) {
          next(new Error("A user with this email already exists"));
        } else {
          next(error);
        }
      });
      ```
      
      **Why good:** Named constant for error code, transforms cryptic MongoDB duplicate key error into user-friendly message, passes through other errors unchanged
      
      ### Good Example -- Validation Error Formatting
      
      ```typescript
      userSchema.post("validate", function (error: any, _doc: any, next: Function) {
        if (error.name === "ValidationError") {
          const messages = Object.values(error.errors).map((err: any) => err.message);
          next(new Error(`Validation failed: ${messages.join(", ")}`));
        } else {
          next(error);
        }
      });
      ```
      
      **Why good:** Collects all validation error messages into a single readable string, preserves original error for non-validation errors
      
      ---
      
      ## Pattern 3: Query Middleware
      
      ### Good Example -- Soft Delete Filter
      
      ```typescript
      // Automatically exclude soft-deleted documents from all find queries
      userSchema.pre("find", function () {
        this.where({ deletedAt: { $exists: false } });
      });
      
      userSchema.pre("findOne", function () {
        this.where({ deletedAt: { $exists: false } });
      });
      
      userSchema.pre("countDocuments", function () {
        this.where({ deletedAt: { $exists: false } });
      });
      ```
      
      **Why good:** Consistent soft-delete filtering across all read operations, no application code needed to remember the filter
      
      ### Good Example -- Query Logging
      
      ```typescript
      const SLOW_QUERY_THRESHOLD_MS = 100;
      
      userSchema.pre("find", function () {
        this.set("startTime", Date.now());
      });
      
      userSchema.post("find", function (docs) {
        const startTime = this.get("startTime") as number;
        const duration = Date.now() - startTime;
        if (duration > SLOW_QUERY_THRESHOLD_MS) {
          console.warn(`Slow query (${duration}ms):`, this.getFilter());
        }
      });
      ```
      
      **Why good:** Named constant for threshold, measures actual query execution time, logs only slow queries with their filters
      
      ---
      
      ## Pattern 4: Complete Soft Delete Plugin
      
      ### Good Example -- Reusable Soft Delete
      
      ```typescript
      import { Schema, type Query } from "mongoose";
      
      interface ISoftDeletable {
        deletedAt?: Date;
        deletedBy?: string;
      }
      
      function applySoftDelete<T>(schema: Schema<T & ISoftDeletable>): void {
        // Add soft-delete fields to the schema
        schema.add({
          deletedAt: { type: Date, default: null },
          deletedBy: { type: String, default: null },
        });
      
        // Filter deleted documents from all queries by default
        schema.pre("find", function () {
          if (!this.getOptions().includeDeleted) {
            this.where({ deletedAt: null });
          }
        });
      
        schema.pre("findOne", function () {
          if (!this.getOptions().includeDeleted) {
            this.where({ deletedAt: null });
          }
        });
      
        schema.pre("countDocuments", function () {
          if (!this.getOptions().includeDeleted) {
            this.where({ deletedAt: null });
          }
        });
      
        // Add softDelete instance method
        schema.methods.softDelete = async function (deletedBy?: string) {
          this.deletedAt = new Date();
          this.deletedBy = deletedBy ?? null;
          return this.save();
        };
      
        // Add restore instance method
        schema.methods.restore = async function () {
          this.deletedAt = null;
          this.deletedBy = null;
          return this.save();
        };
      }
      
      export { applySoftDelete };
      export type { ISoftDeletable };
      
      // Usage:
      // const postSchema = new Schema({ title: String, content: String });
      // applySoftDelete(postSchema);
      // const Post = model("Post", postSchema);
      //
      // Normal queries auto-exclude deleted:
      //   await Post.find();
      // Include deleted with escape hatch:
      //   await Post.find().setOptions({ includeDeleted: true });
      ```
      
      **Why good:** Reusable plugin function, automatic filtering on all query types, escape hatch with `includeDeleted` option, `softDelete` and `restore` methods on documents, uses `save()` so middleware fires on soft-delete
      
      ---
      
      ## Pattern 5: Middleware Ordering and Stacking
      
      ### Important: Middleware Stacks, Not Replaces
      
      ```typescript
      // Multiple pre('save') hooks ALL run -- they don't replace each other
      userSchema.pre("save", function () {
        console.log("Hook 1 runs");
      });
      
      userSchema.pre("save", function () {
        console.log("Hook 2 also runs");
      });
      
      // Both hooks execute in registration order
      ```
      
      ### Execution Order for save()
      
      ```
      1. pre('validate')  -- schema validation hasn't run yet
      2. post('validate') -- schema validation passed
      3. pre('save')      -- document about to be written
      4. post('save')     -- document successfully written
      ```
      
      **Key insight:** `save()` automatically triggers `validate()` first. All `pre('validate')` and `post('validate')` hooks run before any `pre('save')` hook.
      
      ---
      
      ## Middleware Type Reference
      
      See [reference.md](../reference.md#middleware-execution-matrix) for the complete middleware execution matrix showing which operations trigger which hooks.
      
      ---
      
      _For core patterns, see [core.md](core.md). For population, see [population.md](population.md). For transactions, see [transactions.md](transactions.md)._
      
    • population.md 10.6 KB
      # Mongoose - Population & Relationships Examples
      
      > Populate, virtual populate, discriminators, and embedding vs referencing. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Prerequisites**: Understand schema definition and ObjectId references from [core.md](core.md).
      
      **Middleware:** See [middleware.md](middleware.md). **Transactions:** See [transactions.md](transactions.md).
      
      ---
      
      ## Pattern 1: Populate with Field Selection
      
      ### Good Example -- Selective Population
      
      ```typescript
      const MAX_COMMENTS = 50;
      
      const post = await BlogPost.findById(id)
        .populate("authorId", "name email avatar") // Only these fields from author
        .populate({
          path: "comments",
          select: "content authorId createdAt",
          options: {
            sort: { createdAt: -1 },
            limit: MAX_COMMENTS,
          },
          populate: {
            path: "authorId",
            select: "name avatar", // Nested populate for comment authors
          },
        })
        .lean();
      ```
      
      **Why good:** Field selection on every populate call reduces data transfer, limit on nested array prevents unbounded results, nested populate for deep references, sorted by recency
      
      ### Good Example -- Conditional Population
      
      ```typescript
      interface PopulateConfig {
        includeAuthor?: boolean;
        includeComments?: boolean;
      }
      
      async function getPost(id: string, config: PopulateConfig = {}) {
        let query = BlogPost.findById(id);
      
        if (config.includeAuthor) {
          query = query.populate("authorId", "name email");
        }
      
        if (config.includeComments) {
          query = query.populate({
            path: "comments",
            select: "content createdAt",
            options: { limit: 20, sort: { createdAt: -1 } },
          });
        }
      
        return query.lean();
      }
      
      export { getPost };
      ```
      
      **Why good:** Populate only when needed (each populate is a separate DB query), chainable query builder, avoids unnecessary round-trips
      
      ### Bad Example -- Unbounded Populate
      
      ```typescript
      // BAD: Populating all fields, no limits
      const post = await BlogPost.findById(id)
        .populate("authorId") // ALL author fields
        .populate("comments") // ALL comments -- could be thousands
        .populate("relatedPosts"); // ALL related posts
      
      // Each populate is a separate DB query with no bounds
      ```
      
      **Why bad:** No field selection wastes bandwidth and memory, no limit on comments could return thousands of documents, each populate adds a database round-trip
      
      ---
      
      ## Pattern 2: Virtual Populate (Reverse References)
      
      ### Good Example -- Populate Virtual
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      const authorSchema = new Schema(
        {
          name: { type: String, required: true },
          email: { type: String, required: true },
        },
        {
          toJSON: { virtuals: true },
          toObject: { virtuals: true },
        },
      );
      
      // Virtual populate -- look up posts where Post.authorId matches Author._id
      // No array stored on the author document
      authorSchema.virtual("posts", {
        ref: "Post",
        localField: "_id",
        foreignField: "authorId",
        options: { sort: { createdAt: -1 } },
      });
      
      // Virtual with count only (more efficient when you just need the count)
      authorSchema.virtual("postCount", {
        ref: "Post",
        localField: "_id",
        foreignField: "authorId",
        count: true, // Only returns the count, not the documents
      });
      
      const Author = model("Author", authorSchema);
      
      // Usage
      const author = await Author.findById(id)
        .populate("posts")
        .populate("postCount");
      // author.posts = Post[] (array of post documents)
      // author.postCount = number
      
      export { Author };
      ```
      
      **Why good:** Virtual populate for reverse relationships without storing an array on the parent, `count: true` for efficient counts, sort option for consistent ordering, schema options to include virtuals in serialization
      
      ---
      
      ## Pattern 3: Virtuals (Computed Properties)
      
      ### Good Example -- Getter and Setter Virtuals
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      const userSchema = new Schema(
        {
          firstName: { type: String, required: true },
          lastName: { type: String, required: true },
          email: { type: String, required: true },
        },
        {
          toJSON: { virtuals: true },
          toObject: { virtuals: true },
        },
      );
      
      // Read-only virtual
      userSchema.virtual("fullName").get(function () {
        return `${this.firstName} ${this.lastName}`;
      });
      
      // Virtual with getter AND setter
      userSchema
        .virtual("displayName")
        .get(function () {
          return `${this.firstName} ${this.lastName}`;
        })
        .set(function (fullName: string) {
          const parts = fullName.split(" ");
          this.firstName = parts[0] ?? "";
          this.lastName = parts.slice(1).join(" ");
        });
      
      // Computed virtual from another field
      userSchema.virtual("emailDomain").get(function () {
        return this.email.slice(this.email.indexOf("@") + 1);
      });
      
      const User = model("User", userSchema);
      export { User };
      ```
      
      **Why good:** Schema options enable virtuals in JSON/object serialization (otherwise they're invisible in API responses), getter + setter for two-way virtual, multiple computed virtuals
      
      ### Important Gotcha
      
      ```typescript
      // BAD: Forgetting toJSON option -- virtuals disappear in API responses
      const schema = new Schema({ firstName: String, lastName: String });
      schema.virtual("fullName").get(function () {
        return `${this.firstName} ${this.lastName}`;
      });
      
      const doc = await Model.findById(id);
      JSON.stringify(doc); // { firstName: "John", lastName: "Doe" } -- no fullName!
      ```
      
      **Why bad:** Virtual properties are excluded from `toJSON()`/`toObject()` by default. Must set `{ toJSON: { virtuals: true } }` in schema options.
      
      ---
      
      ## Pattern 4: Discriminators (Single Collection Inheritance)
      
      ### Good Example -- Event System with Discriminators
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      // Base event schema
      const eventSchema = new Schema(
        {
          userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
          timestamp: { type: Date, default: Date.now, index: true },
          metadata: { type: Schema.Types.Mixed },
        },
        {
          discriminatorKey: "eventType",
          timestamps: true,
        },
      );
      
      const Event = model("Event", eventSchema);
      
      // Discriminator for page views -- adds type-specific fields
      const PageViewEvent = Event.discriminator(
        "PageView",
        new Schema({
          url: { type: String, required: true },
          referrer: { type: String },
          duration: { type: Number }, // seconds
        }),
      );
      
      // Discriminator for purchases
      const PurchaseEvent = Event.discriminator(
        "Purchase",
        new Schema({
          orderId: { type: Schema.Types.ObjectId, ref: "Order", required: true },
          amount: { type: Number, required: true },
          currency: { type: String, default: "USD" },
        }),
      );
      
      // Query all events (returns mixed types)
      const allEvents = await Event.find({ userId: someUserId });
      
      // Query only purchases (returns only PurchaseEvent documents)
      const MIN_PURCHASE_AMOUNT = 100;
      const purchases = await PurchaseEvent.find({
        userId: someUserId,
        amount: { $gte: MIN_PURCHASE_AMOUNT },
      });
      
      export { Event, PageViewEvent, PurchaseEvent };
      ```
      
      **Why good:** Single collection for all events (efficient indexing on shared fields), `discriminatorKey` specifies which field stores the type, each subtype has its own schema and validation, can query base type for all events or specific type for filtered results
      
      ---
      
      ## Pattern 5: Embedding vs Referencing
      
      ### Good Example -- Embedding (Co-Accessed, Bounded Data)
      
      ```typescript
      import { Schema, model } from "mongoose";
      
      // Addresses are always accessed with the user and bounded (few per user)
      const userSchema = new Schema(
        {
          name: { type: String, required: true },
          email: { type: String, required: true, unique: true },
          addresses: [
            {
              label: { type: String, required: true }, // "home", "work"
              street: { type: String, required: true },
              city: { type: String, required: true },
              state: { type: String, required: true },
              zipCode: { type: String, required: true },
              isDefault: { type: Boolean, default: false },
            },
          ],
          preferences: {
            theme: {
              type: String,
              enum: ["light", "dark"] as const,
              default: "light",
            },
            language: { type: String, default: "en" },
          },
        },
        { timestamps: true },
      );
      
      const User = model("User", userSchema);
      export { User };
      ```
      
      **Why good:** Addresses always accessed with user, bounded (few per user), never shared between users, single read fetches everything
      
      ### Good Example -- Referencing (Independent, Unbounded Data)
      
      ```typescript
      // Posts are independent, unbounded, and accessed separately from the user
      const postSchema = new Schema(
        {
          authorId: {
            type: Schema.Types.ObjectId,
            ref: "User",
            required: true,
            index: true, // Index on foreign key for efficient lookups
          },
          title: { type: String, required: true },
          content: { type: String, required: true },
          tags: [{ type: String }],
        },
        { timestamps: true },
      );
      
      const Post = model("Post", postSchema);
      export { Post };
      ```
      
      **Why good:** Posts grow without limit, accessed independently, can be queried/paginated without loading the user, index on `authorId` for efficient lookups
      
      ### Good Example -- Denormalization (Hybrid Pattern)
      
      ```typescript
      // Store snapshot of frequently-accessed data in the parent
      const orderSchema = new Schema(
        {
          customerId: {
            type: Schema.Types.ObjectId,
            ref: "Customer",
            required: true,
          },
          // Denormalized snapshot avoids populate on every read
          customerSnapshot: {
            name: { type: String, required: true },
            email: { type: String, required: true },
          },
          items: [
            {
              productId: {
                type: Schema.Types.ObjectId,
                ref: "Product",
                required: true,
              },
              // Price at time of order (immutable -- not current price)
              name: { type: String, required: true },
              unitPrice: { type: Number, required: true },
              quantity: { type: Number, required: true, min: 1 },
            },
          ],
          total: { type: Number, required: true },
        },
        { timestamps: true },
      );
      
      export { orderSchema };
      ```
      
      **Why good:** Customer snapshot avoids populate on every order read, product price captured at order time (won't change if product price changes later), full customer data available via `customerId` when needed
      
      ### Bad Example -- Embedding Unbounded Data
      
      ```typescript
      // BAD: Embedding an unbounded array
      const userSchema = new Schema({
        name: String,
        posts: [
          {
            title: String,
            content: String, // Large text per post
            comments: [{ content: String, authorId: Schema.Types.ObjectId }],
          },
        ],
      });
      ```
      
      **Why bad:** Posts grow unbounded and hit 16 MB document size limit, deeply nested comments make querying impossible, updating one post requires reading/writing the entire user document
      
      ---
      
      _For core patterns, see [core.md](core.md). For middleware, see [middleware.md](middleware.md). For transactions, see [transactions.md](transactions.md)._
      
    • transactions.md 11.6 KB
      # Mongoose - Transactions & Advanced Examples
      
      > Sessions, withTransaction, transactionAsyncLocalStorage, connection management, cursor pagination, and indexes. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Prerequisites**: Understand schema definition and CRUD from [core.md](core.md).
      
      **Middleware:** See [middleware.md](middleware.md). **Population:** See [population.md](population.md).
      
      ---
      
      ## Pattern 1: Transactions with withTransaction()
      
      ### Good Example -- Transfer with Session
      
      ```typescript
      import mongoose from "mongoose";
      
      async function transferFunds(
        fromAccountId: string,
        toAccountId: string,
        amount: number,
      ): Promise<void> {
        const session = await mongoose.startSession();
      
        try {
          // withTransaction handles commit, abort, and retry automatically
          await session.withTransaction(async () => {
            // Debit source account (atomic check + update)
            const source = await Account.findOneAndUpdate(
              { _id: fromAccountId, balance: { $gte: amount } },
              { $inc: { balance: -amount } },
              { new: true, session }, // { session } on EVERY operation
            );
      
            if (!source) {
              throw new Error("Insufficient funds or account not found");
            }
      
            // Credit destination account
            const destination = await Account.findOneAndUpdate(
              { _id: toAccountId },
              { $inc: { balance: amount } },
              { new: true, session },
            );
      
            if (!destination) {
              throw new Error("Destination account not found");
            }
      
            // Record the transfer -- array syntax for create() with session
            await Transfer.create(
              [{ fromAccountId, toAccountId, amount, status: "completed" }],
              { session },
            );
          });
        } finally {
          await session.endSession(); // Always clean up
        }
      }
      
      export { transferFunds };
      ```
      
      **Why good:** `withTransaction()` handles commit/abort/retry automatically, `{ session }` passed to every operation, balance check with `$gte` prevents overdraft atomically, `finally` ensures session cleanup, `create()` uses array syntax for session support
      
      ### Bad Example -- Missing Session on Some Operations
      
      ```typescript
      // BAD: Not all operations have { session }
      const session = await mongoose.startSession();
      session.startTransaction();
      
      await Account.findOneAndUpdate(
        { _id: fromId },
        { $inc: { balance: -amount } },
        // Missing { session } -- runs OUTSIDE the transaction!
      );
      
      await Account.findOneAndUpdate(
        { _id: toId },
        { $inc: { balance: amount } },
        { session }, // Only this one is in the transaction
      );
      
      await session.commitTransaction();
      ```
      
      **Why bad:** First update runs outside the transaction and cannot be rolled back, inconsistent state if second update fails, no `endSession()` in finally block
      
      ### Bad Example -- Parallel Operations in Transaction
      
      ```typescript
      // BAD: MongoDB does not support parallel operations in a single session
      const session = await mongoose.startSession();
      await session.withTransaction(async () => {
        await Promise.all([
          Account.findOneAndUpdate(
            { _id: fromId },
            { $inc: { balance: -100 } },
            { session },
          ),
          Account.findOneAndUpdate(
            { _id: toId },
            { $inc: { balance: 100 } },
            { session },
          ),
        ]);
        // Error: "Cannot run operation with session that has ended"
      });
      ```
      
      **Why bad:** MongoDB does not support parallel operations within a single transaction session. Operations must be sequential within a transaction.
      
      ---
      
      ## Pattern 2: transactionAsyncLocalStorage (Mongoose 7.8+)
      
      ### Good Example -- Automatic Session Propagation
      
      ```typescript
      import mongoose from "mongoose";
      
      // Enable once at startup -- session is automatically injected into all operations
      mongoose.set("transactionAsyncLocalStorage", true);
      
      async function createOrderWithTransaction(
        orderData: OrderInput,
      ): Promise<void> {
        // connection.transaction() wraps the callback in AsyncLocalStorage
        await mongoose.connection.transaction(async () => {
          // No { session } needed -- automatically propagated via AsyncLocalStorage
          const order = await Order.create(orderData);
      
          await Product.updateMany(
            { _id: { $in: orderData.items.map((i) => i.productId) } },
            { $inc: { stock: -1 } },
          );
      
          await Customer.findByIdAndUpdate(orderData.customerId, {
            $push: { orderIds: order._id },
          });
          // If anything throws, ALL operations are rolled back
        });
      }
      
      export { createOrderWithTransaction };
      ```
      
      **Why good:** Eliminates manual `{ session }` passing (the biggest source of transaction bugs), cleaner code, no risk of forgetting session, automatic rollback on error
      
      **When to use:** Mongoose 7.8+ projects. Especially valuable when operations span multiple functions/services where passing session through all parameters is cumbersome.
      
      ### Good Example -- Connection.transaction() with Automatic State Reset
      
      ```typescript
      mongoose.set("transactionAsyncLocalStorage", true);
      
      async function updateWithRollbackSafety(userId: string): Promise<void> {
        const user = await User.findById(userId);
        if (!user) throw new Error("User not found");
      
        await mongoose.connection.transaction(async () => {
          user.name = "New Name";
          await user.save(); // Automatically uses the transaction session
      
          // If this throws, user.name reverts to the original value
          await AuditLog.create({ action: "name_change", userId });
        });
        // After successful transaction: user.name is "New Name"
        // After failed transaction: user.name is reverted by Mongoose
      }
      
      export { updateWithRollbackSafety };
      ```
      
      **Why good:** `connection.transaction()` integrates with Mongoose change tracking -- if the transaction aborts, Mongoose automatically resets document state
      
      ---
      
      ## Pattern 3: create() Array Syntax for Transactions
      
      ### Important Gotcha
      
      ```typescript
      // CORRECT: Array syntax for create() with session
      await User.create([{ name: "Alice", email: "alice@test.com" }], { session });
      
      // WRONG: Non-array syntax ignores the session option
      await User.create({ name: "Alice", email: "alice@test.com" }, { session });
      // The { session } is treated as a second document, NOT as options
      ```
      
      **Why this matters:** `Model.create(doc, options)` only accepts options when the first argument is an array. With a single object, the second argument is interpreted as another document to create. This is a common transaction bug where the operation appears to work but runs outside the transaction.
      
      ---
      
      ## Pattern 4: Cursor-Based Pagination
      
      ### Good Example -- Keyset Pagination (Efficient)
      
      ```typescript
      import mongoose from "mongoose";
      
      const PAGE_SIZE = 20;
      
      interface PaginationResult<T> {
        data: T[];
        nextCursor: string | null;
        hasMore: boolean;
      }
      
      async function getUsersWithCursor(
        cursor?: string,
      ): Promise<PaginationResult<Record<string, unknown>>> {
        const query: Record<string, unknown> = { isActive: true };
      
        if (cursor) {
          query._id = { $gt: new mongoose.Types.ObjectId(cursor) };
        }
      
        // Fetch one extra to determine hasMore without a separate count query
        const users = await User.find(query)
          .select("name email role createdAt")
          .sort({ _id: 1 })
          .limit(PAGE_SIZE + 1)
          .lean();
      
        const hasMore = users.length > PAGE_SIZE;
        const data = hasMore ? users.slice(0, PAGE_SIZE) : users;
        const nextCursor = hasMore ? String(data[data.length - 1]._id) : null;
      
        return { data, nextCursor, hasMore };
      }
      
      export { getUsersWithCursor };
      ```
      
      **Why good:** Cursor-based pagination avoids skip/limit performance issues on large collections, fetch one extra to determine hasMore without separate count query, consistent performance regardless of page depth
      
      ### Bad Example -- Deep Offset Pagination
      
      ```typescript
      // BAD: skip/limit degrades with page depth
      const PAGE_SIZE = 20;
      const page = 5000;
      
      const users = await User.find({ isActive: true })
        .skip((page - 1) * PAGE_SIZE) // Scans and discards 99,980 documents
        .limit(PAGE_SIZE);
      ```
      
      **Why bad:** MongoDB scans and discards all skipped documents, performance degrades linearly with page depth, page 5000 requires scanning ~100k documents
      
      ---
      
      ## Pattern 5: Cursor-Based Iteration for Large Datasets
      
      ### Good Example -- Processing Large Collections
      
      ```typescript
      const BATCH_LOG_INTERVAL = 1000;
      
      async function processAllOrders(): Promise<number> {
        let processed = 0;
      
        const cursor = Order.find({ status: "pending" })
          .sort({ createdAt: 1 })
          .cursor();
      
        for await (const order of cursor) {
          await processOrder(order);
          processed += 1;
      
          if (processed % BATCH_LOG_INTERVAL === 0) {
            console.log(`Processed ${processed} orders`);
          }
        }
      
        return processed;
      }
      
      export { processAllOrders };
      ```
      
      **Why good:** Cursor-based iteration keeps memory constant regardless of result size, `for await...of` for clean async iteration, progress logging at intervals
      
      ### Bad Example -- Loading Everything into Memory
      
      ```typescript
      // BAD: Loads ALL matching documents into memory at once
      const orders = await Order.find({ status: "pending" });
      // 1 million pending orders? Out of memory crash
      ```
      
      **Why bad:** Loads entire result set into memory, can crash with OOM for large collections
      
      ---
      
      ## Pattern 6: Indexes
      
      ### Good Example -- Compound Index (ESR Rule)
      
      ```typescript
      const orderSchema = new Schema({
        status: { type: String, enum: ["pending", "shipped", "delivered"] as const },
        customerId: { type: Schema.Types.ObjectId, ref: "Customer" },
        total: { type: Number },
        createdAt: { type: Date, default: Date.now },
      });
      
      // Query: { status: "shipped", total: { $gte: 100 } }, sort: { createdAt: -1 }
      // ESR Rule: Equality first, Sort second, Range last
      orderSchema.index({ status: 1, createdAt: -1, total: 1 });
      
      // Query: { customerId }, sort: { createdAt: -1 }
      orderSchema.index({ customerId: 1, createdAt: -1 });
      
      const Order = model("Order", orderSchema);
      export { Order };
      ```
      
      **Why good:** ESR (Equality-Sort-Range) rule maximizes index efficiency, separate compound index per query pattern
      
      ### Good Example -- TTL Index (Auto-Expiring Documents)
      
      ```typescript
      const SESSION_EXPIRY_SECONDS = 60 * 60 * 24; // 24 hours
      
      const sessionSchema = new Schema({
        userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
        token: { type: String, required: true, unique: true },
        createdAt: { type: Date, default: Date.now },
      });
      
      sessionSchema.index(
        { createdAt: 1 },
        { expireAfterSeconds: SESSION_EXPIRY_SECONDS },
      );
      
      const Session = model("Session", sessionSchema);
      export { Session };
      ```
      
      **Why good:** Named constant for expiry, TTL index for automatic cleanup by MongoDB background thread, no application-level cleanup needed
      
      ### Good Example -- Partial and Sparse Indexes
      
      ```typescript
      // Partial index: smaller, only indexes active records
      userSchema.index(
        { email: 1 },
        {
          partialFilterExpression: { isActive: true },
          unique: true, // Unique among active users only
        },
      );
      
      // Sparse index: allows multiple null/undefined values
      userSchema.index({ googleId: 1 }, { unique: true, sparse: true });
      ```
      
      **Why good:** Partial index is smaller and faster, uniqueness enforced only for active records, sparse index allows multiple users without googleId
      
      ### Index Best Practices
      
      1. **Disable autoIndex in production** -- set `{ autoIndex: false }` in schema options, create indexes via migration scripts
      2. **Follow ESR rule** for compound indexes -- Equality, Sort, Range
      3. **One text index per collection** -- include all searchable fields in one index
      4. **Use `explain("executionStats")`** to verify index usage -- look for `IXSCAN` not `COLLSCAN`
      5. **Remove unused indexes** -- they slow writes and consume storage
      6. **Limit to ~10 indexes per collection** -- more indexes slow write operations
      
      ---
      
      _For core patterns, see [core.md](core.md). For middleware, see [middleware.md](middleware.md). For population, see [population.md](population.md)._
      
  • reference.md 9.2 KB
    # Mongoose Reference
    
    > Decision frameworks, quick reference tables, schema types, query operators, and migration notes. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples.
    
    ---
    
    ## Decision Framework
    
    ### When to Use `save()` vs Direct Update
    
    ```
    Do you need pre('save') / post('save') middleware to fire?
    |-- YES --> findById() then .save()
    |-- NO --> Do you need schema validation on the update?
        |-- YES --> findByIdAndUpdate() with { runValidators: true }
        |-- NO --> findByIdAndUpdate() or updateOne()
    ```
    
    ### When to Use `.lean()`
    
    ```
    Are you sending the result directly as an API response?
    |-- YES --> Use .lean() (3x memory savings, plain objects)
    |-- NO --> Do you need to call .save(), .populate(), or instance methods?
        |-- YES --> Do NOT use lean (need hydrated document)
        |-- NO --> Use .lean() (faster, less memory)
    ```
    
    ### TypeScript: Automatic Inference vs Explicit Interface
    
    ```
    Does your model have instance methods, statics, or virtuals?
    |-- YES --> Use explicit interfaces with Schema generics
    |           (IDoc, IDocMethods, IDocVirtuals, IDocStatics)
    |-- NO --> Let Mongoose infer types automatically from schema
               Use InferSchemaType / InferRawDocType if you need the type elsewhere
    ```
    
    ### Embed vs Reference
    
    ```
    Is the related data always accessed with the parent?
    |-- YES --> Is it bounded (won't grow without limit)?
    |   |-- YES --> Is it small (< 100 items)?
    |   |   |-- YES --> EMBED in the parent document
    |   |   |-- NO --> Consider EMBED with pagination or REFERENCE
    |   |-- NO --> REFERENCE (unbounded arrays hit 16 MB limit)
    |-- NO --> Is the related data shared across many parents?
        |-- YES --> REFERENCE with ObjectId
        |-- NO --> Is it updated independently from the parent?
            |-- YES --> REFERENCE with ObjectId
            |-- NO --> EMBED in the parent document
    ```
    
    ### Populate vs Aggregation $lookup
    
    ```
    Do you need to resolve references?
    |-- YES --> Is it a simple parent-child with field selection?
    |   |-- YES --> Use .populate() with select
    |   |-- NO --> Do you need filtering/sorting on the joined data?
    |       |-- YES --> Use aggregation $lookup + $match
    |       |-- NO --> Use .populate() with match option
    |-- NO --> Skip population, use IDs directly
    ```
    
    ---
    
    ## Schema Types Quick Reference
    
    | Mongoose Type           | TypeScript Interface | Schema Definition                        |
    | ----------------------- | -------------------- | ---------------------------------------- |
    | `String`                | `string`             | `name: { type: String }`                 |
    | `Number`                | `number`             | `age: { type: Number }`                  |
    | `Boolean`               | `boolean`            | `isActive: { type: Boolean }`            |
    | `Date`                  | `Date`               | `createdAt: { type: Date }`              |
    | `Buffer`                | `Buffer`             | `data: { type: Buffer }`                 |
    | `Schema.Types.ObjectId` | `Types.ObjectId`     | `ref: { type: Schema.Types.ObjectId }`   |
    | `Schema.Types.Mixed`    | `any`                | `metadata: { type: Schema.Types.Mixed }` |
    | `[String]`              | `string[]`           | `tags: [{ type: String }]`               |
    | `Map`                   | `Map<string, V>`     | `meta: { type: Map, of: String }`        |
    | `Schema.Types.UUID`     | `string`             | `uuid: { type: Schema.Types.UUID }`      |
    
    **Critical distinction:** `Schema.Types.ObjectId` is for schema definitions. `Types.ObjectId` is for TypeScript interfaces and runtime values. Mixing them up causes type errors.
    
    ---
    
    ## Middleware Execution Matrix
    
    | Operation                  | Middleware Hook Triggered                  | Type      |
    | -------------------------- | ------------------------------------------ | --------- |
    | `doc.save()`               | `pre/post('validate')`, `pre/post('save')` | document  |
    | `Model.create()`           | `pre/post('validate')`, `pre/post('save')` | document  |
    | `Model.insertMany()`       | `pre/post('insertMany')`                   | model     |
    | `Model.findOneAndUpdate()` | `pre/post('findOneAndUpdate')`             | query     |
    | `Model.updateOne()`        | `pre/post('updateOne')`                    | query     |
    | `doc.updateOne()`          | `pre/post('updateOne')`                    | document  |
    | `Model.updateMany()`       | `pre/post('updateMany')`                   | query     |
    | `Model.findOneAndDelete()` | `pre/post('findOneAndDelete')`             | query     |
    | `Model.deleteOne()`        | `pre/post('deleteOne')`                    | query     |
    | `doc.deleteOne()`          | `pre/post('deleteOne')`                    | document  |
    | `Model.deleteMany()`       | `pre/post('deleteMany')`                   | query     |
    | `Model.find()`             | `pre/post('find')`                         | query     |
    | `Model.findOne()`          | `pre/post('findOne')`                      | query     |
    | `Model.aggregate()`        | `pre/post('aggregate')`                    | aggregate |
    
    **Key insights:**
    
    - `insertMany()` triggers `insertMany` model middleware only -- NOT `save` or `validate` hooks
    - `save()` and `create()` are the ONLY operations that trigger `pre('save')` and `pre('validate')`
    - `Model.deleteOne()` triggers **query** middleware; `doc.deleteOne()` triggers **document** middleware -- same method name, different middleware type
    
    ---
    
    ## Common Query Operators
    
    | Operator       | Description             | Example                                   |
    | -------------- | ----------------------- | ----------------------------------------- |
    | `$eq`          | Equal                   | `{ age: { $eq: 25 } }`                    |
    | `$ne`          | Not equal               | `{ status: { $ne: "deleted" } }`          |
    | `$gt` / `$gte` | Greater than (or equal) | `{ age: { $gte: 18 } }`                   |
    | `$lt` / `$lte` | Less than (or equal)    | `{ price: { $lt: 100 } }`                 |
    | `$in` / `$nin` | In / not in array       | `{ role: { $in: ["admin", "mod"] } }`     |
    | `$exists`      | Field exists            | `{ avatar: { $exists: true } }`           |
    | `$regex`       | Regular expression      | `{ name: { $regex: /^A/i } }`             |
    | `$or`          | Logical OR              | `{ $or: [{ a: 1 }, { b: 2 }] }`           |
    | `$and`         | Logical AND             | `{ $and: [{ a: 1 }, { b: 2 }] }`          |
    | `$elemMatch`   | Array element match     | `{ tags: { $elemMatch: { $eq: "js" } } }` |
    
    ## Common Update Operators
    
    | Operator        | Description            | Example                             |
    | --------------- | ---------------------- | ----------------------------------- |
    | `$set`          | Set field value        | `{ $set: { name: "New" } }`         |
    | `$unset`        | Remove field           | `{ $unset: { temp: "" } }`          |
    | `$inc`          | Increment              | `{ $inc: { count: 1 } }`            |
    | `$push`         | Add to array           | `{ $push: { tags: "new" } }`        |
    | `$pull`         | Remove from array      | `{ $pull: { tags: "old" } }`        |
    | `$addToSet`     | Add unique to array    | `{ $addToSet: { tags: "unique" } }` |
    | `$rename`       | Rename field           | `{ $rename: { old: "new" } }`       |
    | `$min` / `$max` | Update if less/greater | `{ $min: { low: 5 } }`              |
    
    ---
    
    ## Connection Options
    
    | Option                     | Default | Notes                                 |
    | -------------------------- | ------- | ------------------------------------- |
    | `maxPoolSize`              | 100     | Maximum sockets in connection pool    |
    | `minPoolSize`              | 0       | Minimum sockets maintained            |
    | `serverSelectionTimeoutMS` | 30000   | Time to find an available server      |
    | `socketTimeoutMS`          | 0       | Time before killing inactive sockets  |
    | `retryWrites`              | true    | Retry failed writes automatically     |
    | `retryReads`               | true    | Retry failed reads automatically      |
    | `family`                   | 0       | Force IPv4 (4) or IPv6 (6)            |
    | `bufferCommands`           | true    | Queue operations before connection    |
    | `autoIndex`                | true    | Auto-create indexes (disable in prod) |
    
    ---
    
    ## Mongoose 9 Migration Notes
    
    Key breaking changes from Mongoose 8 to 9 (released November 2025):
    
    | Change               | Before (v8)                               | After (v9)                          |
    | -------------------- | ----------------------------------------- | ----------------------------------- |
    | Pre hook callbacks   | `pre('save', function(next) { next(); })` | `pre('save', async function() { })` |
    | Type alias           | `FilterQuery<T>`                          | `QueryFilter<T>`                    |
    | Pipeline updates     | Allowed by default                        | Require `{ updatePipeline: true }`  |
    | `isValidObjectId(6)` | Returns `true`                            | Returns `false`                     |
    | Node.js minimum      | 16+                                       | 18+                                 |
    | `create()` generics  | Accepted arbitrary generics               | Type-checked against schema         |
    | UUID representation  | String via getter                         | `bson.UUID` instances               |
    
  • SKILL.md 13.7 KB
    ---
    name: api-database-mongoose
    description: MongoDB ODM with schemas, validation, middleware, and TypeScript support
    ---
    
    # Mongoose ODM Patterns
    
    > **Quick Guide:** Use Mongoose as the ODM layer for MongoDB. Let TypeScript infer types from schema definitions instead of duplicating interfaces. Register all middleware before calling `model()` -- hooks added after compilation are silently ignored. Use `.lean()` for any read-only query. Pass `{ session }` to every operation inside a transaction or enable `transactionAsyncLocalStorage`. Prefer `session.withTransaction()` over manual commit/abort. Use `127.0.0.1` instead of `localhost` in connection strings (Node.js 18+ IPv6 preference causes timeouts).
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST define all middleware (pre/post hooks) BEFORE calling `model()` -- hooks registered after model compilation are silently ignored with no error)**
    
    **(You MUST pass `{ session }` to EVERY operation inside a transaction -- missing session causes that operation to run outside the transaction silently)**
    
    **(You MUST use `.lean()` for read-only queries returning API responses -- skipping lean wastes 3x memory on hydration overhead)**
    
    **(You MUST use `127.0.0.1` instead of `localhost` in connection strings -- Node.js 18+ prefers IPv6 and `localhost` causes connection timeouts)**
    
    **(You MUST NOT use `findOneAndUpdate`/`updateOne` and expect `pre('save')` to fire -- only `save()` and `create()` trigger document middleware)**
    
    **(You MUST NOT use `next()` callbacks in pre hooks on Mongoose 9 -- use async/await instead; `next()` was removed in v9)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Mongoose, mongoose, mongoose.connect, Schema, model, ObjectId, populate, HydratedDocument, InferSchemaType, InferRawDocType, pre('save'), post('save'), lean, mongoose.startSession, withTransaction, discriminator, virtual, Schema.Types.ObjectId, Types.ObjectId
    
    **When to use:**
    
    - Defining MongoDB schemas and models with Mongoose
    - TypeScript integration with schema type inference
    - Middleware hooks (pre/post save, validate, find, delete)
    - Population (resolving references between collections)
    - Transactions with session management
    - Virtuals and instance/static methods
    - Discriminators (single collection inheritance)
    - Connection management (single and multi-database)
    
    **Key patterns covered:**
    
    - Schema definition with automatic TypeScript inference
    - TypeScript typing (HydratedDocument, InferSchemaType, methods/statics/virtuals generics)
    - CRUD operations (create, find, update, delete, lean vs hydrated)
    - Middleware hooks and their execution rules
    - Population with field selection and limits
    - Transactions (withTransaction, transactionAsyncLocalStorage)
    - Validation (built-in validators, custom validators, error messages)
    - Virtuals (computed, populate virtuals)
    - Discriminators (inheritance pattern)
    - Connection setup and multi-database
    
    **When NOT to use:**
    
    - Raw MongoDB driver queries without schema enforcement (use the native driver)
    - Relational data with complex joins and foreign key constraints (use a relational database)
    - Simple key-value storage (use a dedicated key-value store)
    
    **Detailed Resources:**
    
    - For decision frameworks, quick reference tables, and migration notes, see [reference.md](reference.md)
    
    **Core Patterns:**
    
    - [examples/core.md](examples/core.md) -- Connection, schema definition, TypeScript typing, model creation, CRUD, validation
    
    **Middleware & Lifecycle:**
    
    - [examples/middleware.md](examples/middleware.md) -- Pre/post hooks, error handling middleware, query middleware, soft delete
    
    **Relationships & Population:**
    
    - [examples/population.md](examples/population.md) -- Populate, virtual populate, discriminators, embedding vs referencing
    
    **Transactions & Advanced:**
    
    - [examples/transactions.md](examples/transactions.md) -- Sessions, withTransaction, transactionAsyncLocalStorage, connection management
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Mongoose provides schema-based modeling for MongoDB. Its value is the **application-layer enforcement** of structure, validation, middleware, and type safety on top of MongoDB's flexible document model.
    
    **Core principles:**
    
    1. **Schema-first** -- Define schemas before models. Schemas enforce structure, validation, defaults, and middleware at the application layer.
    2. **Infer, don't duplicate** -- Let Mongoose infer TypeScript types from schema definitions. Only define explicit interfaces when adding methods, statics, or virtuals.
    3. **Middleware before model** -- All pre/post hooks must be registered before `model()`. This is the single most common Mongoose bug -- hooks added after compilation are silently ignored.
    4. **Lean for reads** -- `.lean()` returns plain JavaScript objects (3x less memory). Use it for every read-only query. Only skip lean when you need Mongoose document methods.
    5. **Session discipline** -- Every operation inside a transaction must receive `{ session }`. One missed session means that operation runs outside the transaction with no error.
    6. **Validate at the schema** -- Push validation into schema definitions (required, min, max, enum, custom validators with error messages). Don't validate in application code what the schema can enforce.
    
    **When to use Mongoose:**
    
    - You want schema enforcement and validation on MongoDB documents
    - You need middleware hooks (pre/post save, validate, find)
    - You want automatic TypeScript type inference from schemas
    - You need population (reference resolution between collections)
    - You want computed properties (virtuals) and instance methods
    
    **When NOT to use Mongoose:**
    
    - Performance-critical bulk operations where the ODM overhead matters (use native driver)
    - You only need raw MongoDB queries without schema enforcement
    - You're doing heavy aggregation-only workloads (aggregation pipelines bypass most Mongoose features)
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Connection Setup
    
    Establish a single connection at application startup. Use environment variables for credentials. Never hardcode connection strings. Use `127.0.0.1` instead of `localhost` (Node.js 18+ IPv6 preference causes timeouts).
    
    ```typescript
    // Named constants for pool/timeout, env var for URI, typed return
    const connection = await mongoose.connect(process.env.MONGODB_URI!, {
      maxPoolSize: POOL_SIZE_MAX,
      serverSelectionTimeoutMS: SERVER_SELECTION_TIMEOUT_MS,
    });
    ```
    
    See [examples/core.md](examples/core.md) Pattern 1 for production connection setup, event handling, graceful shutdown, and multi-database connections.
    
    ---
    
    ### Pattern 2: Schema Definition with TypeScript Inference
    
    Let Mongoose infer types from the schema definition. Only use explicit interfaces when adding methods, statics, or virtuals. Use `as const` on enum arrays to preserve literal types.
    
    ```typescript
    const userSchema = new Schema(
      {
        email: { type: String, required: true, unique: true, lowercase: true },
        role: { type: String, enum: ["admin", "user"] as const, default: "user" },
      },
      { timestamps: true },
    );
    const User = model("User", userSchema); // TypeScript infers types from schema
    ```
    
    See [examples/core.md](examples/core.md) Patterns 2-3 for complete schemas with validation, subdocuments, `InferSchemaType`, and full generic typing with methods/statics/virtuals.
    
    ---
    
    ### Pattern 3: Explicit Typing (Methods, Statics, Virtuals)
    
    When a model has instance methods, statics, or virtuals, use the full generic parameter set. Define separate interfaces for `IDoc`, `IDocMethods`, `IDocVirtuals`, and `IDocStatics`. Export `HydratedDocument<IDoc, IDocMethods & IDocVirtuals>` for consumers.
    
    ```typescript
    type UserModel = Model<IUser, {}, IUserMethods, IUserVirtuals> & IUserStatics;
    type UserDocument = HydratedDocument<IUser, IUserMethods & IUserVirtuals>;
    const userSchema = new Schema<
      IUser,
      UserModel,
      IUserMethods,
      {},
      IUserVirtuals
    >(
      {
        /* fields */
      },
      { toJSON: { virtuals: true } },
    );
    ```
    
    See [examples/core.md](examples/core.md) Pattern 3 for the complete implementation with all interfaces, generic parameters, methods, virtuals, statics, and middleware ordering.
    
    ---
    
    ### Pattern 4: CRUD Operations
    
    Key rules: use `.lean()` for read-only queries (3x memory savings), `save()` when middleware must fire, `{ new: true, runValidators: true }` on direct updates. Never call `.save()` on a lean result (plain object, no methods).
    
    ```typescript
    const users = await User.find({ isActive: true }).select("name email").lean();
    await User.findByIdAndUpdate(
      id,
      { $set: { name: "New" } },
      { new: true, runValidators: true },
    );
    ```
    
    See [examples/core.md](examples/core.md) Pattern 5 for create, read, update, delete, bulk operations, and common mistakes.
    
    ---
    
    ### Pattern 5: Schema Validation
    
    Push validation into schema definitions: use `required` with messages, `min`/`max`/`minlength`/`maxlength` with messages, `match` for regex, `enum` with `as const` and `{VALUE}` message template, and custom `validate` functions. Use named constants for all numeric limits.
    
    ```typescript
    name: { type: String, required: [true, "Name is required"], minlength: [MIN_LEN, "Too short"] },
    status: { type: String, enum: { values: ["draft", "active"] as const, message: "{VALUE} invalid" } },
    ```
    
    See [examples/core.md](examples/core.md) Pattern 2 for complete validation schemas, subdocuments, and array validation.
    
    </patterns>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Registering middleware after `model()` call -- hooks are silently ignored, no error thrown
    - Running operations in parallel inside a transaction (`Promise.all()`) -- MongoDB does not support parallel operations within a single transaction session
    - Missing `{ session }` on any operation inside a transaction -- that operation runs outside the transaction silently
    - Using `localhost` in connection strings on Node.js 18+ -- IPv6 preference causes connection timeouts, use `127.0.0.1`
    - Mutating a document fetched with `.lean()` and calling `.save()` -- lean returns plain objects without Mongoose methods
    
    **Medium Priority Issues:**
    
    - Using `findOneAndUpdate`/`updateOne` and expecting `pre('save')` to fire -- only `save()` and `create()` trigger document middleware
    - Unbounded `.populate()` without `limit` or field selection -- can return thousands of documents per populate call, each is a separate DB round-trip
    - Not passing `runValidators: true` on `findOneAndUpdate` -- schema validation is skipped by default on direct updates
    - Using `Schema.Types.ObjectId` in TypeScript interfaces -- use `Types.ObjectId` for interfaces, `Schema.Types.ObjectId` for schema definitions only
    - Creating indexes in production application code instead of migration scripts -- index builds can lock the collection
    
    **Common Mistakes:**
    
    - Forgetting `{ new: true }` on `findOneAndUpdate` -- returns the old document by default, not the updated one
    - Using `next()` callbacks in pre hooks on Mongoose 9 -- `next()` was removed in v9, use async/await
    - Not handling duplicate key errors (error code 11000) from unique indexes
    - Using `.lean()` on write operations -- lean is for reads only
    - Checking `doc.isNew` in `post('save')` hooks -- always `false` after save; capture in `pre('save')` via `this.$locals.wasNew`
    - Defining the same middleware hook multiple times without realizing they stack (all run, not just the last one)
    - Using `extends Document` on interfaces -- deprecated pattern that breaks type inference for lean documents and query filters
    
    **Gotchas & Edge Cases:**
    
    - MongoDB has a 16 MB document size limit -- deeply embedded arrays can silently hit this
    - Mongoose buffers all operations until connected -- queries queue silently if connection fails, which can mask connection issues in development
    - `deleteOne`/`deleteMany` on the Model do not trigger document `pre('deleteOne')` middleware -- they trigger query middleware instead; use `doc.deleteOne()` for document middleware
    - Virtual properties are excluded from `toJSON()`/`toObject()` by default -- set `{ toJSON: { virtuals: true } }` in schema options or they disappear in API responses
    - `insertMany()` does not trigger `save` middleware -- it triggers `insertMany` model middleware only
    - Mongoose 9 renamed `FilterQuery` to `QueryFilter` -- update TypeScript imports if upgrading
    - Mongoose 9 disallows pipeline-style updates by default -- pass `{ updatePipeline: true }` or they throw
    - `create()` with an array requires array syntax for `{ session }`: `Model.create([data], { session })` -- the non-array form `Model.create(data, { session })` does not work in transactions
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST define all middleware (pre/post hooks) BEFORE calling `model()` -- hooks registered after model compilation are silently ignored with no error)**
    
    **(You MUST pass `{ session }` to EVERY operation inside a transaction -- missing session causes that operation to run outside the transaction silently)**
    
    **(You MUST use `.lean()` for read-only queries returning API responses -- skipping lean wastes 3x memory on hydration overhead)**
    
    **(You MUST use `127.0.0.1` instead of `localhost` in connection strings -- Node.js 18+ prefers IPv6 and `localhost` causes connection timeouts)**
    
    **(You MUST NOT use `findOneAndUpdate`/`updateOne` and expect `pre('save')` to fire -- only `save()` and `create()` trigger document middleware)**
    
    **(You MUST NOT use `next()` callbacks in pre hooks on Mongoose 9 -- use async/await instead; `next()` was removed in v9)**
    
    **Failure to follow these rules will cause silent middleware bypass, transaction isolation failures, or connection timeouts.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related