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.
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.
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:
- Disables buffering (Fail Fast).
- Check for an existing "Hot" connection on the
globalscope 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.
// 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.
// 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>,
)