Scaffold a Mongoose 8.x project with TypeScript, schemas, models, virtuals, instance/static methods, hooks (pre/post), discriminators, population, and connection management.
73
64%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./database/mongoose-starter/SKILL.mdScaffold 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 seedsrc/
lib/
mongoose.ts # Connection management
models/
user.model.ts # Schema + Model + Types
post.model.ts
comment.model.ts
repositories/
user.repository.ts
post.repository.tssrc/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.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.