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
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. 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: 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
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 Mongoose
6
7> Optimized for current MongoDB server releases, Mongoose 8.x+, Node.js 22+, and TypeScript 5.5+.
8
9Comprehensive guidance for MongoDB database design, Mongoose ODM patterns, and Atlas integration for Node.js/Next.js applications.
10
11- Leverage native parallel subagent dispatch and 200k+ context windows where available.
12
13
14## When to Use This Skill
15
16Use symptom -> action triggers: when one matches, apply this skill and verify with the protocol below.
17
18- Designing MongoDB schemas and data models
19- Building Mongoose models with validation and middleware
20- Implementing the repository pattern for data access
21- Writing aggregation pipelines for complex queries
22- Managing MongoDB Atlas connections and configuration
23- Integrating MongoDB with Next.js API routes
24- Database migration strategies
25
26
27---
28
29<!-- MCP:START -->
30
31<!-- PORTABILITY:START -->
32## Cross-Client Portability
33
34This skill is written to stay usable across GitHub Copilot, Claude Code, and Codex.
35
36- GitHub Copilot: keep the folder in a Copilot-visible skill path or wrap the
37 workflow in project instructions when folder discovery is unavailable.
38- Claude Code: keep the folder in a local skills directory or a compatible plugin source.
39- Codex: install or sync the folder into
40 `$CODEX_HOME/skills/mongodb-mongoose` and restart Codex after major changes.
41
42<!-- PORTABILITY:END -->
43
44## MCP Availability And Fallback
45
46Preferred MCP Server: MongoDB MCP
47
48- 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."
49- Use `mongosh`, MongoDB Atlas UI, local schema files, and Mongoose model inspection when the MCP server is unavailable.
50- Validate indexes, queries, and aggregation pipelines against a local or staging database before finalizing changes.
51- Do not claim an MCP operation was used when the active host does not expose it.
52
53<!-- MCP:END -->
54
55## Anti-Patterns
56
57- Modeling documents like normalized tables by default: MongoDB performance depends on query-driven shape, not relational purity.
58- Returning full hydrated documents for every request: Over-fetching and hydration overhead accumulate quickly in API paths.
59- Adding middleware without write-path tests: Hooks can silently change create, update, and migration behavior.
60
61## Verification Protocol
62
63Before claiming "skill applied successfully":
64
651. Pass/fail: The Mongodb Mongoose implementation names the target runtime, framework version, and affected files.
662. Pass/fail: Build, lint, test, or equivalent local validation is run for the changed surface.
673. Pass/fail: Edge cases for errors, dependency drift, and environment differences are addressed or explicitly out of scope.
684. Pressure-test scenario: Apply the workflow to a change that passes happy-path tests but fails one boundary condition.
695. Success metric: Zero untested success claims; every implementation claim maps to a command or artifact.
70
71## Before and After Example
72
73```javascript
74// Before
75const recipes = await Recipe.find({ author: userId }).populate('author');
76
77// After
78const recipes = await Recipe.find({ author: userId, isPublished: true })
79 .select({ title: 1, slug: 1, createdAt: 1 })
80 .sort({ createdAt: -1 })
81 .lean();
82```
83
84Narrows the query shape, avoids unnecessary hydration, and aligns the result with the view model actually needed.
85
86## Schema Design
87
88### Data Modeling Principles
89- **Embed** when data is accessed together and has a 1:few relationship
90- **Reference** when data is accessed independently or has a 1:many/many:many relationship
91- Design schemas around query patterns, not normalized relational models
92- Use denormalization strategically for read performance
93
94### Mongoose Model Pattern
95```javascript
96import mongoose from 'mongoose';
97
98const recipeSchema = new mongoose.Schema({
99 title: {
100 type: String,
101 required: [true, 'Title is required'],
102 trim: true,
103 maxlength: [200, 'Title cannot exceed 200 characters'],
104 index: true,
105 },
106 slug: {
107 type: String,
108 unique: true,
109 lowercase: true,
110 },
111 ingredients: [{
112 name: { type: String, required: true },
113 amount: { type: Number, required: true },
114 unit: { type: String, enum: ['g', 'kg', 'ml', 'l', 'cup', 'tbsp', 'tsp', 'piece'] },
115 }],
116 author: {
117 type: mongoose.Schema.Types.ObjectId,
118 ref: 'User',
119 required: true,
120 index: true,
121 },
122 tags: [{ type: String, lowercase: true, trim: true }],
123 isPublished: { type: Boolean, default: false },
124}, {
125 timestamps: true,
126 toJSON: { virtuals: true },
127 toObject: { virtuals: true },
128});
129
130// Indexes for common queries
131recipeSchema.index({ title: 'text', tags: 'text' });
132recipeSchema.index({ author: 1, createdAt: -1 });
133
134// Virtual fields
135recipeSchema.virtual('ingredientCount').get(function() {
136 return this.ingredients.length;
137});
138
139// Pre-save middleware
140recipeSchema.pre('save', function(next) {
141 if (this.isModified('title')) {
142 this.slug = this.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
143 }
144 next();
145});
146
147export const Recipe = mongoose.models.Recipe || mongoose.model('Recipe', recipeSchema);
148```
149
150### Schema Best Practices
151- Always define `required`, `type`, and validation rules
152- Use `timestamps: true` for automatic `createdAt`/`updatedAt`
153- Add indexes for frequently queried fields
154- Use `enum` for fields with fixed values
155- Define virtuals for computed properties
156- Use middleware (pre/post hooks) for side effects
157
158---
159
160## Repository Pattern
161
162```javascript
163class RecipeRepository {
164 async findAll(filter = {}, options = {}) {
165 const { page = 1, limit = 20, sort = '-createdAt', populate = '' } = options;
166 const skip = (page - 1) * limit;
167
168 const [recipes, total] = await Promise.all([
169 Recipe.find(filter)
170 .sort(sort)
171 .skip(skip)
172 .limit(limit)
173 .populate(populate)
174 .lean(),
175 Recipe.countDocuments(filter),
176 ]);
177
178 return {
179 data: recipes,
180 pagination: {
181 page,
182 limit,
183 total,
184 pages: Math.ceil(total / limit),
185 },
186 };
187 }
188
189 async findById(id) {
190 return Recipe.findById(id).populate('author', 'name avatar').lean();
191 }
192
193 async create(data) {
194 const recipe = new Recipe(data);
195 return recipe.save();
196 }
197
198 async update(id, data) {
199 return Recipe.findByIdAndUpdate(id, data, {
200 new: true,
201 runValidators: true,
202 });
203 }
204
205 async delete(id) {
206 return Recipe.findByIdAndDelete(id);
207 }
208
209 async search(query, options = {}) {
210 return this.findAll(
211 { $text: { $search: query } },
212 { ...options, sort: { score: { $meta: 'textScore' } } }
213 );
214 }
215}
216
217export const recipeRepository = new RecipeRepository();
218```
219
220---
221
222## Aggregation Pipelines
223
224### Common Patterns
225
226```javascript
227// Group recipes by tag with counts
228const tagStats = await Recipe.aggregate([
229 { $match: { isPublished: true } },
230 { $unwind: '$tags' },
231 { $group: { _id: '$tags', count: { $sum: 1 } } },
232 { $sort: { count: -1 } },
233 { $limit: 20 },
234]);
235
236// Author statistics with lookup
237const authorStats = await Recipe.aggregate([
238 { $group: {
239 _id: '$author',
240 recipeCount: { $sum: 1 },
241 avgRating: { $avg: '$rating' },
242 }},
243 { $lookup: {
244 from: 'users',
245 localField: '_id',
246 foreignField: '_id',
247 as: 'authorInfo',
248 }},
249 { $unwind: '$authorInfo' },
250 { $project: {
251 name: '$authorInfo.name',
252 recipeCount: 1,
253 avgRating: { $round: ['$avgRating', 1] },
254 }},
255 { $sort: { recipeCount: -1 } },
256]);
257
258// Date-based analytics
259const monthlyRecipes = await Recipe.aggregate([
260 { $match: { createdAt: { $gte: new Date('2024-01-01') } } },
261 { $group: {
262 _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } },
263 count: { $sum: 1 },
264 }},
265 { $sort: { _id: 1 } },
266]);
267```
268
269---
270
271## Atlas Connection
272
273### Connection Setup (Next.js)
274```javascript
275import mongoose from 'mongoose';
276
277const MONGODB_URI = process.env.MONGODB_URI;
278
279if (!MONGODB_URI) {
280 throw new Error('MONGODB_URI environment variable is not defined');
281}
282
283let cached = global.mongoose;
284if (!cached) {
285 cached = global.mongoose = { conn: null, promise: null };
286}
287
288export async function connectDB() {
289 if (cached.conn) return cached.conn;
290
291 if (!cached.promise) {
292 cached.promise = mongoose.connect(MONGODB_URI, {
293 bufferCommands: false,
294 });
295 }
296
297 cached.conn = await cached.promise;
298 return cached.conn;
299}
300```
301
302### Connection Best Practices
303- Cache connection in development to prevent multiple connections
304- Use `bufferCommands: false` for explicit error handling
305- Set connection pool size via `maxPoolSize` for production
306- Use Atlas connection string with `retryWrites=true&w=majority`
307
308---
309
310## Migration Strategies
311
312### Document Versioning
313```javascript
314const userSchema = new mongoose.Schema({
315 schemaVersion: { type: Number, default: 2 },
316 // ... fields
317});
318
319userSchema.pre('save', function(next) {
320 if (this.schemaVersion < 2) {
321 // Migrate old fields to new format
322 this.schemaVersion = 2;
323 }
324 next();
325});
326```
327
328### Batch Migration Script
329```javascript
330async function migrateUsers() {
331 const batchSize = 100;
332 let processed = 0;
333 let batch;
334
335 do {
336 batch = await User.find({ schemaVersion: { $lt: 2 } }).limit(batchSize);
337 for (const user of batch) {
338 user.schemaVersion = 2;
339 await user.save();
340 processed++;
341 }
342 console.log(`Migrated ${processed} users`);
343 } while (batch.length === batchSize);
344}
345```
346
347---
348
349## Performance Tips
350
351- Use `.lean()` for read-only queries (returns plain objects, 5-10x faster)
352- Use `.select()` to return only needed fields
353- Create compound indexes matching your query patterns
354- Use `$project` early in aggregation to reduce working set
355- Avoid `$lookup` in high-frequency queries; denormalize instead
356- Use `explain()` to analyze query performance
357
358## Troubleshooting
359
360| Issue | Solution |
361|-------|----------|
362| Slow queries | Add indexes, use `.lean()`, check with `explain()` |
363| Connection timeouts | Check Atlas network access, increase pool size |
364| Validation errors | Review schema constraints, check middleware order |
365| Duplicate key errors | Ensure unique indexes, handle with try/catch |
366| Memory issues | Use cursors for large datasets, limit batch sizes |
367
368---
369
370## Common Pitfalls
371
372- Modeling data like a normalized relational schema by default: MongoDB performance depends on query-driven document shape, not tables-first design.
373- Returning full hydrated documents everywhere: Hydration and over-fetching add cost when a lean projection would do.
374- Adding middleware without explicit write-path tests: Hooks can silently change behavior in create, update, and migration flows.
375
376## References & Resources
377
378### Documentation
379- [Aggregation Reference](./references/aggregation-reference.md) — Pipeline stages, accumulator operators, and common aggregation recipes
380- [Indexing Strategies](./references/indexing-strategies.md) — Index types, ESR rule, compound indexes, and performance analysis
381
382### Scripts
383- [Seed Database](./scripts/seed-database.js) — Zero-dependency MongoDB seeding script with sample recipe data
384
385### Examples
386- [Recipe API Example](./examples/recipe-api-example.md) — Complete Mongoose + Next.js Recipe CRUD API with models, routes, and validation
387
388---
389
390## Related Skills
391
392- [javascript-development](../javascript-development/SKILL.md): Use it when the workflow also needs modern JavaScript and TypeScript application code.
393- [nextjs-development](../nextjs-development/SKILL.md): Use it when the workflow also needs Next.js App Router and server-first React patterns.
394- [sql-development](../sql-development/SKILL.md): Use it when the workflow also needs SQL query, schema, and performance tuning work.
395- [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.