npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nest-als-transaction

v1.0.3

Published

NestJS Transaction Helper using AsyncLocalStorage

Readme

NestJS ALS Transaction Helper

Note: This library is designed specifically for TypeORM and manages QueryRunner contexts.

A NestJS library for managing database transactions transparently using Node.js AsyncLocalStorage. This allows you to perform nested transactions and manage QueryRunner contexts without passing them as arguments throughout your service methods.

The Challenge

In complex NestJS applications, managing database transactions across multiple service layers often leads to three major problems:

1. Data Inconsistency (No Transaction)

By default, TypeORM operations run in their own auto-commit transactions. If a business process involves multiple steps and the last step fails, the previous steps are not rolled back, leaving the database in an inconsistent state.

Example

async createOrder(data) {
  // 1. Order created successfully
  const order = await this.orderRepo.save(data);

  // 2. Inventory deduction FAILS (e.g., db connection lost)
  // CRITICAL: The order persists, but stock was not deducted!
  await this.inventoryService.deductStock(data.items); 
}

2. Complex Manual Management

To ensure data consistency, developers often manage transactions manually. This requires writing repetitive code to handle connections, start transactions, commit changes, and handle errors (rollback) in every single service method.

Example: Repetitive Transaction Code

async createOrder(data: CreateOrderDto) {
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    const order = await queryRunner.manager.save(Order, data);
    // ... more operations
    await queryRunner.commitTransaction();
  } catch (err) {
    await queryRunner.rollbackTransaction();
    throw err;
  } finally {
    await queryRunner.release(); // Easy to forget!
  }
}

3. Service Integration Complexity

When multiple services work together (e.g., OrderService calls InventoryService), they must share the same transaction to be atomic. This forces you to pass the QueryRunner object as a parameter to every method in the chain.

Example: Burden of Passing Parameters

// inventory.service.ts
// You must add 'queryRunner' to every method signature
async deductStock(items: Item[], queryRunner?: QueryRunner) {
    // You must check if a transaction exists
    const manager = queryRunner ? queryRunner.manager : this.defaultManager;
    
    // If you call another internal method, you must pass it again
    await this.updateLog(items, queryRunner);
    
    return manager.save(Inventory, ...);
}

Repository Method Updates: If you use a Base Repository, you must update every method (save, update, delete) to accept an optional QueryRunner, making your code verbose and harder to maintain.

The Solution: Async Local Storage (ALS)

This library uses Node.js AsyncLocalStorage to store the transaction context implicitly during the request lifecycle.

Key Benefits

  1. Automatic Context Propagation: The active QueryRunner is stored globally for the request. You don't need to pass it down the stack.
  2. Intelligent Nesting: If you call executeInTransaction within an existing transaction, it automatically creates a SAVEPOINT instead of opening a new connection. This prevents deadlocks and connection pool exhaustion.
  3. Clean Architecture: It enables "Zero Prop-Drilling" where your services and repositories don't just "know" about the transaction context without explicit parameters.

Integration Guide

To fully leverage this library and remove QueryRunner parameters from your code, integrate TransactionHelper into your Base Repository.

1. Setup Base Repository

Inject TransactionHelper and use it to retrieve the active QueryRunner.

// src/cores/base/base.repository.ts
import { Injectable } from '@nestjs/common';
import { Repository, DeepPartial, UpdateResult } from 'typeorm';
import { TransactionHelper } from 'nest-als-transaction';

@Injectable()
export class BaseRepository<T> {
  constructor(
    private readonly baseRepo: Repository<T>,
    private readonly txHelper: TransactionHelper
  ) {}

  /**
   * Retrieves the active EntityManager from ALS (if in a transaction)
   * or falls back to the default managers.
   */
  protected getManager() {
    const queryRunner = this.txHelper.getQueryRunner();
    return queryRunner ? queryRunner.manager : this.baseRepo.manager;
  }

  async save(entity: DeepPartial<T>): Promise<T> {
    return this.getManager().save(this.baseRepo.target, entity);
  }

  async update(criteria: any, partialEntity: DeepPartial<T>): Promise<UpdateResult> {
    const manager = this.getManager();
    // Use QueryBuilder or manager.update depending on preference
    return manager.getRepository(this.baseRepo.target).update(criteria, partialEntity);
  }
  
  // Implement other methods (delete, find, etc.) similarly...
}

2. Usage in Services

Now your services can focus purely on business logic.

// src/services/order.service.ts
@Injectable()
export class OrderService {
  constructor(
    private readonly txHelper: TransactionHelper,
    private readonly orderRepo: OrderRepository, // extends BaseRepository
    private readonly inventoryService: InventoryService
  ) {}

  async createOrder(data: CreateOrderDto) {
    // Start a transaction scope
    return this.txHelper.executeInTransaction(async () => {
      
      // 1. Create Order
      // The repo automatically picks up the transaction from ALS!
      const order = await this.orderRepo.save(data);

      // 2. Call other service
      // No need to pass 'queryRunner' manually.
      // The InventoryService internally uses repositories that also pick up the context.
      await this.inventoryService.deductStock(data.items);
      
      return order;
    });
  }
}

Installation

npm install nest-als-transaction

Setup

Import TransactionModule in your AppModule. Note that this module is @Global().

Requirement: You must have TypeOrmModule configured and a DataSource available for injection.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransactionModule } from 'nest-als-transaction';

@Module({
  imports: [
    TypeOrmModule.forRoot({ ... }),
    TransactionModule,
  ],
})
export class AppModule {}

Usage Options

Option A: Fully Integrated (Recommended)

Use the Base Repository integration shown above. This keeps your code cleanest.

Option B: Manual Access

If you don't want to modify your Base Repository, you can still access the QueryRunner manually inside the transaction block:

this.txHelper.executeInTransaction(async (qr) => {
    // Pass 'qr' manually to legacy code that requires it
    await this.legacyService.doSomething(qr);
});

License

MIT


Feel free to comment or clone this repo. If you find any issues, please let me know!