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

@nestjs-transactional/core

v2.0.0

Published

Declarative transaction management for NestJS — core primitives (AsyncLocalStorage context, TransactionManager, @Transactional decorator, adapter port)

Readme

@nestjs-transactional/core

npm version License: MIT

Declarative transactions for NestJS, with Spring's semantics.

Put @Transactional() on a method and everything it touches runs in one transaction — across await boundaries, without threading a manager through your call stack. All seven Spring propagation modes are implemented, including NESTED via savepoints.

This package is ORM-agnostic and does nothing on its own: it needs an adapter. Most applications install @nestjs-transactional/typeorm alongside it, which also makes injected repositories transaction-aware automatically. For event delivery that survives a crash, add @nestjs-transactional/outbox; for @nestjs/cqrs handlers, @nestjs-transactional/cqrs.

Install

pnpm add @nestjs-transactional/core @nestjs-transactional/typeorm reflect-metadata

Load reflect-metadata once at your entry point, as NestJS itself requires.

Module format

This package ships ESM only, matching NestJS 12, which is ESM-only across its own packages. There is no CommonJS build.

A CommonJS application still works: Node loads ESM from require() since 22.12.0, which is why engines.node is >=22.13.0. What does not follow Node here is tooling with its own module loader — Jest above all, which needs NODE_OPTIONS=--experimental-vm-modules and a few config settings. The 19 example applications in the repository all run their suites that way and can be copied from.

Reasoning and measurements: ADR-022.

Quick start

import { Module } from '@nestjs/common';
import { TransactionalModule } from '@nestjs-transactional/core';
import { TypeOrmTransactionalModule } from '@nestjs-transactional/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      /* ... */
    }),

    // Infrastructure only: TransactionManager, AdapterRegistry, the
    // interceptor. `isGlobal` matters — the adapter package below
    // needs to see the registry from its own DI scope.
    TransactionalModule.forRoot({ isGlobal: true }),

    // Registers the TypeORM adapter for the default dataSource.
    TypeOrmTransactionalModule.forRoot(),
  ],
})
export class AppModule {}

That is the whole setup. Now any method — a controller handler, a service method, a CQRS handler — becomes transactional by decoration:

import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-transactional/core';

@Injectable()
export class OrdersService {
  @Transactional()
  async placeOrder(dto: PlaceOrderDto): Promise<Order> {
    const order = await this.orders.save(dto);
    await this.stock.reserve(order); // same transaction
    return order; // commits here; a throw rolls both back
  }
}

Propagation

@Transactional({ propagation }) decides what happens when a transactional method is called from inside another one. The default, REQUIRED, joins the caller — which is what you want almost always.

| Mode | Caller has a transaction | Caller has none | | --- | --- | --- | | REQUIRED (default) | join it | start one | | REQUIRES_NEW | suspend it, run independently, resume | start one | | NESTED | run in a savepoint | start one | | SUPPORTS | join it | run without a transaction | | NOT_SUPPORTED | suspend it, run without one, resume | run without one | | NEVER | throw IllegalTransactionStateError | run without one | | MANDATORY | join it | throw IllegalTransactionStateError |

REQUIRES_NEW is how you make a side effect survive the caller's rollback — an audit row that must persist even when the operation fails. NESTED gives you a partial rollback inside one transaction; it needs a driver with savepoint support, and the TypeORM adapter raises a clear error rather than silently degrading if the driver has none.

Options

class ReportsService {
  @Transactional({
    propagation: PropagationMode.REQUIRES_NEW,
    isolation: 'SERIALIZABLE',
  })
  async rebuild() {}

  // Roll back on anything except ValidationError.
  @Transactional({ noRollbackFor: [ValidationError] })
  async processBatch() {}

  // Shorthand for { readOnly: true }.
  @ReadOnly()
  async exportCsv() {}

  // Target one dataSource in a multi-dataSource application.
  @TransactionalOn('billing')
  async chargeCard() {}
}

Two options carry caveats worth knowing before you rely on them:

  • readOnly is enforced by the database only on Postgres-family dialects, where the adapter issues SET TRANSACTION READ ONLY. Elsewhere it documents intent and nothing rejects a write. Spring treats it as a hint too. See DD-027.
  • timeout is accepted by the type but not implemented by the TypeORM adapter. It is deliberately not approximated: Postgres' statement_timeout bounds each statement rather than the transaction, so timeout: 5000 on a method issuing four queries would allow twenty seconds. It stays in the surface for adapters whose driver exposes a real transaction budget.

Commit and rollback hooks

Register from inside a transactional method; the hook binds to the transaction currently running.

@Transactional()
async placeOrder(dto: PlaceOrderDto) {
  const order = await this.orders.save(dto);

  // Runs only after the commit succeeds — never on rollback.
  this.manager.registerAfterCommit(() => this.analytics.track(order.id));

  // Receives the error that caused the rollback.
  this.manager.registerAfterRollback((error) => this.metrics.failed(error));

  return order;
}

A throwing hook is logged and swallowed: it changes neither the transaction's outcome nor its sibling hooks. For event handlers with these semantics as first-class decorators, see the cqrs package.

Testing

InMemoryTransactionAdapter from the /testing subpath records commits, rollbacks and savepoints without a database:

import { InMemoryTransactionAdapter } from '@nestjs-transactional/core/testing';

const adapter = new InMemoryTransactionAdapter();

await Test.createTestingModule({
  imports: [TransactionalModule.forRoot({ isGlobal: true, adapter })],
}).compile();

expect(adapter.committedTransactions).toHaveLength(1);
expect(adapter.rolledBackTransactions).toHaveLength(0);

adapter.reset() clears the arrays between cases. Pass a dataSource name to the constructor for multi-dataSource tests.

Custom adapters

To support another ORM, implement TransactionAdapter<THandle> and hand it to forRoot directly:

TransactionalModule.forRoot({ isGlobal: true, adapter: myAdapter });

One forRoot call registers one adapter. Multi-dataSource applications call it once per dataSource.

Documentation

License

MIT