Mongodb Mongoose
Optimized for current MongoDB server releases, Mongoose 8.x+, Node.js 22+, and TypeScript 5.5+.
Comprehensive guidance for MongoDB database design, Mongoose ODM patterns, and Atlas integration for Node.js/Next.js applications.
- Leverage native parallel subagent dispatch and 200k+ context windows where available.
When to Use This Skill
Use symptom -> action triggers: when one matches, apply this skill and verify with the protocol below.
- Designing MongoDB schemas and data models
- Building Mongoose models with validation and middleware
- Implementing the repository pattern for data access
- Writing aggregation pipelines for complex queries
- Managing MongoDB Atlas connections and configuration
- Integrating MongoDB with Next.js API routes
- Database migration strategies
<!-- MCP:START --> <!-- PORTABILITY:START -->
Cross-Client Portability
This skill is written to stay usable across GitHub Copilot, Claude Code, and Codex.
- GitHub Copilot: keep the folder in a Copilot-visible skill path or wrap the workflow in project instructions when folder discovery is unavailable.
- Claude Code: keep the folder in a local skills directory or a compatible plugin source.
- Codex: install or sync the folder into
$CODEX_HOME/skills/mongodb-mongooseand restart Codex after major changes.
MCP Availability And Fallback
Preferred MCP Server: MongoDB MCP
- Fallback prompt: "Use the Mongodb Mongoose skill without MCP. Follow the documented local or manual fallback, show the selected tool surface, and report the verification evidence."
- Use
mongosh, MongoDB Atlas UI, local schema files, and Mongoose model inspection when the MCP server is unavailable. - Validate indexes, queries, and aggregation pipelines against a local or staging database before finalizing changes.
- Do not claim an MCP operation was used when the active host does not expose it.
Anti-Patterns
- Modeling documents like normalized tables by default: MongoDB performance depends on query-driven shape, not relational purity.
- Returning full hydrated documents for every request: Over-fetching and hydration overhead accumulate quickly in API paths.
- Adding middleware without write-path tests: Hooks can silently change create, update, and migration behavior.
Verification Protocol
Before claiming "skill applied successfully":
- Pass/fail: The Mongodb Mongoose implementation names the target runtime, framework version, and affected files.
- Pass/fail: Build, lint, test, or equivalent local validation is run for the changed surface.
- Pass/fail: Edge cases for errors, dependency drift, and environment differences are addressed or explicitly out of scope.
- Pressure-test scenario: Apply the workflow to a change that passes happy-path tests but fails one boundary condition.
- Success metric: Zero untested success claims; every implementation claim maps to a command or artifact.
Before and After Example
// Before
const recipes = await Recipe.find({ author: userId }).populate('author');
// After
const recipes = await Recipe.find({ author: userId, isPublished: true })
.select({ title: 1, slug: 1, createdAt: 1 })
.sort({ createdAt: -1 })
.lean();
Narrows the query shape, avoids unnecessary hydration, and aligns the result with the view model actually needed.
Schema Design
Data Modeling Principles
- Embed when data is accessed together and has a 1:few relationship
- Reference when data is accessed independently or has a 1:many/many:many relationship
- Design schemas around query patterns, not normalized relational models
- Use denormalization strategically for read performance
Mongoose Model Pattern
import mongoose from 'mongoose';
const recipeSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Title is required'],
trim: true,
maxlength: [200, 'Title cannot exceed 200 characters'],
index: true,
},
slug: {
type: String,
unique: true,
lowercase: true,
},
ingredients: [{
name: { type: String, required: true },
amount: { type: Number, required: true },
unit: { type: String, enum: ['g', 'kg', 'ml', 'l', 'cup', 'tbsp', 'tsp', 'piece'] },
}],
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true,
},
tags: [{ type: String, lowercase: true, trim: true }],
isPublished: { type: Boolean, default: false },
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
});
// Indexes for common queries
recipeSchema.index({ title: 'text', tags: 'text' });
recipeSchema.index({ author: 1, createdAt: -1 });
// Virtual fields
recipeSchema.virtual('ingredientCount').get(function() {
return this.ingredients.length;
});
// Pre-save middleware
recipeSchema.pre('save', function(next) {
if (this.isModified('title')) {
this.slug = this.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
}
next();
});
export const Recipe = mongoose.models.Recipe || mongoose.model('Recipe', recipeSchema);
Schema Best Practices
- Always define
required,type, and validation rules - Use
timestamps: truefor automaticcreatedAt/updatedAt - Add indexes for frequently queried fields
- Use
enumfor fields with fixed values - Define virtuals for computed properties
- Use middleware (pre/post hooks) for side effects
Repository Pattern
class RecipeRepository {
async findAll(filter = {}, options = {}) {
const { page = 1, limit = 20, sort = '-createdAt', populate = '' } = options;
const skip = (page - 1) * limit;
const [recipes, total] = await Promise.all([
Recipe.find(filter)
.sort(sort)
.skip(skip)
.limit(limit)
.populate(populate)
.lean(),
Recipe.countDocuments(filter),
]);
return {
data: recipes,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
};
}
async findById(id) {
return Recipe.findById(id).populate('author', 'name avatar').lean();
}
async create(data) {
const recipe = new Recipe(data);
return recipe.save();
}
async update(id, data) {
return Recipe.findByIdAndUpdate(id, data, {
new: true,
runValidators: true,
});
}
async delete(id) {
return Recipe.findByIdAndDelete(id);
}
async search(query, options = {}) {
return this.findAll(
{ $text: { $search: query } },
{ ...options, sort: { score: { $meta: 'textScore' } } }
);
}
}
export const recipeRepository = new RecipeRepository();
Aggregation Pipelines
Common Patterns
// Group recipes by tag with counts
const tagStats = await Recipe.aggregate([
{ $match: { isPublished: true } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 20 },
]);
// Author statistics with lookup
const authorStats = await Recipe.aggregate([
{ $group: {
_id: '$author',
recipeCount: { $sum: 1 },
avgRating: { $avg: '$rating' },
}},
{ $lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'authorInfo',
}},
{ $unwind: '$authorInfo' },
{ $project: {
name: '$authorInfo.name',
recipeCount: 1,
avgRating: { $round: ['$avgRating', 1] },
}},
{ $sort: { recipeCount: -1 } },
]);
// Date-based analytics
const monthlyRecipes = await Recipe.aggregate([
{ $match: { createdAt: { $gte: new Date('2024-01-01') } } },
{ $group: {
_id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } },
count: { $sum: 1 },
}},
{ $sort: { _id: 1 } },
]);
Atlas Connection
Connection Setup (Next.js)
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error('MONGODB_URI environment variable is not defined');
}
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
export async function connectDB() {
if (cached.conn) return cached.conn;
if (!cached.promise) {
cached.promise = mongoose.connect(MONGODB_URI, {
bufferCommands: false,
});
}
cached.conn = await cached.promise;
return cached.conn;
}
Connection Best Practices
- Cache connection in development to prevent multiple connections
- Use
bufferCommands: falsefor explicit error handling - Set connection pool size via
maxPoolSizefor production - Use Atlas connection string with
retryWrites=true&w=majority
Migration Strategies
Document Versioning
const userSchema = new mongoose.Schema({
schemaVersion: { type: Number, default: 2 },
// ... fields
});
userSchema.pre('save', function(next) {
if (this.schemaVersion < 2) {
// Migrate old fields to new format
this.schemaVersion = 2;
}
next();
});
Batch Migration Script
async function migrateUsers() {
const batchSize = 100;
let processed = 0;
let batch;
do {
batch = await User.find({ schemaVersion: { $lt: 2 } }).limit(batchSize);
for (const user of batch) {
user.schemaVersion = 2;
await user.save();
processed++;
}
console.log(`Migrated ${processed} users`);
} while (batch.length === batchSize);
}
Performance Tips
- Use
.lean()for read-only queries (returns plain objects, 5-10x faster) - Use
.select()to return only needed fields - Create compound indexes matching your query patterns
- Use
$projectearly in aggregation to reduce working set - Avoid
$lookupin high-frequency queries; denormalize instead - Use
explain()to analyze query performance
Troubleshooting
| Issue | Solution |
|---|---|
| Slow queries | Add indexes, use .lean(), check with explain() |
| Connection timeouts | Check Atlas network access, increase pool size |
| Validation errors | Review schema constraints, check middleware order |
| Duplicate key errors | Ensure unique indexes, handle with try/catch |
| Memory issues | Use cursors for large datasets, limit batch sizes |
Common Pitfalls
- Modeling data like a normalized relational schema by default: MongoDB performance depends on query-driven document shape, not tables-first design.
- Returning full hydrated documents everywhere: Hydration and over-fetching add cost when a lean projection would do.
- Adding middleware without explicit write-path tests: Hooks can silently change behavior in create, update, and migration flows.
References & Resources
Documentation
- Aggregation Reference — Pipeline stages, accumulator operators, and common aggregation recipes
- Indexing Strategies — Index types, ESR rule, compound indexes, and performance analysis
Scripts
- Seed Database — Zero-dependency MongoDB seeding script with sample recipe data
Examples
- Recipe API Example — Complete Mongoose + Next.js Recipe CRUD API with models, routes, and validation
Related Skills
- javascript-development: Use it when the workflow also needs modern JavaScript and TypeScript application code.
- nextjs-development: Use it when the workflow also needs Next.js App Router and server-first React patterns.
- sql-development: Use it when the workflow also needs SQL query, schema, and performance tuning work.
- code-quality: Use it when the workflow also needs two-stage review (spec compliance first, then code quality), maintainability, and refactoring guidance.