MongoDB Cursor Rules: NoSQL Document Database

Cursor rules for MongoDB covering connection management, CRUD operations, aggregation pipeline, indexes, schema design, Mongoose ODM, transactions, and Atlas deployment.

June 11, 2026by PromptGenius Team
mongodbcursor-rulesdatabasenosqlmongoosebackend
MongoDB Cursor Rules: NoSQL Document Database

Overview

MongoDB is the most popular NoSQL document database, storing data in flexible JSON-like documents with a powerful query engine, horizontal scaling, and native high availability. These cursor rules enforce connection pooling, schema design patterns, index strategies, aggregation pipeline usage, Mongoose ODM conventions, and transaction patterns so AI assistants generate efficient, production-ready MongoDB code.

Note:

Enforces MongoClient connection pooling, embedded vs reference schema decisions, compound index creation, aggregation pipeline over map-reduce, Mongoose schema/validation, transaction patterns, and Atlas best practices.

Rules Configuration

---
description: Enforces MongoDB best practices including connection pooling, schema design (embedded vs references), indexes, aggregation pipelines, Mongoose ODM patterns, transactions, and Atlas deployment conventions.
globs: **/*.js,**/*.ts,models/**/*.js,models/**/*.ts
---
# MongoDB Best Practices

You are an expert in MongoDB, document database design, and Node.js/TypeScript backend development.
You understand schema design tradeoffs, indexing strategies, aggregation pipelines, and production deployment.

### Connection Management
- Use MongoClient with connection pooling (default pool size: 100)
- Reuse the client across requests — never create a new client per query
- Set `maxPoolSize`, `minPoolSize`, and `maxIdleTimeMS` for pool tuning
- Use connection string with retryable reads/writes
- Handle connection errors: `client.on("error", console.error)`
- Close the client on graceful shutdown: `await client.close()`
- Use `mongodb+srv://` protocol for Atlas cluster connections

### Schema Design
- Embed related data that is read together (users → addresses)
- Reference related data that grows independently (users → orders)
- Use the "one-to-few" / "one-to-many" / "one-to-squillions" heuristic
- Denormalize for read performance when write frequency is low
- Avoid deeply nested arrays that exceed 16MB document limit
- Use ObjectId for references to other collections
- Design schemas for query patterns, not for normalization

### CRUD Operations
- Use `findOne()` for single document lookups, `find()` with `.toArray()` for multiple
- Use projections: `.project({ name: 1, email: 1, _id: 0 })` to limit returned fields
- Use `insertOne()` for single inserts, `insertMany()` for bulk (unordered by default)
- Use `updateOne()` with atomic operators: `$set`, `$inc`, `$push`, `$pull`, `$addToSet`
- Use `replaceOne()` for full document replacement
- Use `deleteOne()` for single deletions, `deleteMany({})` to clear a collection
- Always use `$set` for partial updates — never replace the entire document accidentally

### Indexes
- Create indexes for query fields: `.createIndex({ email: 1 }, { unique: true })`
- Use compound indexes for multi-field queries: `{ status: 1, createdAt: -1 }`
- Follow ESR rule: Equality first, then Sort, then Range in compound indexes
- Use `explain()` to verify index usage: `collection.find(query).explain("executionStats")`
- Create sparse indexes for optional fields: `{ deletedAt: 1 }, { sparse: true }`
- Use TTL indexes for auto-expiring data: `{ createdAt: 1 }, { expireAfterSeconds: 3600 }`
- Create text indexes for full-text search: `{ title: "text", body: "text" }`
- Create geospatial indexes: `2dsphere` for GeoJSON, `2d` for legacy coordinates
- Monitor with `currentOp()` and `explain()` — never create indexes blindly

### Aggregation Pipeline
- Use aggregation pipeline for complex queries, reporting, and transformations
- Chain stages: `$match``$group``$sort``$project``$limit`
- Always place `$match` and `$limit` earliest in the pipeline to reduce document flow
- Use `$lookup` for joins (prefer embedded data when possible)
- Use `$unwind` to flatten arrays, `$addFields` to add computed fields
- Use `$facet` for multiple parallel aggregations in one query
- Set `allowDiskUse: true` for large aggregations (>100MB memory)
- Use `$merge` or `$out` to persist aggregation results to a collection

### Mongoose ODM
- Define schemas with explicit types: `const UserSchema = new Schema({ name: String, email: { type: String, required: true, unique: true } })`
- Use schema validation: `required`, `min`, `max`, `enum`, `validate` (custom validators)
- Add timestamps: `{ timestamps: true }` for automatic createdAt/updatedAt
- Use virtuals for computed properties: `schema.virtual("fullName").get(function() { return this.firstName + " " + this.lastName })`
- Define middleware: `schema.pre("save", async function(next) { ... })`
- Use `schema.index()` to define indexes in the schema definition
- Use `Model.create()`, `Model.findById()`, `Model.findOneAndUpdate()`
- Use `.populate("field")` to resolve references (prefer .select() and field limiting)
- Use `.lean()` for read-only queries to return plain JS objects (faster)
- Use `toJSON: { virtuals: true }` to include virtuals in JSON output

### Transactions
- Use `session.withTransaction(async () => { ... })` for atomic operations
- Transactions require replica sets (Atlas provides this automatically)
- Keep transactions short — they hold locks and affect performance
- Only use transactions when atomicity across collections is required
- Pass the session to all operations: `collection.updateOne(filter, update, { session })`
- Handle `TransientTransactionError` with automatic retry

### Security & Production
- Enable authentication: always set username/password in connection string
- Use network access controls: IP whitelist in Atlas
- Validate and sanitize user input — prevent NoSQL injection
- Never pass raw user input to `$where` or unchecked `$regex`
- Use `mongosh` or Compass for ad-hoc queries, never production direct access
- Set up alerts for disk usage, connection count, and query performance in Atlas

Installation

Create mongodb.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursorrules/ — Copilot users place it in .github/copilot-instructions.md instead.

# Install MongoDB driver
npm install mongodb

# Or install Mongoose for ODM
npm install mongoose

# Install MongoDB locally via Docker
docker run -d --name mongo -p 27017:27017 mongo:8

Examples

// db.ts — Connection management with connection pooling
import { MongoClient, type Db } from "mongodb";

const uri = process.env.MONGODB_URI || "mongodb://localhost:27017";
const client = new MongoClient(uri, {
  maxPoolSize: 10,
  minPoolSize: 2,
  maxIdleTimeMS: 30000,
});

let db: Db;

export async function connectDB(): Promise<Db> {
  if (db) return db;
  await client.connect();
  db = client.db("myapp");
  return db;
}

process.on("SIGTERM", async () => {
  await client.close();
});
// user.model.ts — Mongoose schema with indexes and middleware
import { Schema, model, type Document } from "mongoose";

interface IUser extends Document {
  email: string;
  name: string;
  status: "active" | "inactive";
}

const UserSchema = new Schema<IUser>(
  {
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      trim: true,
    },
    name: { type: String, required: true },
    status: {
      type: String,
      enum: ["active", "inactive"],
      default: "active",
    },
  },
  { timestamps: true },
);

UserSchema.index({ status: 1, createdAt: -1 });

UserSchema.pre("save", async function (next) {
  console.log(`Saving user ${this.email}`);
  next();
});

export const User = model<IUser>("User", UserSchema);
// order.service.ts — Aggregation pipeline with lookup
import { getDb } from "./db";

export async function getOrderStats(userId: string) {
  const db = await getDb();
  const orders = db.collection("orders");

  return orders
    .aggregate([
      { $match: { userId } },
      {
        $group: {
          _id: "$status",
          total: { $sum: "$total" },
          count: { $sum: 1 },
          avg: { $avg: "$total" },
        },
      },
      { $sort: { total: -1 } },
    ])
    .toArray();
}
// transfer.ts — Transaction across collections
import { getDb } from "./db";

export async function transferFunds(from: string, to: string, amount: number) {
  const db = await getDb();
  const accounts = db.collection("accounts");
  const session = db.client.startSession();

  try {
    await session.withTransaction(async () => {
      await accounts.updateOne(
        { _id: from, balance: { $gte: amount } },
        { $inc: { balance: -amount } },
        { session },
      );

      await accounts.updateOne(
        { _id: to },
        { $inc: { balance: amount } },
        { session },
      );
    });
  } finally {
    await session.endSession();
  }
}