The main problem I had when finding a suitable solution was, that the prisma client in the lambda of an interactive transaction is not a fully fledged Client, but just Prisma.TransactionClient, which is missing connect, $disconnect, transaction methods. If prisma would provide a full Client at this place, all you could do to solve the problem was just doing transactions like this:

**THIS DOES NOT WORK BECAUSE prismaServiceBoundToTransaction IS JUST OF TYPE Prisma.TransactionClient!!!**
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
      const userService = new UserService(prismaServiceBoundToTransaction)
      const otherService = new OtherService(prismaServiceBoundToTransaction)
      
      //Following calls will use prismaServiceBoundToTransaction internally
      await userService.update()
      await otherService.delete()
    }

Of course above only works, if UserService and OtherService are stateless.

So for my solution I created a new Interface that will offer all methods of Prisma.TransactionClient, but also a custom method to create a transaction. All of the services like your UserService will only retrieve this exact interface, so they can't call $transaction but only my interactiveTransaction method!

export interface PrismaClientWithCustomTransaction
  extends Readonly<Prisma.TransactionClient> {
  interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F>;
}

We then create a concrete class TransactionalPrismaClient that implements the mentioned interface and delivers it, by retrieving a Prisma.TransactionClient in it's constructor and forwarding all of its methods. Additionally we also just implement the interactiveTransaction method by executing the lambda method with the Prisma.TransactionClient

export class TransactionalPrismaClient<
  T extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
  U = 'log' extends keyof T
    ? T['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
      ? Prisma.GetEvents<T['log']>
      : never
    : never,
  GlobalReject extends
    | Prisma.RejectOnNotFound
    | Prisma.RejectPerOperation
    | false
    | undefined = 'rejectOnNotFound' extends keyof T
    ? T['rejectOnNotFound']
    : false
> implements PrismaClientWithCustomTransaction
{
  constructor(private readonly transactionalClient: Prisma.TransactionClient) {}

  $executeRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRaw(query, ...values);
  }
  $executeRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRawUnsafe(query, ...values);
  }
  $queryRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRaw(query, ...values);
  }
  $queryRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRawUnsafe(query, ...values);
  }
  get otherEntity(): Prisma.OtherEntityDelegate<GlobalReject> {
    return this.transactionalClient.otherEntity;
  }
  get userEntity(): Prisma.UserEntityDelegate<GlobalReject> {
    return this.transactionalClient.userEntity;
  }

  async interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F> {
    return await fn(this.transactionalClient);
  }
}

And in your PrismaService we also need to implement the interactiveTransaction method, so that it satifies our defined interface PrismaClientWithCustomTransaction.

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, PrismaClientWithCustomTransaction
{
  private readonly logger = new ConsoleLogger(PrismaService.name);

  async onModuleInit() {
    this.logger.log('Trying to connect to db.');
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }

  interactiveTransaction<R>(
    fn: (prisma: Prisma.TransactionClient) => Promise<R>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    },
    numRetries = 1
  ): Promise<R> {
    let result: Promise<R> | null = null;

    for (let i = 0; i < numRetries; i++) {
      try {
        result = this.$transaction(fn, options);
      } catch (e) {
        if (e instanceof Prisma.PrismaClientKnownRequestError) {
          //TODO?
        } else {
          throw e;
        }
      }

      if (result != null) {
        return result;
      }
    }

    throw new Error(
      'No result in transaction after maximum number of retries.'
    );
  }
}

Because in our services we now expect the PrismaClientWithCustomTransaction interface, the auto injecting of NestJs wont work anymore and we have to provide PrismaService using a token:

providers: [
    {
      provide: 'PRISMA_SERVICE_TOKEN',
      useClass: PrismaService,
    },
  ],
exportt class UserService{
    constructor(@Inject('PRISMA_SERVICE_TOKEN') private readonly prisma: PrismaClientWithCustomTransaction){}
}

Alright so now we can do the following:

@Injectable()
export class OrdersService {
  constructor( @Inject('PRISMA_SERVICE_TOKEN')
    private readonly prisma: PrismaClientWithCustomTransaction, ...) {}

  async someFn() {
    return await this.prisma.interactiveTransaction(
      async (client) => {
        //You can still use client directly, if you dont need nested transaction logic
        return client.userEntity.create(...)

 
        //Or create services for nested usage
        const transactionalClient = new TransactionalPrismaClient(client);
        const userService = new UserService(transactionalClient);
        return userService.createUser(...);
        });
      },
      { isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead }
    );
  }
}

And if you need the connect, $disconnect, $use, you can of course still inject the original PrismaService with its regular interface.

Answer from Peter Herbst on Stack Overflow
🌐
Medium
medium.com › @jedirichang › nestjs-prisma-transaction-with-repositories-2803e5a5487a
NestJS: Prisma Transaction with Repositories | by Richang Sharma | Medium
September 6, 2024 - Now Prisma on its own does provide transaction support, but doing transactions inside a request where there are multiple service methods to be executed in conjunction as a transaction is not at all suitable in Prisma for some reason.
Top answer
1 of 1
1

The main problem I had when finding a suitable solution was, that the prisma client in the lambda of an interactive transaction is not a fully fledged Client, but just Prisma.TransactionClient, which is missing connect, $disconnect, transaction methods. If prisma would provide a full Client at this place, all you could do to solve the problem was just doing transactions like this:

**THIS DOES NOT WORK BECAUSE prismaServiceBoundToTransaction IS JUST OF TYPE Prisma.TransactionClient!!!**
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
      const userService = new UserService(prismaServiceBoundToTransaction)
      const otherService = new OtherService(prismaServiceBoundToTransaction)
      
      //Following calls will use prismaServiceBoundToTransaction internally
      await userService.update()
      await otherService.delete()
    }

Of course above only works, if UserService and OtherService are stateless.

So for my solution I created a new Interface that will offer all methods of Prisma.TransactionClient, but also a custom method to create a transaction. All of the services like your UserService will only retrieve this exact interface, so they can't call $transaction but only my interactiveTransaction method!

export interface PrismaClientWithCustomTransaction
  extends Readonly<Prisma.TransactionClient> {
  interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F>;
}

We then create a concrete class TransactionalPrismaClient that implements the mentioned interface and delivers it, by retrieving a Prisma.TransactionClient in it's constructor and forwarding all of its methods. Additionally we also just implement the interactiveTransaction method by executing the lambda method with the Prisma.TransactionClient

export class TransactionalPrismaClient<
  T extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
  U = 'log' extends keyof T
    ? T['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
      ? Prisma.GetEvents<T['log']>
      : never
    : never,
  GlobalReject extends
    | Prisma.RejectOnNotFound
    | Prisma.RejectPerOperation
    | false
    | undefined = 'rejectOnNotFound' extends keyof T
    ? T['rejectOnNotFound']
    : false
> implements PrismaClientWithCustomTransaction
{
  constructor(private readonly transactionalClient: Prisma.TransactionClient) {}

  $executeRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRaw(query, ...values);
  }
  $executeRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<number> {
    return this.transactionalClient.$executeRawUnsafe(query, ...values);
  }
  $queryRaw<T = unknown>(
    query: TemplateStringsArray | Prisma.Sql,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRaw(query, ...values);
  }
  $queryRawUnsafe<T = unknown>(
    query: string,
    ...values: any[]
  ): PrismaPromise<T> {
    return this.transactionalClient.$queryRawUnsafe(query, ...values);
  }
  get otherEntity(): Prisma.OtherEntityDelegate<GlobalReject> {
    return this.transactionalClient.otherEntity;
  }
  get userEntity(): Prisma.UserEntityDelegate<GlobalReject> {
    return this.transactionalClient.userEntity;
  }

  async interactiveTransaction<F>(
    fn: (prisma: Prisma.TransactionClient) => Promise<F>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    }
  ): Promise<F> {
    return await fn(this.transactionalClient);
  }
}

And in your PrismaService we also need to implement the interactiveTransaction method, so that it satifies our defined interface PrismaClientWithCustomTransaction.

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, PrismaClientWithCustomTransaction
{
  private readonly logger = new ConsoleLogger(PrismaService.name);

  async onModuleInit() {
    this.logger.log('Trying to connect to db.');
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }

  interactiveTransaction<R>(
    fn: (prisma: Prisma.TransactionClient) => Promise<R>,
    options?: {
      maxWait?: number | undefined;
      timeout?: number | undefined;
      isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
    },
    numRetries = 1
  ): Promise<R> {
    let result: Promise<R> | null = null;

    for (let i = 0; i < numRetries; i++) {
      try {
        result = this.$transaction(fn, options);
      } catch (e) {
        if (e instanceof Prisma.PrismaClientKnownRequestError) {
          //TODO?
        } else {
          throw e;
        }
      }

      if (result != null) {
        return result;
      }
    }

    throw new Error(
      'No result in transaction after maximum number of retries.'
    );
  }
}

Because in our services we now expect the PrismaClientWithCustomTransaction interface, the auto injecting of NestJs wont work anymore and we have to provide PrismaService using a token:

providers: [
    {
      provide: 'PRISMA_SERVICE_TOKEN',
      useClass: PrismaService,
    },
  ],
exportt class UserService{
    constructor(@Inject('PRISMA_SERVICE_TOKEN') private readonly prisma: PrismaClientWithCustomTransaction){}
}

Alright so now we can do the following:

@Injectable()
export class OrdersService {
  constructor( @Inject('PRISMA_SERVICE_TOKEN')
    private readonly prisma: PrismaClientWithCustomTransaction, ...) {}

  async someFn() {
    return await this.prisma.interactiveTransaction(
      async (client) => {
        //You can still use client directly, if you dont need nested transaction logic
        return client.userEntity.create(...)

 
        //Or create services for nested usage
        const transactionalClient = new TransactionalPrismaClient(client);
        const userService = new UserService(transactionalClient);
        return userService.createUser(...);
        });
      },
      { isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead }
    );
  }
}

And if you need the connect, $disconnect, $use, you can of course still inject the original PrismaService with its regular interface.

🌐
Wanago
wanago.io › home › api with nestjs #104. writing transactions with prisma
API with NestJS #104. Writing transactions with Prisma
April 17, 2023 - In this article, we’ve discussed the idea of transactions and how to use them with Prisma. When doing that, we’ve compared various solutions, such as nested writes, bulk operations, and the transactions API. We’ve also used both the sequential operations approach and the interactive transactions. All of the above equips us with solutions for many different use cases we might encounter in our applications. Series Navigation<< API with NestJS #103.
🌐
GitHub
github.com › takeny1998 › nestjs-prisma-transactional
GitHub - takeny1998/nestjs-prisma-transactional: Manage PrismaORM transactions easily by adding decorator to your nest.js services. · GitHub
TransactionModule, ], controllers: [...], providers: [ { provide: 'PrismaService', useValue: new PrismaService().$extends(txExtension), }, ], exports: ['PrismaService'], }) export class AppModule {} Otherwise, a good alternative is to use the CustomPrismaModule from the nestjs-prisma package.
Author   takeny1998
🌐
Prisma
prisma.io › nestjs
Enterprise-ready database for NestJS apps - Prisma ORM
Prisma's transaction API integrates smoothly with NestJS's asynchronous architecture, allowing you to perform multiple database operations as a single atomic unit while following NestJS's best practices.
🌐
DEV Community
dev.to › callgent › nestjs-prisma-transaction-propagation-test-rollback-multi-tenancy-4485
Nestjs + Prisma, transaction propagation & test rollback & multi-tenancy - DEV Community
June 23, 2024 - After some investigation, we come to @nestjs-cls/transactional, @Injectable() export class MyService { constructor( private readonly txHost: TransactionHost<TransactionalAdapterPrisma>, private readonly anotherService: AnotherService, ) {} @Transactional() async myMethod(){ const prisma = this.txHost.tx as PrismaClient; // db operations1 await this.anotherService.anotherMethod(); // db operations2 // db operations3 } }
🌐
Medium
medium.com › @yisarasaq2018 › database-transaction-in-prisma-with-nestjs-and-postgres-a4bb4bbb6426
DATABASE TRANSACTION IN PRISMA WITH NESTJS AND POSTGRES | by Yisa Rasaq | Medium
January 14, 2025 - Using Prisma’s $transaction method in a NestJS application allows you to group multiple database operations into a single transaction.
Find elsewhere
🌐
Callgent
docs.callgent.com › blog › nestjs+prisma-transaction-propagation-and-test-rollback-and-tenancy
🔧 Nestjs + Prisma, transaction propagation & test rollback & multi-tenancy - Service as a Callgent
March 16, 2024 - After some investigation, we come to @nestjs-cls/transactional, @Injectable() export class MyService { constructor( private readonly txHost: TransactionHost<TransactionalAdapterPrisma>, private readonly anotherService: AnotherService, ) {} @Transactional() async myMethod(){ const prisma = this.txHost.tx as PrismaClient; // db operations1 await this.anotherService.anotherMethod(); // db operations2 // db operations3 } }
🌐
Papooch
papooch.github.io › plugins › available plugins › @nestjs-cls/transactional
@nestjs-cls/transactional | NestJS CLS
The plugin works in conjunction with various adapters that provide the actual transactional logic and types for the underlying database library, so you'll need to install one of those as well. Adapters for the following libraries are available: Prisma (see @nestjs-cls/transactional-adapter-prisma)
🌐
Medium
masoudx.medium.com › stop-passing-the-hot-potato-managing-prisma-transactions-in-nestjs-8f30eeb5bb54
Stop Passing the Hot Potato: Managing Prisma Transactions in NestJS | by Masoud Banimahd | Medium
November 25, 2025 - A transaction ensures that these three steps are treated as a single atomic unit. Either they all happen, or none of them happen. If step 3 fails, steps 1 and 2 roll back as if they never touched the database. In NestJS with Prisma, handling these transactions is straightforward when everything happens in one function.
🌐
npm
npmjs.com › package › @takeny1998 › nestjs-prisma-transactional
@takeny1998/nestjs-prisma-transactional - npm
January 29, 2024 - TransactionModule, ], controllers: [...], providers: [ { provide: 'PrismaService', useValue: new PrismaService().$extends(txExtension), }, ], exports: ['PrismaService'], }) export class AppModule {} Otherwise, a good alternative is to use the CustomPrismaModule from the nestjs-prisma package.
      » npm install @takeny1998/nestjs-prisma-transactional
    
Published   Jan 29, 2024
Version   1.0.5
Author   heebeom-yang
🌐
npm
npmjs.com › package › @nestjs-cls › transactional-adapter-prisma
nestjs-cls/transactional-adapter-prisma
Prisma adapter for the @nestjs-cls/transactional plugin.
      » npm install @nestjs-cls/transactional-adapter-prisma
    
Published   Dec 15, 2025
Version   1.3.2
🌐
Papooch
papooch.github.io › plugins › available plugins › @nestjs-cls/transactional › prisma adapter
Prisma adapter | NestJS CLS - Ing. Ondřej Švanda
The tx property on the TransactionHost<TransactionalAdapterPrisma> refers to the transactional PrismaClient instance when used in a transactional context. It is the instance that is passed to the prisma.$transaction(( tx ) => { ...
🌐
Callgent's Blog
blog.callgent.com › nestjs-prisma-transaction-propagation-test-rollback-multi-tenancy
Nestjs + Prisma, transaction propagation & test rollback & multi-tenancy
June 27, 2024 - After some investigation, we come to @nestjs-cls/transactional, @Injectable() export class MyService { constructor( private readonly txHost: TransactionHost<TransactionalAdapterPrisma>, private readonly anotherService: AnotherService, ) {} @Transactional() async myMethod(){ const prisma = this.txHost.tx as PrismaClient; // db operations1 await this.anotherService.anotherMethod(); // db operations2 // db operations3 } }
🌐
GitHub
github.com › nicheeeer › prisma-transaction
GitHub - nicheeeer/prisma-transaction: prisma-transaction is a library that provides a simple way to use transactions in Prisma within NestJS using decorators · GitHub
prisma-transaction is a library that provides a simple way to use transactions in Prisma within NestJS using decorators - nicheeeer/prisma-transaction
Author   nicheeeer
🌐
npm
npmjs.com › package › @myfunc › prisma-transactional
@myfunc/prisma-transactional - npm
June 27, 2024 - Now you can use PrismaTransactional. In Example application described all possible decorator's use cases. For running example app, please edit DB connection string in the .env file. ... You can add decorator to any class-method. All queries inside will be wrapped in a single transaction.
      » npm install @myfunc/prisma-transactional
    
Published   Jun 27, 2024
Version   0.3.0
Author   myfunc
🌐
DEV Community
dev.to › kenfdev › cross-module-transaction-with-prisma-5d08
Cross Module Transaction with Prisma - DEV Community
June 14, 2022 - The point is that you call prisma.$transaction and you pass a callback to it with the parameter prisma. Inside the transaction, you use the prisma instance passed as the callback to use it as the transaction prisma client. It's simple and easy to use. But what if you don't want to show the prisma interface inside the transaction code?
🌐
GitHub
github.com › prisma › prisma › discussions › 23626
i am facing the same issue in nest js when a transaction fail it dosent roll back the changes in the database · prisma/prisma · Discussion #23626
March 26, 2024 - Can you also try a different Transaction Isolation Level and check if you get the same error? Beta Was this translation helpful? Give feedback. ... There was an error while loading. Please reload this page. Something went wrong. There was an error while loading. Please reload this page. ... `public removeAssistant = async (doctor_id: string): Promise<ServiceResponse<string | null>> => { const doctor = await prisma.doctor.findUnique({where:{id: doctor_id}}); if (!doctor?.assistant_id) { return new ServiceResponse(null, true, httpStatus.BAD_REQUEST, 'Assistant is not assigned to doctor'); }
Author   prisma
🌐
Reddit
reddit.com › r › node › comments › 12pvair › api_with_nestjs_104_writing_transactions_with
API with NestJS #104. Writing transactions with Prisma
Unofficial Node.js subreddit. Discuss Node.js, JavaScript, TypeScript, and anything else in the Node.js ecosystem!