Agent skill
api-database-mongoose
MongoDB ODM with schemas, validation, middleware, and TypeScript support
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/api-database-mongoose
SKILL.md
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 enabletransactionAsyncLocalStorage. Prefersession.withTransaction()over manual commit/abort. Use127.0.0.1instead oflocalhostin 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:
- examples/middleware.md -- Pre/post hooks, error handling middleware, query middleware, soft delete
Relationships & Population:
- examples/population.md -- Populate, virtual populate, discriminators, embedding vs referencing
Transactions & Advanced:
- examples/transactions.md -- Sessions, withTransaction, transactionAsyncLocalStorage, connection management
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:
- Schema-first -- Define schemas before models. Schemas enforce structure, validation, defaults, and middleware at the application layer.
- Infer, don't duplicate -- Let Mongoose infer TypeScript types from schema definitions. Only define explicit interfaces when adding methods, statics, or virtuals.
- 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. - 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. - Session discipline -- Every operation inside a transaction must receive
{ session }. One missed session means that operation runs outside the transaction with no error. - 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)
Core Patterns
Pattern 1: Connection Setup
Establish a single connection at application startup. Use environment variables for credentials. Never hardcode connection strings.
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");
}
return mongoose.connect(uri, {
maxPoolSize: POOL_SIZE_MAX,
minPoolSize: POOL_SIZE_MIN,
serverSelectionTimeoutMS: SERVER_SELECTION_TIMEOUT_MS,
socketTimeoutMS: SOCKET_TIMEOUT_MS,
retryWrites: true,
retryReads: true,
});
}
export { connectDatabase };
Why good: Named constants for all numeric values, environment variable for URI, typed return, error on missing URI
// BAD: Hardcoded URI, localhost, no options
mongoose.connect("mongodb://localhost:27017/mydb");
Why bad: Hardcoded connection string leaks credentials, localhost fails on Node.js 18+ (IPv6 preference), no pool or timeout configuration
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.
import { Schema, model } from "mongoose";
const userSchema = new Schema(
{
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
role: {
type: String,
enum: ["admin", "user", "moderator"] as const,
default: "user",
},
age: { type: Number, min: 0, max: 150 },
isActive: { type: Boolean, default: true },
tags: [{ type: String }],
},
{ timestamps: true },
);
// TypeScript automatically infers the document type from schema
const User = model("User", userSchema);
export { User, userSchema };
Why good: TypeScript infers types automatically, as const preserves enum literal types, timestamps via schema option, named exports
// BAD: Duplicating interface and schema definition
interface IUser {
name: string;
email: string;
}
const userSchema = new Schema({ name: String, email: String });
// Types drift apart -- schema allows undefined, interface says required
Why bad: Manual interface duplicates schema, they drift out of sync, no validation constraints, no error messages
Pattern 3: Explicit Typing (Methods, Statics, Virtuals)
When a model has instance methods, statics, or virtuals, use the full generic parameter set.
import {
Schema,
model,
type HydratedDocument,
type Model,
type Types,
} from "mongoose";
interface IUser {
email: string;
passwordHash: string;
firstName: string;
lastName: string;
role: "admin" | "user" | "moderator";
}
interface IUserMethods {
comparePassword(candidate: string): Promise<boolean>;
}
interface IUserVirtuals {
fullName: string;
}
type UserModel = Model<IUser, {}, IUserMethods, IUserVirtuals>;
type UserDocument = HydratedDocument<IUser, IUserMethods & IUserVirtuals>;
const userSchema = new Schema<
IUser,
UserModel,
IUserMethods,
{},
IUserVirtuals
>(
{
/* fields */
},
{ toJSON: { virtuals: true } },
);
userSchema.methods.comparePassword = async function (candidate: string) {
// implementation
return false;
};
userSchema.virtual("fullName").get(function () {
return `${this.firstName} ${this.lastName}`;
});
// Middleware BEFORE model()
userSchema.pre("save", async function () {
if (this.isModified("passwordHash")) {
// hash password
}
});
// model() AFTER all middleware and methods
const User = model<IUser, UserModel>("User", userSchema);
export { User, userSchema };
export type { IUser, IUserMethods, UserDocument };
Why good: Separate interfaces for document, methods, virtuals; correct generic parameter order; HydratedDocument exported for consumers; middleware before model()
See examples/core.md for the complete implementation with all generic parameters.
Pattern 4: CRUD Operations
// Create -- triggers save middleware
const user = await User.create({ name: "Alice", email: "[email protected]" });
// Read with lean (read-only response)
const PAGE_SIZE = 20;
const users = await User.find({ isActive: true })
.select("name email role")
.sort({ createdAt: -1 })
.limit(PAGE_SIZE)
.lean();
// Update with save() -- triggers pre('save') middleware
const doc = await User.findById(id);
if (!doc) throw new Error(`User not found: ${id}`);
doc.name = "Updated";
await doc.save();
// Direct update -- does NOT trigger save middleware
await User.findByIdAndUpdate(
id,
{ $set: { name: "Updated" } },
{
new: true, // return updated document
runValidators: true, // enforce schema validation
},
);
Why good: .lean() for read-only, save() when middleware matters, { new: true, runValidators: true } on direct updates
// BAD: lean() then trying to save
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 objects without Mongoose methods -- .save() does not exist
Pattern 5: Schema Validation
const MIN_NAME_LENGTH = 2;
const MAX_NAME_LENGTH = 100;
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`,
],
},
price: {
type: Number,
required: true,
min: [0, "Price cannot be negative"],
validate: {
validator: (v: number) => Number.isFinite(v),
message: "Price must be a finite number",
},
},
sku: {
type: String,
required: true,
unique: true,
match: [SKU_PATTERN, "SKU must match format XX-000000"],
},
status: {
type: String,
enum: {
values: ["draft", "active", "archived"] as const,
message: "{VALUE} is not a valid status",
},
default: "draft",
},
});
export { productSchema };
Why good: Named constants for validation limits, custom error messages on every validator, regex validation with descriptive message, enum with as const for TypeScript inference
See examples/core.md for subdocument schemas and array validation.
<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
localhostin connection strings on Node.js 18+ -- IPv6 preference causes connection timeouts, use127.0.0.1 - Mutating a document fetched with
.lean()and calling.save()-- lean returns plain objects without Mongoose methods
Medium Priority Issues:
- Using
findOneAndUpdate/updateOneand expectingpre('save')to fire -- onlysave()andcreate()trigger document middleware - Unbounded
.populate()withoutlimitor field selection -- can return thousands of documents per populate call, each is a separate DB round-trip - Not passing
runValidators: trueonfindOneAndUpdate-- schema validation is skipped by default on direct updates - Using
Schema.Types.ObjectIdin TypeScript interfaces -- useTypes.ObjectIdfor interfaces,Schema.Types.ObjectIdfor schema definitions only - Creating indexes in production application code instead of migration scripts -- index builds can lock the collection
Common Mistakes:
- Forgetting
{ new: true }onfindOneAndUpdate-- 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.isNewinpost('save')hooks -- alwaysfalseafter save; capture inpre('save')viathis.$locals.wasNew - Defining the same middleware hook multiple times without realizing they stack (all run, not just the last one)
- Using
extends Documenton 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/deleteManyon the Model do not trigger documentpre('deleteOne')middleware -- they trigger query middleware instead; usedoc.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 triggersavemiddleware -- it triggersinsertManymodel middleware only- Mongoose 9 renamed
FilterQuerytoQueryFilter-- 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 formModel.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>
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?