CtrlK
BlogDocsLog inGet started
Tessl Logo

mongoose-starter

Scaffold a Mongoose 8.x project with TypeScript, schemas, models, virtuals, instance/static methods, hooks (pre/post), discriminators, population, and connection management.

73

Quality

64%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./database/mongoose-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Mongoose Starter

Scaffold a Mongoose 8.x project with TypeScript, schemas, models, virtuals, instance/static methods, hooks (pre/post), discriminators, population, and connection management.

Prerequisites

  • Node.js >= 20.x
  • TypeScript >= 5.3
  • MongoDB >= 7.x (or MongoDB Atlas)
  • npm or pnpm

Scaffold Command

npm install mongoose
npm install -D typescript @types/node tsx

Database Initialization

# 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

Project Structure

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

Key Conventions

  • One file per model under src/models/. Each file exports the schema, model, and TypeScript interfaces.
  • Use Mongoose's built-in TypeScript support: define an interface for the document, then pass it as a generic to Schema<T> and model<T>.
  • Use a singleton connection pattern. Call connectDB() once at app startup.
  • Prefer lean() queries when you do not need Mongoose document methods (returns plain JS objects, faster).
  • Use indexes defined in the schema, not created manually in MongoDB.
  • Use pre/post hooks for cross-cutting concerns (password hashing, audit logging).
  • Use populate() sparingly. For complex joins, consider using aggregation pipelines.
  • Use discriminators for single-collection inheritance instead of separate collections.

Essential Patterns

Connection Management (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);
});

User Model (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);

Post Model with Population (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);

Discriminator Pattern (Single Collection Inheritance)

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 };

Repository Pattern (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);
}

Aggregation Pipeline Example

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 },
  ]);
}

Common Commands

# 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

Integration Notes

  • NestJS: Use @nestjs/mongoose with MongooseModule.forRootAsync() and MongooseModule.forFeature(). Schemas are defined slightly differently with NestJS decorators.
  • Express/Fastify: Call connectDB() in the server bootstrap before handling requests.
  • Testing: Use mongodb-memory-server for in-memory MongoDB during tests. Clear collections between tests with Model.deleteMany({}).
  • Docker: Pair with docker-compose-generator skill for the MongoDB service with healthcheck.
  • Auth: Pair with jwt-auth-skill. Store hashed passwords using bcrypt in the pre('save') hook.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.