
Mongodb Mongoose
- 28 installs
- 7 repo stars
- Updated August 2, 2026
- practicalswan/agent-skills
mongodb-mongoose is a Claude Code skill for ai & agent building.
About
mongodb-mongoose is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- mongodb-mongoose
- AI & Agent Building
- AI-coding skill
Mongodb Mongoose by the numbers
- 28 all-time installs (skills.sh)
- Ranked #9,505 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/practicalswan/agent-skills --skill mongodb-mongooseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 28 |
|---|---|
| repo stars | ★ 7 |
| Last updated | August 2, 2026 |
| Repository | practicalswan/agent-skills ↗ |
How do I helps with ai & agent building tasks.?
Helps with ai & agent building tasks.
Who is it for?
Best when you're working on ai & agent building and need structured help with mongodb mongoose.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks., or when mongodb-mongoose is a claude code skill for ai & agent building.
What you get
Structured output aligned to mongodb-mongoose: mongodb-mongoose, AI & Agent Building.
Files
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":
1. Pass/fail: The Mongodb Mongoose implementation names the target runtime, framework version, and affected files. 2. Pass/fail: Build, lint, test, or equivalent local validation is run for the changed surface. 3. Pass/fail: Edge cases for errors, dependency drift, and environment differences are addressed or explicitly out of scope. 4. Pressure-test scenario: Apply the workflow to a change that passes happy-path tests but fails one boundary condition. 5. 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
---
<!-- PORTABILITY:START -->
Cross-Client Portability
This skill is written to stay usable across GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- GitHub Copilot: keep the folder in a Copilot-visible skill or plugin path, or wrap the workflow as project instructions if the host does not support portable skill folders directly.
- Claude Code: keep the folder in a local skills directory or a compatible plugin or marketplace source.
- Codex: install or sync the folder into
$CODEX_HOME/skills/<skill-name>and restart Codex after major changes. - Gemini CLI: this repository generates a project command named
/skills:mongodb-mongoosefrom this skill. Rebuild commands withpython scripts/export-gemini-skill.py mongodb-mongooseand then run/commands reloadinside Gemini CLI.
<!-- PORTABILITY:END -->
<!-- MCP:START -->
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.
<!-- MCP:END -->
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.
Changelog
[2026-04-25] - Version 1.2 Verification Protocol Refresh
Added
- Added a
Verification Protocolsection with skill-specific pass/fail checks, one pressure-test scenario, and a measurable success metric. - Added guidance to leverage native parallel subagent dispatch and 200k+ context windows where available.
Changed
- Updated
SKILL.mdfrontmatter toversion: "1.2"andlast_updated: 2026-04-25. - Reframed activation guidance toward symptom -> action triggers and standardized two-stage review wording where applicable.
[2026-04-24] - Version 1.1 Refresh
Changed
- Updated the SKILL frontmatter version to
1.1for the 2026-04-24 catalog refresh. - Added an "Optimized for ..." note at the top so the guidance is anchored to current platform versions.
[2026-04-24] - Skill Refresh
Changed
- Standardized the SKILL frontmatter with version metadata, last-updated date, tags, and a concise catalog description.
- Reformatted the portability and MCP guidance with a preferred server line, a copy-paste fallback prompt, and consistent bullet lists.
- Added a catalog-standard Anti-Patterns section and refreshed the Related Skills links at the end of the skill.
- Added an explicit before-and-after example and a Common Pitfalls section for Mongoose schema and query work.
[2026-04-24] - Catalog Audit Cleanup
Fixed
- Replaced stale
nestjsrelated-skill references with existing maintained skill links.
All notable changes to this skill will be documented in this file.
[2026-04-04] - Cross-Client Portability Refresh
Changed
- Added a standard portability note covering GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- Documented the preferred MCP server surface for this skill and a local no-MCP fallback workflow.
Tested
- Validated
SKILL.mdfrontmatter, portability sections, and Gemini export readiness withpython scripts/validate-skills.py.
[2026-03-09] - Workspace Modernization
Added
- Added a 2026-03-09 maintenance entry after reviewing the skill; the existing structure and guidance remained suitable.
[2026-02-28] — Description Rewrite & Cross-References
Changed
- Rewrote skill description to ~200 characters with clear, specific activation keywords
- Improved keyword specificity to reduce overlap with related skills
Added
## Related Skillscross-reference table with 2-4 related skills and "Use When" guidance
Recipe CRUD API — Mongoose + Next.js API Routes
Complete example: schema with validation, repository with pagination/search, API route handlers, connection utility, and aggregation for statistics.
---
1. Connection Utility
lib/mongodb.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.__mongooseCache;
if (!cached) {
cached = global.__mongooseCache = { 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 })
.then((m) => m);
}
cached.conn = await cached.promise;
return cached.conn;
}---
2. Recipe Schema
models/Recipe.js
import mongoose from "mongoose";
const ingredientSchema = new mongoose.Schema(
{
name: { type: String, required: true, trim: true },
quantity: { type: Number, required: true, min: 0 },
unit: { type: String, required: true, trim: true }
},
{ _id: false }
);
const stepSchema = new mongoose.Schema(
{
order: { type: Number, required: true },
instruction: { type: String, required: true, trim: true }
},
{ _id: false }
);
const recipeSchema = new mongoose.Schema(
{
title: {
type: String,
required: [true, "Title is required"],
trim: true,
maxlength: [200, "Title cannot exceed 200 characters"]
},
slug: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
description: { type: String, trim: true, maxlength: 2000 },
category: {
type: String,
required: true,
enum: [
"Italian", "Mexican", "Thai", "Japanese", "Indian",
"French", "Chinese", "American", "Mediterranean", "Korean", "Other"
]
},
tags: [{ type: String, trim: true, lowercase: true }],
ingredients: {
type: [ingredientSchema],
validate: [
(val) => val.length >= 1,
"At least one ingredient is required"
]
},
steps: {
type: [stepSchema],
validate: [(val) => val.length >= 1, "At least one step is required"]
},
prepTime: { type: Number, min: 0 },
cookTime: { type: Number, min: 0 },
servings: { type: Number, min: 1, default: 4 },
difficulty: {
type: String,
enum: ["Easy", "Medium", "Hard"],
default: "Medium"
},
rating: { type: Number, min: 0, max: 5, default: 0 },
views: { type: Number, default: 0 },
authorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true
},
status: {
type: String,
enum: ["draft", "published", "archived"],
default: "draft"
}
},
{ timestamps: true }
);
// ---------- Indexes (ESR order) ----------
recipeSchema.index({ status: 1, category: 1, rating: -1 });
recipeSchema.index({ authorId: 1, createdAt: -1 });
recipeSchema.index({ tags: 1 });
recipeSchema.index(
{ title: "text", description: "text", "ingredients.name": "text" },
{ weights: { title: 10, description: 5, "ingredients.name": 2 } }
);
// ---------- Middleware ----------
recipeSchema.pre("save", function (next) {
if (this.isModified("title") && !this.isModified("slug")) {
this.slug = this.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
next();
});
// Virtual: total time
recipeSchema.virtual("totalTime").get(function () {
return (this.prepTime || 0) + (this.cookTime || 0);
});
recipeSchema.set("toJSON", { virtuals: true });
export default mongoose.models.Recipe ||
mongoose.model("Recipe", recipeSchema);---
3. Recipe Repository
repositories/RecipeRepository.js
import Recipe from "@/models/Recipe";
export class RecipeRepository {
async findById(id) {
return Recipe.findById(id).populate("authorId", "displayName avatar").lean();
}
async findBySlug(slug) {
return Recipe.findOne({ slug })
.populate("authorId", "displayName avatar")
.lean();
}
async search({ query, category, tags, difficulty, page = 1, limit = 12 }) {
const filter = { status: "published" };
if (query) {
filter.$text = { $search: query };
}
if (category) {
filter.category = category;
}
if (tags?.length) {
filter.tags = { $in: tags };
}
if (difficulty) {
filter.difficulty = difficulty;
}
const skip = (page - 1) * limit;
const [recipes, total] = await Promise.all([
Recipe.find(filter)
.sort(query ? { score: { $meta: "textScore" } } : { createdAt: -1 })
.skip(skip)
.limit(limit)
.populate("authorId", "displayName avatar")
.lean(),
Recipe.countDocuments(filter)
]);
return {
data: recipes,
total,
page,
totalPages: Math.ceil(total / limit)
};
}
async create(data) {
const recipe = new Recipe(data);
await recipe.save();
return recipe.toJSON();
}
async update(id, data) {
return Recipe.findByIdAndUpdate(id, data, {
new: true,
runValidators: true
}).lean();
}
async delete(id) {
return Recipe.findByIdAndDelete(id).lean();
}
async incrementViews(id) {
return Recipe.findByIdAndUpdate(id, { $inc: { views: 1 } });
}
async findByAuthor(authorId, { page = 1, limit = 12 } = {}) {
const skip = (page - 1) * limit;
const filter = { authorId };
const [recipes, total] = await Promise.all([
Recipe.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean(),
Recipe.countDocuments(filter)
]);
return { data: recipes, total, page, totalPages: Math.ceil(total / limit) };
}
// ---------- Aggregations ----------
async getStatistics() {
const [result] = await Recipe.aggregate([
{
$facet: {
overview: [
{
$group: {
_id: null,
totalRecipes: { $sum: 1 },
published: {
$sum: { $cond: [{ $eq: ["$status", "published"] }, 1, 0] }
},
avgRating: { $avg: "$rating" },
totalViews: { $sum: "$views" }
}
}
],
byCategory: [
{
$group: {
_id: "$category",
count: { $sum: 1 },
avgRating: { $avg: "$rating" }
}
},
{ $sort: { count: -1 } }
],
byDifficulty: [
{ $group: { _id: "$difficulty", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
],
topRated: [
{ $match: { status: "published" } },
{ $sort: { rating: -1 } },
{ $limit: 5 },
{ $project: { title: 1, rating: 1, category: 1, slug: 1 } }
],
monthlyTrend: [
{
$group: {
_id: {
year: { $year: "$createdAt" },
month: { $month: "$createdAt" }
},
count: { $sum: 1 }
}
},
{ $sort: { "_id.year": 1, "_id.month": 1 } },
{ $limit: 12 }
]
}
}
]);
return {
overview: result.overview[0] ?? {},
byCategory: result.byCategory,
byDifficulty: result.byDifficulty,
topRated: result.topRated,
monthlyTrend: result.monthlyTrend
};
}
}
export const recipeRepository = new RecipeRepository();---
4. API Route Handlers
List / Create — app/api/recipes/route.js
import { NextResponse } from "next/server";
import { connectDB } from "@/lib/mongodb";
import { recipeRepository } from "@/repositories/RecipeRepository";
export async function GET(request) {
try {
await connectDB();
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1", 10);
const limit = Math.min(parseInt(searchParams.get("limit") || "12", 10), 50);
const query = searchParams.get("q") || undefined;
const category = searchParams.get("category") || undefined;
const difficulty = searchParams.get("difficulty") || undefined;
const tags = searchParams.get("tags")?.split(",").filter(Boolean);
const result = await recipeRepository.search({
query, category, tags, difficulty, page, limit
});
return NextResponse.json(result);
} catch (err) {
console.error("GET /api/recipes error:", err);
return NextResponse.json(
{ error: "Failed to fetch recipes" },
{ status: 500 }
);
}
}
export async function POST(request) {
try {
await connectDB();
const body = await request.json();
const recipe = await recipeRepository.create(body);
return NextResponse.json(recipe, { status: 201 });
} catch (err) {
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map((e) => e.message);
return NextResponse.json({ error: "Validation failed", errors }, { status: 400 });
}
if (err.code === 11000) {
return NextResponse.json(
{ error: "A recipe with this slug already exists" },
{ status: 409 }
);
}
console.error("POST /api/recipes error:", err);
return NextResponse.json(
{ error: "Failed to create recipe" },
{ status: 500 }
);
}
}Get / Update / Delete — app/api/recipes/[id]/route.js
import { NextResponse } from "next/server";
import mongoose from "mongoose";
import { connectDB } from "@/lib/mongodb";
import { recipeRepository } from "@/repositories/RecipeRepository";
function isValidObjectId(id) {
return mongoose.Types.ObjectId.isValid(id);
}
export async function GET(request, { params }) {
try {
await connectDB();
const { id } = await params;
if (!isValidObjectId(id)) {
return NextResponse.json({ error: "Invalid recipe ID" }, { status: 400 });
}
const recipe = await recipeRepository.findById(id);
if (!recipe) {
return NextResponse.json({ error: "Recipe not found" }, { status: 404 });
}
await recipeRepository.incrementViews(id);
return NextResponse.json(recipe);
} catch (err) {
console.error("GET /api/recipes/[id] error:", err);
return NextResponse.json(
{ error: "Failed to fetch recipe" },
{ status: 500 }
);
}
}
export async function PUT(request, { params }) {
try {
await connectDB();
const { id } = await params;
if (!isValidObjectId(id)) {
return NextResponse.json({ error: "Invalid recipe ID" }, { status: 400 });
}
const body = await request.json();
const recipe = await recipeRepository.update(id, body);
if (!recipe) {
return NextResponse.json({ error: "Recipe not found" }, { status: 404 });
}
return NextResponse.json(recipe);
} catch (err) {
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map((e) => e.message);
return NextResponse.json({ error: "Validation failed", errors }, { status: 400 });
}
console.error("PUT /api/recipes/[id] error:", err);
return NextResponse.json(
{ error: "Failed to update recipe" },
{ status: 500 }
);
}
}
export async function DELETE(request, { params }) {
try {
await connectDB();
const { id } = await params;
if (!isValidObjectId(id)) {
return NextResponse.json({ error: "Invalid recipe ID" }, { status: 400 });
}
const recipe = await recipeRepository.delete(id);
if (!recipe) {
return NextResponse.json({ error: "Recipe not found" }, { status: 404 });
}
return NextResponse.json({ message: "Recipe deleted" });
} catch (err) {
console.error("DELETE /api/recipes/[id] error:", err);
return NextResponse.json(
{ error: "Failed to delete recipe" },
{ status: 500 }
);
}
}Statistics — app/api/recipes/stats/route.js
import { NextResponse } from "next/server";
import { connectDB } from "@/lib/mongodb";
import { recipeRepository } from "@/repositories/RecipeRepository";
export async function GET() {
try {
await connectDB();
const stats = await recipeRepository.getStatistics();
return NextResponse.json(stats);
} catch (err) {
console.error("GET /api/recipes/stats error:", err);
return NextResponse.json(
{ error: "Failed to fetch statistics" },
{ status: 500 }
);
}
}---
5. Usage Examples
Fetch recipes with search and pagination
const res = await fetch("/api/recipes?q=pasta&category=Italian&page=1&limit=10");
const { data, total, page, totalPages } = await res.json();Create a recipe
const res = await fetch("/api/recipes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Spicy Thai Basil Stir-Fry",
description: "A quick and flavorful Thai stir-fry.",
category: "Thai",
tags: ["spicy", "quick"],
ingredients: [
{ name: "chicken breast", quantity: 300, unit: "g" },
{ name: "Thai basil", quantity: 1, unit: "cups" },
{ name: "chili flakes", quantity: 1, unit: "tsp" }
],
steps: [
{ order: 1, instruction: "Slice the chicken into thin strips." },
{ order: 2, instruction: "Stir-fry on high heat with garlic and chili." },
{ order: 3, instruction: "Add basil leaves and serve over rice." }
],
prepTime: 10,
cookTime: 15,
servings: 2,
difficulty: "Easy",
authorId: "665a1b2c3d4e5f6a7b8c9d0e",
status: "published"
})
});Get statistics dashboard
const res = await fetch("/api/recipes/stats");
const { overview, byCategory, byDifficulty, topRated, monthlyTrend } =
await res.json();
console.log(`Total recipes: ${overview.totalRecipes}`);
console.log(`Average rating: ${overview.avgRating.toFixed(1)}`);
console.log(`Top category: ${byCategory[0]._id} (${byCategory[0].count})`);MIT License
Copyright (c) 2026 Sithu Win San
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MongoDB Aggregation Pipeline Reference
Quick-reference for aggregation stages, accumulators, and expression operators used with Mongoose's Model.aggregate().
---
Pipeline Stages
$match
Filter documents (like find()). Place early to leverage indexes.
{ $match: { status: "published", rating: { $gte: 4 } } }$group
Group documents by key and apply accumulators.
{
$group: {
_id: "$category",
count: { $sum: 1 },
avgRating: { $avg: "$rating" },
recipes: { $push: "$title" }
}
}$project
Reshape documents — include, exclude, or compute fields.
{
$project: {
title: 1,
authorName: "$author.name",
ingredientCount: { $size: "$ingredients" },
_id: 0
}
}$lookup
Left outer join to another collection.
{
$lookup: {
from: "users",
localField: "authorId",
foreignField: "_id",
as: "author"
}
}Pipeline variant (correlated sub-query):
{
$lookup: {
from: "comments",
let: { recipeId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$recipeId", "$$recipeId"] } } },
{ $sort: { createdAt: -1 } },
{ $limit: 5 }
],
as: "recentComments"
}
}$unwind
Deconstruct an array field into one document per element.
{ $unwind: { path: "$tags", preserveNullAndEmptyArrays: true } }$sort
Order documents. 1 ascending, -1 descending.
{ $sort: { rating: -1, createdAt: -1 } }$limit
{ $limit: 10 }$skip
{ $skip: 20 }$addFields
Add or overwrite fields without dropping existing ones.
{
$addFields: {
fullName: { $concat: ["$firstName", " ", "$lastName"] },
isPopular: { $gte: ["$views", 1000] }
}
}$facet
Run multiple pipelines in parallel on the same input.
{
$facet: {
metadata: [{ $count: "total" }],
data: [{ $sort: { createdAt: -1 } }, { $skip: 0 }, { $limit: 10 }]
}
}$bucket
Group documents into value-range buckets.
{
$bucket: {
groupBy: "$prepTime",
boundaries: [0, 15, 30, 60, 120, Infinity],
default: "Other",
output: { count: { $sum: 1 }, recipes: { $push: "$title" } }
}
}$merge
Write pipeline output into an existing collection (upsert-capable).
{
$merge: {
into: "monthlyStats",
on: ["year", "month"],
whenMatched: "merge",
whenNotMatched: "insert"
}
}$out
Replace an entire collection with pipeline output.
{ $out: "cachedLeaderboard" }---
Accumulator Operators
Used inside $group (and $setWindowFields).
| Operator | Description | Example |
|---|---|---|
$sum | Sum numeric values or count | { $sum: "$price" } / { $sum: 1 } |
$avg | Average | { $avg: "$rating" } |
$first | First value in group (order-dependent) | { $first: "$title" } |
$last | Last value in group | { $last: "$updatedAt" } |
$push | Collect all values into an array | { $push: "$tag" } |
$addToSet | Collect unique values into an array | { $addToSet: "$category" } |
$min | Minimum value | { $min: "$prepTime" } |
$max | Maximum value | { $max: "$rating" } |
---
Expression Operators
$cond — if/then/else
{
$project: {
difficulty: {
$cond: {
if: { $lte: ["$prepTime", 15] },
then: "Easy",
else: "Advanced"
}
}
}
}$switch — multi-branch conditional
{
$project: {
difficulty: {
$switch: {
branches: [
{ case: { $lte: ["$prepTime", 15] }, then: "Easy" },
{ case: { $lte: ["$prepTime", 45] }, then: "Medium" }
],
default: "Hard"
}
}
}
}$map — transform each array element
{
$project: {
ingredientNames: {
$map: {
input: "$ingredients",
as: "ing",
in: "$$ing.name"
}
}
}
}$filter — keep matching array elements
{
$project: {
mainIngredients: {
$filter: {
input: "$ingredients",
as: "ing",
cond: { $eq: ["$$ing.isMain", true] }
}
}
}
}---
Common Recipes
Top N per Group
Get the top 3 recipes per category by rating:
const topPerCategory = await Recipe.aggregate([
{ $sort: { rating: -1 } },
{
$group: {
_id: "$category",
recipes: { $push: { title: "$title", rating: "$rating" } }
}
},
{
$project: {
category: "$_id",
topRecipes: { $slice: ["$recipes", 3] }
}
}
]);Running Totals
Cumulative sign-ups per day:
const runningTotals = await User.aggregate([
{
$group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
dailyCount: { $sum: 1 }
}
},
{ $sort: { _id: 1 } },
{
$setWindowFields: {
sortBy: { _id: 1 },
output: {
cumulativeUsers: {
$sum: "$dailyCount",
window: { documents: ["unbounded", "current"] }
}
}
}
}
]);Pivot (Rows to Columns)
Ratings distribution for a recipe:
const pivot = await Comment.aggregate([
{ $match: { recipeId: targetId } },
{
$group: {
_id: "$rating",
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } },
{
$group: {
_id: null,
distribution: { $push: { k: { $toString: "$_id" }, v: "$count" } }
}
},
{ $replaceRoot: { newRoot: { $arrayToObject: "$distribution" } } }
]);Time-Series Bucketing
Recipe submissions grouped by month:
const monthly = await Recipe.aggregate([
{
$group: {
_id: {
year: { $year: "$createdAt" },
month: { $month: "$createdAt" }
},
count: { $sum: 1 },
avgRating: { $avg: "$rating" }
}
},
{ $sort: { "_id.year": 1, "_id.month": 1 } },
{
$project: {
_id: 0,
period: {
$concat: [
{ $toString: "$_id.year" }, "-",
{ $cond: [{ $lt: ["$_id.month", 10] }, { $concat: ["0", { $toString: "$_id.month" }] }, { $toString: "$_id.month" }] }
]
},
count: 1,
avgRating: { $round: ["$avgRating", 1] }
}
}
]);Pagination with Total Count ($facet)
async function paginateRecipes(filter, page = 1, limit = 12) {
const skip = (page - 1) * limit;
const [result] = await Recipe.aggregate([
{ $match: filter },
{
$facet: {
metadata: [{ $count: "total" }],
data: [
{ $sort: { createdAt: -1 } },
{ $skip: skip },
{ $limit: limit }
]
}
}
]);
const total = result.metadata[0]?.total ?? 0;
return {
data: result.data,
total,
page,
totalPages: Math.ceil(total / limit)
};
}MongoDB Indexing Strategies
Reference for choosing, creating, and maintaining indexes in MongoDB with Mongoose.
---
Index Types
Single Field
Index on one field. Supports queries, sorts, and range scans on that field.
// Mongoose schema
recipeSchema.index({ slug: 1 });
// Shell
db.recipes.createIndex({ slug: 1 });Compound
Index on multiple fields. Supports queries that match a prefix of the index key pattern.
recipeSchema.index({ category: 1, rating: -1, createdAt: -1 });
// Supports queries on:
// { category }
// { category, rating }
// { category, rating, createdAt }
// Does NOT efficiently support:
// { rating } (not a prefix)Multikey
Automatically created when indexing a field that contains an array. Each array element gets an entry.
recipeSchema.index({ tags: 1 });
// Efficiently finds: { tags: "vegetarian" }A compound index can include at most one array field.
Text
Full-text search index. One per collection.
recipeSchema.index(
{ title: "text", description: "text", "ingredients.name": "text" },
{ weights: { title: 10, description: 5, "ingredients.name": 2 } }
);
// Query: db.recipes.find({ $text: { $search: "pasta tomato" } })2dsphere
Geospatial queries on GeoJSON data.
storeSchema.index({ location: "2dsphere" });
// Query: $nearSphere, $geoWithin, $geoIntersectsHashed
Hash of field value. Supports equality only (no range). Used for hashed sharding.
userSchema.index({ email: "hashed" });Wildcard
Index all fields (or a subtree) in documents with variable schemas.
// Index everything under metadata.*
productSchema.index({ "metadata.$**": 1 });---
Index Properties
Unique
Reject duplicate values.
userSchema.index({ email: 1 }, { unique: true });Partial
Index only documents matching a filter expression. Smaller index, faster writes.
recipeSchema.index(
{ rating: -1 },
{ partialFilterExpression: { status: "published" } }
);Queries must include the partial filter predicate to use this index.
Sparse
Only index documents where the field exists. Legacy alternative to partial indexes.
userSchema.index({ phone: 1 }, { sparse: true });TTL (Time-To-Live)
Auto-delete documents after a duration. Works on Date fields only.
sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
// Document deleted when current time >= expiresAt
// Fixed TTL from creation:
logSchema.index({ createdAt: 1 }, { expireAfterSeconds: 86400 }); // 24h---
Compound Index Ordering — The ESR Rule
Order compound index fields for maximum efficiency:
1. Equality — fields tested with = (exact match) 2. Sort — fields used in sort() 3. Range — fields tested with $gt, $lt, $in, $regex
Example: Find published recipes in a category, sorted by rating, with prep time under 30 min.
// Query
db.recipes.find({
status: "published", // Equality
category: "Italian", // Equality
prepTime: { $lte: 30 } // Range
}).sort({ rating: -1 }); // Sort
// Optimal index: Equality -> Sort -> Range
recipeSchema.index({ status: 1, category: 1, rating: -1, prepTime: 1 });---
Using explain()
Analyze query plans to verify index usage.
const explanation = await Recipe.find({ category: "Italian" })
.sort({ rating: -1 })
.explain("executionStats");
// Key fields to check:
// executionStats.nReturned — docs returned
// executionStats.totalDocsExamined — docs scanned (want ≈ nReturned)
// executionStats.totalKeysExamined — index keys scanned
// winningPlan.stage — "IXSCAN" = good, "COLLSCAN" = badQuick diagnostic rule: If totalDocsExamined >> nReturned, the index isn't selective enough.
Mongoose helper
mongoose.set("debug", true); // Log all queries to console---
Covered Queries
A query is covered when the index contains all requested fields — MongoDB never reads the document.
// Index
recipeSchema.index({ category: 1, title: 1, rating: 1 });
// Covered query (projected fields all in index, _id excluded)
db.recipes.find(
{ category: "Mexican" },
{ title: 1, rating: 1, _id: 0 }
);Verify with explain(): look for totalDocsExamined: 0.
---
Index Size Considerations
| Factor | Impact |
|---|---|
| Number of indexed fields | Each additional field increases index entry size |
| Array fields (multikey) | One entry per array element — can explode index size |
| String length | Long strings = larger index; consider hashed index for equality-only |
| Number of indexes | Each index adds write overhead (insert/update/delete) |
| Working set | Indexes should fit in RAM for best performance |
Check index sizes
db.recipes.stats().indexSizes
// { "_id_": 245760, "category_1_rating_-1": 163840, ... }
db.recipes.stats().totalIndexSize // bytesMongoose: list indexes for a model
const indexes = await Recipe.collection.getIndexes();
console.log(indexes);---
When NOT to Index
Indexes help reads but hurt writes. Avoid indexing when:
- Low-cardinality fields — A boolean
isActivefield with 50/50 distribution won't benefit much. Exception: partial indexes filtering on it. - Write-heavy, read-rare collections — Logs or event streams where you rarely query mid-collection.
- Small collections — Under ~1000 documents, a collection scan is fast enough.
- Fields only in aggregation — If a field is only used deep inside a pipeline after
$group, the index won't help. - Too many indexes per collection — Each index slows writes. Aim for the fewest indexes that cover your access patterns. Audit with
$indexStats. - Wide compound indexes on flexible queries — If queries combine fields unpredictably, a few targeted indexes beat one wide one.
Identifying unused indexes
db.recipes.aggregate([{ $indexStats: {} }]);
// Check "accesses.ops" — if 0 for weeks, consider dropping---
Mongoose Schema Index Declaration Summary
const recipeSchema = new Schema({ /* ... */ });
// Single field
recipeSchema.index({ slug: 1 }, { unique: true });
// Compound (ESR order)
recipeSchema.index({ status: 1, category: 1, rating: -1 });
// Text search
recipeSchema.index({ title: "text", description: "text" });
// TTL
recipeSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
// Partial
recipeSchema.index(
{ authorId: 1, createdAt: -1 },
{ partialFilterExpression: { status: "published" } }
);
// Ensure indexes are created (development only)
await mongoose.connection.syncIndexes();#!/usr/bin/env node
/**
* Seed a MongoDB database with sample users, recipes, and comments.
*
* Usage:
* node seed-database.js # seed with defaults (20 per collection)
* node seed-database.js --count 50 # seed 50 documents per collection
* node seed-database.js --clear # drop collections before seeding
* node seed-database.js --clear --count 100
*
* Requires MONGODB_URI env var (e.g. mongodb://localhost:27017/kitchen_odyssey).
* No external dependencies beyond the mongodb driver (or mongoose).
*/
const { MongoClient, ObjectId } = require("mongodb");
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
console.error("Error: MONGODB_URI environment variable is not set.");
process.exit(1);
}
const args = process.argv.slice(2);
const shouldClear = args.includes("--clear");
const countFlagIdx = args.indexOf("--count");
const COUNT =
countFlagIdx !== -1 && args[countFlagIdx + 1]
? Math.max(1, parseInt(args[countFlagIdx + 1], 10) || 20)
: 20;
// ---------------------------------------------------------------------------
// Simple random-data generators (no external deps)
// ---------------------------------------------------------------------------
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function pickN(arr, n) {
const shuffled = [...arr].sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.min(n, arr.length));
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomDate(startYear = 2023, endYear = 2026) {
const start = new Date(startYear, 0, 1).getTime();
const end = new Date(endYear, 0, 1).getTime();
return new Date(start + Math.random() * (end - start));
}
// Data pools
const FIRST_NAMES = [
"Alice", "Bob", "Carmen", "David", "Elena", "Frank", "Grace", "Henry",
"Iris", "Jack", "Karen", "Leo", "Mia", "Nathan", "Olivia", "Paul",
"Quinn", "Rita", "Sam", "Tina", "Uma", "Victor", "Wendy", "Xavier"
];
const LAST_NAMES = [
"Smith", "Johnson", "Lee", "Brown", "Garcia", "Kim", "Patel", "Chen",
"Williams", "Lopez", "Nguyen", "Anderson", "Tanaka", "Muller", "Costa"
];
const CATEGORIES = [
"Italian", "Mexican", "Thai", "Japanese", "Indian",
"French", "Chinese", "American", "Mediterranean", "Korean"
];
const TAGS = [
"vegetarian", "vegan", "gluten-free", "quick", "comfort-food",
"healthy", "spicy", "dessert", "breakfast", "one-pot",
"grilled", "baked", "soup", "salad", "seafood"
];
const RECIPE_ADJECTIVES = [
"Classic", "Spicy", "Creamy", "Zesty", "Smoky",
"Rustic", "Fresh", "Crispy", "Savory", "Tangy"
];
const RECIPE_NOUNS = [
"Pasta", "Tacos", "Curry", "Stir-Fry", "Soup",
"Salad", "Risotto", "Bowl", "Stew", "Sandwich",
"Noodles", "Dumplings", "Pizza", "Burrito", "Casserole"
];
const INGREDIENTS = [
"chicken breast", "olive oil", "garlic cloves", "onion", "tomatoes",
"salt", "black pepper", "basil", "rice", "soy sauce",
"ginger", "lemon juice", "bell pepper", "mushrooms", "spinach",
"cheese", "butter", "flour", "eggs", "cream",
"cilantro", "chili flakes", "coconut milk", "potatoes", "carrots"
];
const UNITS = ["g", "ml", "cups", "tbsp", "tsp", "pieces", "cloves", "slices"];
const COMMENT_TEXTS = [
"Loved this recipe! Will make again.",
"Turned out great, though I added extra garlic.",
"Easy to follow instructions. Delicious result.",
"My family really enjoyed this one.",
"Good but could use a bit more seasoning.",
"Perfect weeknight dinner recipe.",
"The leftovers were even better the next day!",
"I substituted tofu and it worked well.",
"Restaurant quality. Highly recommend.",
"Simple ingredients, amazing flavor.",
"A new household favorite!",
"Took longer than expected but worth the wait."
];
// ---------------------------------------------------------------------------
// Document generators
// ---------------------------------------------------------------------------
function generateUser(index) {
const first = pick(FIRST_NAMES);
const last = pick(LAST_NAMES);
const suffix = index.toString().padStart(3, "0");
return {
_id: new ObjectId(),
username: `${first.toLowerCase()}${last.toLowerCase()}${suffix}`,
email: `${first.toLowerCase()}.${last.toLowerCase()}${suffix}@example.com`,
displayName: `${first} ${last}`,
role: index === 0 ? "admin" : pick(["user", "user", "user", "admin"]),
avatar: `https://api.dicebear.com/7.x/initials/svg?seed=${first}${last}`,
bio: `Hi, I'm ${first}. I love cooking ${pick(CATEGORIES)} food!`,
createdAt: randomDate(2023, 2025),
updatedAt: randomDate(2025, 2026)
};
}
function generateRecipe(index, userIds) {
const ingredientCount = randomInt(4, 10);
const ingredients = pickN(INGREDIENTS, ingredientCount).map((name) => ({
name,
quantity: randomInt(1, 500),
unit: pick(UNITS)
}));
const stepCount = randomInt(3, 8);
const steps = Array.from({ length: stepCount }, (_, i) => ({
order: i + 1,
instruction: `Step ${i + 1}: ${pick(["Prepare", "Cook", "Mix", "Heat", "Combine", "Season", "Serve", "Let rest"])} the ${pick(INGREDIENTS)} ${pick(["until golden", "for 5 minutes", "thoroughly", "on medium heat", "until tender"])}.`
}));
return {
_id: new ObjectId(),
title: `${pick(RECIPE_ADJECTIVES)} ${pick(RECIPE_NOUNS)}`,
slug: `recipe-${index.toString().padStart(4, "0")}`,
description: `A delicious ${pick(CATEGORIES).toLowerCase()} dish that's perfect for ${pick(["weeknight dinners", "special occasions", "meal prep", "family gatherings"])}.`,
category: pick(CATEGORIES),
tags: pickN(TAGS, randomInt(1, 4)),
ingredients,
steps,
prepTime: randomInt(5, 30),
cookTime: randomInt(10, 90),
servings: randomInt(1, 8),
difficulty: pick(["Easy", "Medium", "Hard"]),
rating: Math.round((randomInt(25, 50) / 10) * 10) / 10,
views: randomInt(0, 5000),
authorId: pick(userIds),
status: pick(["published", "published", "published", "draft"]),
createdAt: randomDate(2024, 2026),
updatedAt: randomDate(2025, 2026)
};
}
function generateComment(userIds, recipeIds) {
return {
_id: new ObjectId(),
recipeId: pick(recipeIds),
authorId: pick(userIds),
text: pick(COMMENT_TEXTS),
rating: randomInt(1, 5),
createdAt: randomDate(2024, 2026)
};
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
let client;
try {
console.log(`Connecting to MongoDB...`);
client = new MongoClient(MONGODB_URI);
await client.connect();
const db = client.db();
console.log(`Database: ${db.databaseName}`);
if (shouldClear) {
console.log("Clearing existing data...");
await Promise.all([
db.collection("users").deleteMany({}),
db.collection("recipes").deleteMany({}),
db.collection("comments").deleteMany({})
]);
console.log("Collections cleared.");
}
// Generate users
console.log(`Generating ${COUNT} users...`);
const users = Array.from({ length: COUNT }, (_, i) => generateUser(i));
await db.collection("users").insertMany(users);
const userIds = users.map((u) => u._id);
// Generate recipes
console.log(`Generating ${COUNT} recipes...`);
const recipes = Array.from({ length: COUNT }, (_, i) =>
generateRecipe(i, userIds)
);
await db.collection("recipes").insertMany(recipes);
const recipeIds = recipes.map((r) => r._id);
// Generate comments (2-3x recipe count)
const commentCount = COUNT * randomInt(2, 3);
console.log(`Generating ${commentCount} comments...`);
const comments = Array.from({ length: commentCount }, () =>
generateComment(userIds, recipeIds)
);
await db.collection("comments").insertMany(comments);
console.log("\nSeed complete:");
console.log(` Users: ${users.length}`);
console.log(` Recipes: ${recipes.length}`);
console.log(` Comments: ${comments.length}`);
} catch (err) {
console.error("Seed failed:", err.message);
process.exit(1);
} finally {
if (client) await client.close();
}
}
main();
Related skills
FAQ
What does mongodb-mongoose do?
mongodb-mongoose is a Claude Code skill for ai & agent building.
When should I use mongodb-mongoose?
When you need to helps with ai & agent building tasks., or when mongodb-mongoose is a claude code skill for ai & agent building.
What are the main capabilities?
mongodb-mongoose; AI & Agent Building; AI-coding skill.