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
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: true for automatic createdAt/updatedAt
- Add indexes for frequently queried fields
- Use
enum for 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: false for explicit error handling
- Set connection pool size via
maxPoolSize for 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
$project early in aggregation to reduce working set
- Avoid
$lookup in 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
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
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-mongoose and restart Codex after major changes.
MCP Availability And Fallback
Preferred MCP Server: MongoDB MCP
- Fallback prompt: "Use the Mongodb Mongoose skill without MCP. Rely on the local
SKILL.md, bundled references or scripts, and manual verification. Show the exact commands, evidence, and final checks you used before concluding."
- 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.
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.
1---2name: mongodb-mongoose3description: MongoDB with Mongoose — schemas, models, aggregation pipelines, migrations, and Atlas connections. Use when designing collections, writing queries, or integrating MongoDB into Node.js/Next.js apps.4---5# Mongodb Mongoose67> Optimized for current MongoDB server releases, Mongoose 8.x+, Node.js 22+, and TypeScript 5.5+.89Comprehensive guidance for MongoDB database design, Mongoose ODM patterns, and Atlas integration for Node.js/Next.js applications.1011- Leverage native parallel subagent dispatch and 200k+ context windows where available.121314## When to Use This Skill1516Use symptom -> action triggers: when one matches, apply this skill and verify with the protocol below.1718- Designing MongoDB schemas and data models19- Building Mongoose models with validation and middleware20- Implementing the repository pattern for data access21- Writing aggregation pipelines for complex queries22- Managing MongoDB Atlas connections and configuration23- Integrating MongoDB with Next.js API routes24- Database migration strategies252627---2829## Anti-Patterns3031- Modeling documents like normalized tables by default: MongoDB performance depends on query-driven shape, not relational purity.32- Returning full hydrated documents for every request: Over-fetching and hydration overhead accumulate quickly in API paths.33- Adding middleware without write-path tests: Hooks can silently change create, update, and migration behavior.3435## Verification Protocol3637Before claiming "skill applied successfully":38391. Pass/fail: The Mongodb Mongoose implementation names the target runtime, framework version, and affected files.402. Pass/fail: Build, lint, test, or equivalent local validation is run for the changed surface.413. Pass/fail: Edge cases for errors, dependency drift, and environment differences are addressed or explicitly out of scope.424. Pressure-test scenario: Apply the workflow to a change that passes happy-path tests but fails one boundary condition.435. Success metric: Zero untested success claims; every implementation claim maps to a command or artifact.4445## Before and After Example4647```javascript48// Before49const recipes = await Recipe.find({ author: userId }).populate('author');5051// After52const recipes = await Recipe.find({ author: userId, isPublished: true })53 .select({ title: 1, slug: 1, createdAt: 1 })54 .sort({ createdAt: -1 })55 .lean();56```5758Narrows the query shape, avoids unnecessary hydration, and aligns the result with the view model actually needed.5960## Schema Design6162### Data Modeling Principles63- **Embed** when data is accessed together and has a 1:few relationship64- **Reference** when data is accessed independently or has a 1:many/many:many relationship65- Design schemas around query patterns, not normalized relational models66- Use denormalization strategically for read performance6768### Mongoose Model Pattern69```javascript70import mongoose from 'mongoose';7172const recipeSchema = new mongoose.Schema({73 title: {74 type: String,75 required: [true, 'Title is required'],76 trim: true,77 maxlength: [200, 'Title cannot exceed 200 characters'],78 index: true,79 },80 slug: {81 type: String,82 unique: true,83 lowercase: true,84 },85 ingredients: [{86 name: { type: String, required: true },87 amount: { type: Number, required: true },88 unit: { type: String, enum: ['g', 'kg', 'ml', 'l', 'cup', 'tbsp', 'tsp', 'piece'] },89 }],90 author: {91 type: mongoose.Schema.Types.ObjectId,92 ref: 'User',93 required: true,94 index: true,95 },96 tags: [{ type: String, lowercase: true, trim: true }],97 isPublished: { type: Boolean, default: false },98}, {99 timestamps: true,100 toJSON: { virtuals: true },101 toObject: { virtuals: true },102});103104// Indexes for common queries105recipeSchema.index({ title: 'text', tags: 'text' });106recipeSchema.index({ author: 1, createdAt: -1 });107108// Virtual fields109recipeSchema.virtual('ingredientCount').get(function() {110 return this.ingredients.length;111});112113// Pre-save middleware114recipeSchema.pre('save', function(next) {115 if (this.isModified('title')) {116 this.slug = this.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');117 }118 next();119});120121export const Recipe = mongoose.models.Recipe || mongoose.model('Recipe', recipeSchema);122```123124### Schema Best Practices125- Always define `required`, `type`, and validation rules126- Use `timestamps: true` for automatic `createdAt`/`updatedAt`127- Add indexes for frequently queried fields128- Use `enum` for fields with fixed values129- Define virtuals for computed properties130- Use middleware (pre/post hooks) for side effects131132---133134## Repository Pattern135136```javascript137class RecipeRepository {138 async findAll(filter = {}, options = {}) {139 const { page = 1, limit = 20, sort = '-createdAt', populate = '' } = options;140 const skip = (page - 1) * limit;141142 const [recipes, total] = await Promise.all([143 Recipe.find(filter)144 .sort(sort)145 .skip(skip)146 .limit(limit)147 .populate(populate)148 .lean(),149 Recipe.countDocuments(filter),150 ]);151152 return {153 data: recipes,154 pagination: {155 page,156 limit,157 total,158 pages: Math.ceil(total / limit),159 },160 };161 }162163 async findById(id) {164 return Recipe.findById(id).populate('author', 'name avatar').lean();165 }166167 async create(data) {168 const recipe = new Recipe(data);169 return recipe.save();170 }171172 async update(id, data) {173 return Recipe.findByIdAndUpdate(id, data, {174 new: true,175 runValidators: true,176 });177 }178179 async delete(id) {180 return Recipe.findByIdAndDelete(id);181 }182183 async search(query, options = {}) {184 return this.findAll(185 { $text: { $search: query } },186 { ...options, sort: { score: { $meta: 'textScore' } } }187 );188 }189}190191export const recipeRepository = new RecipeRepository();192```193194---195196## Aggregation Pipelines197198### Common Patterns199200```javascript201// Group recipes by tag with counts202const tagStats = await Recipe.aggregate([203 { $match: { isPublished: true } },204 { $unwind: '$tags' },205 { $group: { _id: '$tags', count: { $sum: 1 } } },206 { $sort: { count: -1 } },207 { $limit: 20 },208]);209210// Author statistics with lookup211const authorStats = await Recipe.aggregate([212 { $group: {213 _id: '$author',214 recipeCount: { $sum: 1 },215 avgRating: { $avg: '$rating' },216 }},217 { $lookup: {218 from: 'users',219 localField: '_id',220 foreignField: '_id',221 as: 'authorInfo',222 }},223 { $unwind: '$authorInfo' },224 { $project: {225 name: '$authorInfo.name',226 recipeCount: 1,227 avgRating: { $round: ['$avgRating', 1] },228 }},229 { $sort: { recipeCount: -1 } },230]);231232// Date-based analytics233const monthlyRecipes = await Recipe.aggregate([234 { $match: { createdAt: { $gte: new Date('2024-01-01') } } },235 { $group: {236 _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } },237 count: { $sum: 1 },238 }},239 { $sort: { _id: 1 } },240]);241```242243---244245## Atlas Connection246247### Connection Setup (Next.js)248```javascript249import mongoose from 'mongoose';250251const MONGODB_URI = process.env.MONGODB_URI;252253if (!MONGODB_URI) {254 throw new Error('MONGODB_URI environment variable is not defined');255}256257let cached = global.mongoose;258if (!cached) {259 cached = global.mongoose = { conn: null, promise: null };260}261262export async function connectDB() {263 if (cached.conn) return cached.conn;264265 if (!cached.promise) {266 cached.promise = mongoose.connect(MONGODB_URI, {267 bufferCommands: false,268 });269 }270271 cached.conn = await cached.promise;272 return cached.conn;273}274```275276### Connection Best Practices277- Cache connection in development to prevent multiple connections278- Use `bufferCommands: false` for explicit error handling279- Set connection pool size via `maxPoolSize` for production280- Use Atlas connection string with `retryWrites=true&w=majority`281282---283284## Migration Strategies285286### Document Versioning287```javascript288const userSchema = new mongoose.Schema({289 schemaVersion: { type: Number, default: 2 },290 // ... fields291});292293userSchema.pre('save', function(next) {294 if (this.schemaVersion < 2) {295 // Migrate old fields to new format296 this.schemaVersion = 2;297 }298 next();299});300```301302### Batch Migration Script303```javascript304async function migrateUsers() {305 const batchSize = 100;306 let processed = 0;307 let batch;308309 do {310 batch = await User.find({ schemaVersion: { $lt: 2 } }).limit(batchSize);311 for (const user of batch) {312 user.schemaVersion = 2;313 await user.save();314 processed++;315 }316 console.log(`Migrated ${processed} users`);317 } while (batch.length === batchSize);318}319```320321---322323## Performance Tips324325- Use `.lean()` for read-only queries (returns plain objects, 5-10x faster)326- Use `.select()` to return only needed fields327- Create compound indexes matching your query patterns328- Use `$project` early in aggregation to reduce working set329- Avoid `$lookup` in high-frequency queries; denormalize instead330- Use `explain()` to analyze query performance331332## Troubleshooting333334| Issue | Solution |335|-------|----------|336| Slow queries | Add indexes, use `.lean()`, check with `explain()` |337| Connection timeouts | Check Atlas network access, increase pool size |338| Validation errors | Review schema constraints, check middleware order |339| Duplicate key errors | Ensure unique indexes, handle with try/catch |340| Memory issues | Use cursors for large datasets, limit batch sizes |341342---343344## Common Pitfalls345346- Modeling data like a normalized relational schema by default: MongoDB performance depends on query-driven document shape, not tables-first design.347- Returning full hydrated documents everywhere: Hydration and over-fetching add cost when a lean projection would do.348- Adding middleware without explicit write-path tests: Hooks can silently change behavior in create, update, and migration flows.349350## References & Resources351352### Documentation353- [Aggregation Reference](./references/aggregation-reference.md) — Pipeline stages, accumulator operators, and common aggregation recipes354- [Indexing Strategies](./references/indexing-strategies.md) — Index types, ESR rule, compound indexes, and performance analysis355356### Scripts357- [Seed Database](./scripts/seed-database.js) — Zero-dependency MongoDB seeding script with sample recipe data358359### Examples360- [Recipe API Example](./examples/recipe-api-example.md) — Complete Mongoose + Next.js Recipe CRUD API with models, routes, and validation361362---363364<!-- MCP:START -->365366<!-- PORTABILITY:START -->367## Cross-Client Portability368369This skill is written to stay usable across GitHub Copilot, Claude Code, and Codex.370371- GitHub Copilot: keep the folder in a Copilot-visible skill path or wrap the372 workflow in project instructions when folder discovery is unavailable.373- Claude Code: keep the folder in a local skills directory or a compatible plugin source.374- Codex: install or sync the folder into375 `$CODEX_HOME/skills/mongodb-mongoose` and restart Codex after major changes.376377<!-- PORTABILITY:END -->378379## MCP Availability And Fallback380381Preferred MCP Server: MongoDB MCP382383- Fallback prompt: "Use the Mongodb Mongoose skill without MCP. Rely on the local `SKILL.md`, bundled references or scripts, and manual verification. Show the exact commands, evidence, and final checks you used before concluding."384- Use `mongosh`, MongoDB Atlas UI, local schema files, and Mongoose model inspection when the MCP server is unavailable.385- Validate indexes, queries, and aggregation pipelines against a local or staging database before finalizing changes.386387<!-- MCP:END -->388389## Related Skills390391- [javascript-development](../javascript-development/SKILL.md): Use it when the workflow also needs modern JavaScript and TypeScript application code.392- [nextjs-development](../nextjs-development/SKILL.md): Use it when the workflow also needs Next.js App Router and server-first React patterns.393- [sql-development](../sql-development/SKILL.md): Use it when the workflow also needs SQL query, schema, and performance tuning work.394- [code-quality](../code-quality/SKILL.md): Use it when the workflow also needs two-stage review (spec compliance first, then code quality), maintainability, and refactoring guidance.