Skip to content

Mongoose Plugin

MongoDB is a document-oriented database that fits naturally with TypeScript's object structure. The MongoosePlugin integrates the popular Mongoose ODM into Karin's lifecycle, managing connection pooling, schema synchronization, and graceful teardown.

Concept: The Pantry

In our Restaurant analogy, the Database is the Pantry.

  • The Entities are the inventory list (Schemas).
  • The Service (Chef) doesn't grow the vegetables; it requests them from the Pantry.
  • The Connection Pool is the team of runners fetching ingredients. You don't want to hire a new runner for every single carrot (Connection Overhead); you want a standing team ready to go (Pool).

Dependency Injection Architecture

Unlike vanilla Mongoose where you might import User model globally, Karin enforces Dependency Injection. This ensures your services remain testable (you can mock the Model) and modular.

1. Defining Schemas (The Entity Layer)

We use decorators to define the schema metadata. This keeps the definition close to the TypeScript class, reducing context switching.

typescript
import { Schema, Prop } from "@project-karin/mongoose";
import { Types } from "mongoose";

@Schema({ 
  timestamps: true, 
  collection: "users",
  // Automatically create indexes on startup
  autoIndex: process.env.NODE_ENV !== "production" 
})
export class User {
  @Prop({ required: true, index: true })
  email: string;

  @Prop({ type: [String], default: ["user"] })
  roles: string[];

  // Relationships
  @Prop({ type: Types.ObjectId, ref: "Organization" })
  organization: Types.ObjectId;
}

2. Injecting Models

Instead of calling User.find(), you inject the model instance. This "Model" is actually a Mongoose Model<Document> wrapper that is scoped to the active connection.

typescript
import { Service } from "@project-karin/core";
import { InjectModel } from "@project-karin/mongoose";
import { Model } from "mongoose";
import { User } from "./entities/user.entity";

@Service()
export class UserService {
  constructor(
    @InjectModel(User) private readonly userModel: Model<User>
  ) {}

  async findByEmail(email: string) {
    // Standard Mongoose API
    return this.userModel.findOne({ email }).exec();
  }
}

Connection Strategy & Lifecycle

One of the most complex aspects of using MongoDB in microservices is managing connections, specifically the Connection Pool.

The Buffering Problem

Mongoose, by default, buffers commands. If you try to query users before the database connects, Mongoose queues the query. In a serverless environment, this can lead to timeouts. The MongoosePlugin configures the connection strategy based on your environment.

  • Long-Running Server: Maintains a persistent pool of TCP connections (default pool size: 5). It relies on Mongoose's auto-reconnect logic.
  • Serverless (Workers/Lambda): The plugin detects this mode and:
    1. Disables buffering (Fail Fast).
    2. Check for an existing "Hot" connection on the global scope to reuse it between invocations (Lambda Warm Start).

Automatic Error Filtering

The plugin optionally registers a global Exception Filter. This filter intercepts Mongoose-specific errors (like ValidationError or MongoServerError: E11000 duplicate key) and transforms them into standard HTTP 400/409 Exceptions.

typescript
// main.ts
new MongoosePlugin({
  uri: "...",
  // Explicitly enable/disable the automatic filter
  autoRegisterExceptionFilter: true, 
})

Advanced Configuration

For multi-tenant applications or complex setups, you can connect to multiple databases.

typescript
// Primary DB
const usersDb = new MongoosePlugin({
  uri: "mongodb://...",
  connectionName: "PRIMARY",
  models: [User]
});

// Analytics DB
const analyticsDb = new MongoosePlugin({
  uri: "mongodb://...",
  connectionName: "ANALYTICS",
  models: [Event]
});

// Service Injection
constructor(
  @InjectModel(User, "PRIMARY") private userModel: Model<User>,
  @InjectModel(Event, "ANALYTICS") private eventModel: Model<Event>,
)

Released under the MIT License.