nestjs-mongo-transactions
v1.1.0
Published
A simple and powerful NestJS package for handling MongoDB transactions with Mongoose using the @Transaction decorator and AsyncLocalStorage
Maintainers
Readme
NestJS MongoDB Transactions
A simple and powerful NestJS package for handling MongoDB transactions with Mongoose using the @Transaction decorator. This package uses AsyncLocalStorage to automatically propagate transaction context to all nested MongoDB operations, making transaction management seamless and transparent.
Features
- 🎯 Simple Decorator: Just add
@Transaction()to any method - 🔄 Automatic Context Propagation: Uses AsyncLocalStorage to pass transaction context to nested calls
- 🔌 Mongoose Plugin: Automatically attaches session to all MongoDB operations
- 🏗️ NestJS Native: Designed specifically for NestJS applications
- 🛡️ Type-Safe: Full TypeScript support
- 📦 Zero Configuration: Works out of the box with minimal setup
Installation
npm install nestjs-mongo-transactionsPeer Dependencies
Make sure you have these installed:
npm install @nestjs/common @nestjs/mongoose mongoose reflect-metadataQuick Start
1. Setup Mongoose Connection
In your main module (e.g., app.module.ts), register the transaction plugin:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { createTransactionConnectionFactory } from 'nestjs-mongo-transactions';
@Module({
imports: [
MongooseModule.forRootAsync({
useFactory: () => ({
uri: 'mongodb://localhost:27017/mydb',
// Register the transaction plugin
connectionFactory: createTransactionConnectionFactory(),
}),
}),
],
})
export class AppModule {}2. Use the @Transaction Decorator
Simply add @Transaction() to any method where you want to use transactions:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Transaction } from 'nestjs-mongo-transactions';
@Injectable()
export class UserService {
constructor(
@InjectModel('User') private userModel: Model<User>,
@InjectModel('Account') private accountModel: Model<Account>,
) {}
@Transaction()
async createUserWithAccount(userData: any, accountData: any) {
// Both operations will be in the same transaction
const user = await this.userModel.create(userData);
const account = await this.accountModel.create({
...accountData,
userId: user._id,
});
// If this fails, both user and account creation will be rolled back
if (!account) {
throw new Error('Account creation failed');
}
return { user, account };
}
}3. Nested Calls Work Automatically
The transaction context propagates to nested service calls:
@Injectable()
export class OrderService {
constructor(
private userService: UserService,
private paymentService: PaymentService,
private inventoryService: InventoryService,
) {}
@Transaction()
async createOrder(orderData: any) {
// All of these operations (and their nested calls) are in the same transaction
const user = await this.userService.updateUser(orderData.userId, { ... });
const payment = await this.paymentService.processPayment(orderData.payment);
const inventory = await this.inventoryService.decreaseStock(orderData.items);
// If any operation fails, everything rolls back
return this.orderModel.create(orderData);
}
}
@Injectable()
export class PaymentService {
// No @Transaction decorator needed here - it inherits from the parent
async processPayment(paymentData: any) {
// This operation automatically uses the transaction from createOrder
return this.paymentModel.create(paymentData);
}
}Advanced Usage
Multiple Connections
If you have multiple MongoDB connections, you can specify which connection to use:
@Transaction('secondary')
async myMethod() {
// Uses the 'secondary' connection
}Manual Transaction Control
For advanced use cases, you can access the transaction context directly:
import { getCurrentSession, runWithSession } from 'nestjs-mongo-transactions';
async function manualTransactionControl() {
const session = getCurrentSession();
if (session) {
// You have access to the current session
console.log('In transaction:', session.id);
}
}Alternative Plugin Registration
If you prefer more control, you can register the plugin manually:
import { registerTransactionPlugin } from 'nestjs-mongo-transactions';
@Module({
imports: [
MongooseModule.forRootAsync({
useFactory: () => ({
uri: 'mongodb://localhost:27017/mydb',
connectionFactory: (connection) => {
registerTransactionPlugin(connection);
// Add your custom logic here
return connection;
},
}),
}),
],
})
export class AppModule {}How It Works
@Transaction Decorator: When you call a method decorated with
@Transaction(), it:- Starts a MongoDB session
- Begins a transaction
- Stores the session in AsyncLocalStorage
- Executes your method
- Commits on success or rolls back on error
- Cleans up the session
Mongoose Plugin: The plugin automatically attaches the session from AsyncLocalStorage to all Mongoose operations:
- Queries (find, findOne, update, delete, etc.)
- Aggregations
- Document saves
- All nested operations
AsyncLocalStorage: This Node.js feature maintains the transaction context across async calls without explicitly passing it through function parameters.
API Reference
Decorators
@Transaction(connectionName?: string)
Method decorator that wraps the method in a MongoDB transaction.
Parameters:
connectionName(optional): Name of the Mongoose connection to use
Functions
createTransactionConnectionFactory()
Creates a connection factory that registers the transaction plugin.
registerTransactionPlugin(connection: Connection)
Registers the transaction plugin on a Mongoose connection.
getCurrentSession(): ClientSession | undefined
Gets the current transaction session from AsyncLocalStorage.
runWithSession<T>(session: ClientSession, callback: () => T | Promise<T>): T | Promise<T>
Runs a callback with a given session in AsyncLocalStorage.
Important Notes
⚠️ MongoDB Transactions Requirement: MongoDB transactions only work with replica sets or sharded clusters. For local development, you can set up a single-node replica set:
# Start MongoDB with replica set
mongod --replSet rs0
# In MongoDB shell
rs.initiate()