database/mongoose-starter/SKILL.md
Scaffold a Mongoose 8.x project with TypeScript, schemas, models, virtuals, instance/static methods, hooks (pre/post), discriminators, population, and connection management.
npx skillsauth add achreftlili/deep-dev-skills mongoose-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold a Mongoose 8.x project with TypeScript, schemas, models, virtuals, instance/static methods, hooks (pre/post), discriminators, population, and connection management.
npm install mongoose
npm install -D typescript @types/node tsx
# MongoDB doesn't require schema migrations — collections are created on first write.
# Ensure MongoDB is running:
mongosh --eval "db.runCommand({ ping: 1 })"
# Create indexes defined in schemas
node -e "require('./src/models'); setTimeout(() => process.exit(), 3000)"
# Seed initial data (if seed script exists)
npm run seed
src/
lib/
mongoose.ts # Connection management
models/
user.model.ts # Schema + Model + Types
post.model.ts
comment.model.ts
repositories/
user.repository.ts
post.repository.ts
src/models/. Each file exports the schema, model, and TypeScript interfaces.Schema<T> and model<T>.connectDB() once at app startup.lean() queries when you do not need Mongoose document methods (returns plain JS objects, faster).pre/post hooks for cross-cutting concerns (password hashing, audit logging).populate() sparingly. For complex joins, consider using aggregation pipelines.src/lib/mongoose.ts)import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI ?? "mongodb://localhost:27017/myapp";
export async function connectDB(): Promise<void> {
if (mongoose.connection.readyState === 1) return;
await mongoose.connect(MONGODB_URI, {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
});
console.log("MongoDB connected");
}
export async function disconnectDB(): Promise<void> {
await mongoose.disconnect();
}
// Graceful shutdown
process.on("SIGINT", async () => {
await disconnectDB();
process.exit(0);
});
src/models/user.model.ts)import mongoose, { Schema, Document, Model, Types } from "mongoose";
// --- Interfaces ---
export interface IUser {
email: string;
name: string;
password: string;
role: "user" | "admin";
avatar?: string;
createdAt: Date;
updatedAt: Date;
}
export interface IUserMethods {
comparePassword(candidate: string): Promise<boolean>;
fullDisplayName(): string;
}
export interface IUserDocument extends IUser, IUserMethods, Document {
_id: Types.ObjectId;
}
interface IUserModel extends Model<IUser, object, IUserMethods> {
findByEmail(email: string): Promise<IUserDocument | null>;
}
// --- Schema ---
const userSchema = new Schema<IUser, IUserModel, IUserMethods>(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
index: true,
},
name: { type: String, required: true, trim: true },
password: { type: String, required: true, select: false },
role: { type: String, enum: ["user", "admin"], default: "user" },
avatar: { type: String },
},
{
timestamps: true,
toJSON: {
virtuals: true,
transform(_doc, ret) {
delete ret.password;
delete ret.__v;
return ret;
},
},
}
);
// --- Virtuals ---
userSchema.virtual("posts", {
ref: "Post",
localField: "_id",
foreignField: "author",
});
// --- Indexes ---
userSchema.index({ role: 1, createdAt: -1 });
// --- Instance Methods ---
userSchema.methods.comparePassword = async function (candidate: string): Promise<boolean> {
// In production, use bcrypt.compare(candidate, this.password)
return candidate === this.password;
};
userSchema.methods.fullDisplayName = function (): string {
return `${this.name} (${this.role})`;
};
// --- Static Methods ---
userSchema.statics.findByEmail = function (email: string) {
return this.findOne({ email: email.toLowerCase() }).select("+password");
};
// --- Hooks ---
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) return next();
// In production: this.password = await bcrypt.hash(this.password, 12);
next();
});
userSchema.post("save", function (doc) {
console.log(`User saved: ${doc.email}`);
});
// --- Model ---
export const User = mongoose.model<IUser, IUserModel>("User", userSchema);
src/models/post.model.ts)import mongoose, { Schema, Types } from "mongoose";
export interface IPost {
title: string;
content?: string;
status: "draft" | "published" | "archived";
author: Types.ObjectId;
tags: string[];
viewCount: number;
publishedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
const postSchema = new Schema<IPost>(
{
title: { type: String, required: true },
content: { type: String },
status: {
type: String,
enum: ["draft", "published", "archived"],
default: "draft",
index: true,
},
author: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
index: true,
},
tags: [{ type: String, lowercase: true, trim: true }],
viewCount: { type: Number, default: 0 },
publishedAt: { type: Date },
},
{ timestamps: true }
);
// Compound index for common query patterns
postSchema.index({ status: 1, createdAt: -1 });
postSchema.index({ author: 1, status: 1 });
postSchema.index({ tags: 1 });
// Text index for search
postSchema.index({ title: "text", content: "text" });
// Auto-set publishedAt when status changes to published
postSchema.pre("save", function (next) {
if (this.isModified("status") && this.status === "published" && !this.publishedAt) {
this.publishedAt = new Date();
}
next();
});
export const Post = mongoose.model<IPost>("Post", postSchema);
import mongoose, { Schema } from "mongoose";
// Base notification schema
interface INotification {
recipient: mongoose.Types.ObjectId;
read: boolean;
createdAt: Date;
}
const notificationSchema = new Schema<INotification>(
{
recipient: { type: Schema.Types.ObjectId, ref: "User", required: true },
read: { type: Boolean, default: false },
},
{ timestamps: true, discriminatorKey: "kind" }
);
export const Notification = mongoose.model("Notification", notificationSchema);
// Email notification discriminator
const EmailNotification = Notification.discriminator(
"EmailNotification",
new Schema({ subject: String, body: String })
);
// Push notification discriminator
const PushNotification = Notification.discriminator(
"PushNotification",
new Schema({ title: String, message: String, token: String })
);
export { EmailNotification, PushNotification };
src/repositories/user.repository.ts)import { User, type IUser } from "../models/user.model";
import type { FilterQuery } from "mongoose";
export async function createUser(data: Pick<IUser, "email" | "name" | "password">) {
return User.create(data);
}
export async function findUserById(id: string) {
return User.findById(id).populate("posts");
}
export async function findUserByEmail(email: string) {
return User.findByEmail(email);
}
export async function listUsers(params: {
page: number;
limit: number;
role?: "user" | "admin";
}) {
const filter: FilterQuery<IUser> = {};
if (params.role) filter.role = params.role;
const [users, total] = await Promise.all([
User.find(filter)
.sort({ createdAt: -1 })
.skip((params.page - 1) * params.limit)
.limit(params.limit)
.lean(),
User.countDocuments(filter),
]);
return { users, total, pages: Math.ceil(total / params.limit) };
}
export async function updateUser(id: string, data: Partial<IUser>) {
return User.findByIdAndUpdate(id, { $set: data }, { new: true, runValidators: true });
}
export async function deleteUser(id: string) {
return User.findByIdAndDelete(id);
}
import { Post } from "../models/post.model";
export async function getAuthorStats() {
return Post.aggregate([
{ $match: { status: "published" } },
{
$group: {
_id: "$author",
postCount: { $sum: 1 },
totalViews: { $sum: "$viewCount" },
avgViews: { $avg: "$viewCount" },
},
},
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "author",
},
},
{ $unwind: "$author" },
{
$project: {
_id: 0,
authorName: "$author.name",
postCount: 1,
totalViews: 1,
avgViews: { $round: ["$avgViews", 0] },
},
},
{ $sort: { totalViews: -1 } },
{ $limit: 10 },
]);
}
# Start MongoDB locally (Docker)
docker run -d --name mongodb -p 27017:27017 mongo:7
# Connect with mongosh
mongosh mongodb://localhost:27017/myapp
# Start the application
npx tsx src/main.ts
# Run with watch mode
npx tsx watch src/main.ts
@nestjs/mongoose with MongooseModule.forRootAsync() and MongooseModule.forFeature(). Schemas are defined slightly differently with NestJS decorators.connectDB() in the server bootstrap before handling requests.mongodb-memory-server for in-memory MongoDB during tests. Clear collections between tests with Model.deleteMany({}).docker-compose-generator skill for the MongoDB service with healthcheck.jwt-auth-skill. Store hashed passwords using bcrypt in the pre('save') hook.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.