In many cases, developers must use transactions when performing various operations on the server. For example - a transfer of money, or other measurable value, and much more. With such operations, I really do not want to receive an error that will interrupt the process and violate the data integrity. What is a "transaction" anyway? Wikipedia : says "A symbolizes a unit of work performed within a database management system (or similar system) against a database, and treated in a coherent and reliable way independent of other transactions. A transaction generally represents any change in a database." database transaction Transactions in a database environment have two main purposes: To provide reliable units of work that allow correct recovery from failures and keep a database consistent even in cases of system failure, when execution stops (completely or partially) and many operations upon a database remain uncompleted, with unclear status. To provide isolation between programs accessing a database concurrently. If this isolation is not provided, the programs' outcomes are possibly erroneous. Now, consider a situation where an error can occur, leading to very unpleasant consequences if you do not use transactions. I made a small in which there are two entities: project User Purse Users can transfer money to each other. When transferring, the sufficiency of the amount on the balance of the sender is checked, as well as many other checks. If a situation occurs when the money has been debited from the sender's balance but not transferred to the recipient's account, or vice versa, we will see either a very sad, angry person, or we will not see a very happy one ( ). depends on the transfer amount Great, with the fact that transactions are important and need to be sorted out ( ). But how do you apply them? hopefully everyone agrees with this First, let's look at the options for queries with errors and without errors that will occur if you use PostgreSQL. The usual set of queries without errors: // ... . , . , . . ($ ) = $ ($ ) . , . , . . ($ ) = $ ($ ) SELECT "User" "id" AS "User_id" "User" "name" AS "User_name" "User" "defaultPurseId" AS "User_defaultPurseId" FROM "user" "User" WHERE "User" "id" IN 1 START TRANSACTION UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 START TRANSACTION UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT By the way - I did not write this request by hand, but pulled it out of the ORM logs, but it reflects the essence. Everything is pretty simple and straightforward. To build the queries, was used, which we will return to a little later. TypeORM The ORM and Postgres settings are set by default, so each operation will be performed in its own transaction, but to take advantage of this advantage, you need to write one query in which all the logic associated with the database will take place at once. Below is an example of the execution of multiple queries executed in one transaction: // ... . , . , . . ($ ) = $ ($ ) . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT The key difference with the previous example of requests is that in this case all requests are executed in one transaction, and therefore, if an error occurs at some stage, the entire transaction will be rolled back with all the requests inside it. More or less like this: // ... . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 ROLLBACK And here, by the way, is the code that produced all the previous SQL queries. It contains a flag, when set, an error occurs at the most inopportune moment: makeRemittance(fromId: , toId: , sum: , withError = , transaction = ): <RemittanceResultDto> { fromUser = .userRepository.findOne(fromId, { transaction }); toUser = .userRepository.findOne(toId, { transaction }); (fromUser === ) { (NOT_FOUND_USER_WITH_ID(fromId)); } (toUser === ) { (NOT_FOUND_USER_WITH_ID(toId)); } (fromUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(fromId)); } (toUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(toId)); } fromPurse = .purseRepository.findOne(fromUser.defaultPurseId, { transaction }); toPurse = .purseRepository.findOne(toUser.defaultPurseId, { transaction }); modalSum = .abs(sum); (fromPurse.balance < modalSum) { (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; .purseRepository.save(fromPurse, { transaction }); (withError) { ( ); } .purseRepository.save(toPurse, { transaction }); remittance = RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; remittance; } // ... async number number number false true Promise const await this const await this if undefined throw new Error if undefined throw new Error if null throw new Error if null throw new Error const await this const await this const Math if throw new Error await this if throw new Error 'Unexpectable error was thrown while remittance' await this const new return // ... Fine! We saved ourselves from losses or very upset users ( ). at least in matters related to money transfers Alternative Ways What's next? What other ways are there to write a transaction? It just so happened that the person whose article you are currently reading ( ) really loves one wonderful framework when he has to write a backend. this is me The name of this framework is . It works on the Node.js platform, and the code in it is written in Typescript. This great framework has support, almost out of the box, for the very TypeORM. Nest.js Which (or which?) I, as it happens, also really like. I didn't like only one thing - a rather confusing, as it seems to me, overly complicated approach to writing transactions. This is the official for writing transactions: example { getConnection } ; getConnection().transaction( transactionalEntityManager => { transactionalEntityManager.save(users); transactionalEntityManager.save(photos); }); import from 'typeorm' await async await await // ... Second way to create transactions from the documentation: () save(user: User, () transactionManager: EntityManager) { transactionManager.save(User, user); } @Transaction @TransactionManager return In general, the point of this approach is as follows: you need to get a - an entity that will allow you to execute queries within a transaction. And then use this entity for all actions with the base. Sounds good, as long as you don't have to deal with using this approach in practice. transactionEntityManager: EntityManager To begin with, I don't really like the idea of injecting dependencies directly into the methods of service classes, as well as the fact that the methods are written in this way become isolated in terms of using the dependencies injected into the service itself. All the dependencies necessary for the method to work will have to be dropped into it. But the most annoying thing is that if your method calls other services embedded in yours, then you have to create the same special methods in those third-party services. And pass in them. transactionEntityManager At the same time, it should be borne in mind that if you decide to use the approach through decorators, then when you transfer the from one service to the second, and the method of the second service will also be decorated - in the second method you will receive the that is not passed as a dependency, and the one that is created by the decorator, which means two different transactions, which means unfortunate users. transactionEntityManager transactionEntityManager Start From Examples Below is the code for a controller action that handles user requests: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransaction( () remittanceDto: RemittanceDto) { .connection.transaction( { .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } // ... @Post 'remittance-with-typeorm-transaction' @ApiResponse type async @Body return await this => transactionManager return this // ... In it, we need to have access to the object to create a . We could do as the TypeORM documentation advises - and just use the function as shown above: connection transactionManager getConnection { getConnection } ; ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransaction( () remittanceDto: RemittanceDto) { getConnection().transaction( { .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } import from 'typeorm' // ... @Post 'remittance-with-typeorm-transaction' @ApiResponse type async @Body return await => transactionManager return this // ... But it seems to me that such code will be more difficult to test, and this is simply wrong ( ). Therefore, we will have to pass the dependency into the controller constructor. It's very lucky that Nest allows you to do this by simply describing the field in the constructor with the appropriate type: great argument connection () ( ) AppController { ( ) { } } @Controller @ApiTags 'app' export class constructor readonly appService: AppService, readonly connection: Connection, private private // <-- it is - what we need // ... Thus, we come to the conclusion that in order to be able to use transactions in Nest when using TypeORM, it is necessary to pass the class into the controller / service constructor, for now we just remember this. connection Now let's look at the method of our : makeRemittanceWithTypeOrmV1 appService makeRemittanceWithTypeOrmV1(transactionEntityManager: EntityManager, fromId: , toId: , sum: , withError = ) { fromUser = transactionEntityManager.findOne(User, fromId); toUser = transactionEntityManager.findOne(User, toId); (fromUser === ) { (NOT_FOUND_USER_WITH_ID(fromId)); } (toUser === ) { (NOT_FOUND_USER_WITH_ID(toId)); } (fromUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(fromId)); } (toUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(toId)); } fromPurse = transactionEntityManager.findOne(Purse, fromUser.defaultPurseId); toPurse = transactionEntityManager.findOne(Purse, toUser.defaultPurseId); modalSum = .abs(sum); (fromPurse.balance < modalSum) { (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; .appServiceV2.savePurse(fromPurse); (withError) { ( ); } transactionEntityManager.save(toPurse); remittance = RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; remittance; } async number number number false const await // <-- we need to use only provided transactionEntityManager, for make all requests in transaction const await // <-- and there if undefined throw new Error if undefined throw new Error if null throw new Error if null throw new Error const await // <-- there const await // <--there const Math if throw new Error await this // <-- oops, something was wrong if throw new Error 'Unexpectable error was thrown while remittance' await const new return The whole project is synthetic, but to show the unpleasantness of this approach - I moved the method used to save the wallet into a separate , and used this service with this method inside the considered method. You can see the code of this method and service below: savePurse appServiceV2 service makeRemittanceWithTypeOrmV1 () AppServiceV2 { ( ) { } savePurse(purse: Purse) { .purseRepository.save(purse); } } @Injectable export class constructor (Purse) readonly purseRepository: Repository<Purse>, @InjectRepository private async await this // ... Actually, in this situation, we get the following SQL queries: // ... . , . , . . ($ ) // < = $ ($ ) . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "User" "id" AS "User_id" "User" "name" AS "User_name" "User" "defaultPurseId" AS "User_defaultPurseId" FROM "user" "User" WHERE "User" "id" IN 1 START TRANSACTION -- this transaction from appServiceV2 😩 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT If we send a request for an error to occur, we will clearly see that the internal transaction from is not rolled back, and therefore our users are indignant again. appServiceV2 // ... . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 START TRANSACTION UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT ROLLBACK Here we conclude that for a standard approach to trunking, you need to have special methods into which you will need to pass . transactionEntityManager If we want to get rid of the need to explicitly inject the into the corresponding methods, then the documentation advises us to look at decorators. transactionEntityManager By applying them, we get this kind of controller action: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransactionDecorators( () remittanceDto: RemittanceDto) { .appService.makeRemittanceWithTypeOrmV2(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); } // ... @Post 'remittance-with-typeorm-transaction-decorators' @ApiResponse type async @Body return this // ... Now it has become simpler - there is no need to use the class, neither in the constructor, nor by calling the global method TypeORM. Perfectly. But the method of our service should still receive a dependency - . This is where those decorators come to the rescue: connection transactionEntityManager () makeRemittanceWithTypeOrmV2(fromId: , toId: , sum: , withError: , () transactionEntityManager: EntityManager = ) { fromUser = transactionEntityManager.findOne(User, fromId); toUser = transactionEntityManager.findOne(User, toId); (fromUser === ) { (NOT_FOUND_USER_WITH_ID(fromId)); } (toUser === ) { (NOT_FOUND_USER_WITH_ID(toId)); } (fromUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(fromId)); } (toUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(toId)); } fromPurse = transactionEntityManager.findOne(Purse, fromUser.defaultPurseId); toPurse = transactionEntityManager.findOne(Purse, toUser.defaultPurseId); modalSum = .abs(sum); (fromPurse.balance < modalSum) { (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; .appServiceV2.savePurseInTransaction(fromPurse, transactionEntityManager); (withError) { ( ); } transactionEntityManager.save(toPurse); remittance = RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; remittance; } // ... @Transaction // <-- this async number number number boolean @TransactionManager null /* <-- and this */ const await const await if undefined throw new Error if undefined throw new Error if null throw new Error if null throw new Error const await const await const Math if throw new Error await this // <-- we will check is it will working if throw new Error 'Unexpectable error was thrown while remittance' await const new return // ... We have already figured out the fact that simply using a third-party service method breaks our transactions. Therefore, we used the new method of the third-party service , which looks like this: transactionEntityManager () savePurseInTransaction(purse: Purse, () transactionManager: EntityManager = ) { transactionManager.save(Purse, purse); } // .. @Transaction async @TransactionManager null await // ... As you can see from the code, in this method we also used decorators - this way we achieve uniformity across all methods in the project ( ), and also get rid of the need to use in the constructor of controllers using our service . yep yep connection appServiceV2 With this approach, we get the following requests: // ... . , . , . . ($ ) . , . , . . ($ ) = $ ($ ) . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT And, as a consequence, the destruction of the transaction and application logic on error: // ... . , . , . . ($ ) . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT ROLLBACK The only working way, which the documentation describes, is to avoid using decorators. If you use decorators in all methods at once, then those of them that will be used by other services will inject their own , as happened with our service and its method. Let's try to replace this method with another: transactionEntityManagers appServiceV2 savePurseInTransaction () makeRemittanceWithTypeOrmV2(fromId: , toId: , sum: , withError: , () transactionEntityManager: EntityManager = ) { .appServiceV2.savePurseInTransactionV2(fromPurse, transactionEntityManager); } savePurseInTransactionV2(purse: Purse, transactionManager: EntityManager) { transactionManager.save(Purse, purse); } // app.service.ts @Transaction async number number number boolean @TransactionManager null // ... await this // ... // app.service-v2.ts // .. async await // .. For the consistency of our methods, and getting rid of the hierarchy that has appeared, which is manifested in the fact that some methods can call others, but still others will not be able to call the first - we will change the method of the class. Thus, having received the first option from the documentation. appService A Different Way Well, it seems we still have to inject this into the controller constructors. But the proposed way of writing code with transactions still looks very cumbersome and inconvenient. connection What to do? Solving this problem, I made a package that allows you to use transactions in the simplest way. It is called . nest-transact What is he doing? Everything is simple here. For our example with users and money transfers, let's look at the same logic written with nest-transact. The code of our controller has not changed, and since we have made sure that we cannot do without in the constructor, we will specify it: connection () ( ) AppController { ( ) { } } @Controller @ApiTags 'app' export class constructor readonly appService: AppService, readonly connection: Connection, private private // <-- use this // ... Controller's action: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTransaction( () remittanceDto: RemittanceDto) { .connection.transaction( { .appService.withTransaction(transactionManager) .makeRemittance(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } // ... @Post 'remittance-with-transaction' @ApiResponse type async @Body return await this => transactionManager return this /* <-- this is interesting new thing*/ // ... Its difference from the action, in the case of using the first method from the documentation: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransaction( () remittanceDto: RemittanceDto) { .connection.transaction( { .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } @Post 'remittance-with-typeorm-transaction' @ApiResponse type async @Body return await this => transactionManager return this Is that we can use the usual methods of services without creating specific variations for transactions in which it is necessary to pass . And also - that before using our service business method, we call the method on the same service, passing our to it. transactionManager withTransaction transactionManager Here you can ask the question - where did this method come from? Hence: () AppService TransactionFor<AppService> { ( ) { (moduleRef); } } @Injectable export class extends /* <-- step 1 */ constructor (User) readonly userRepository: Repository<User>, (Purse) readonly purseRepository: Repository<Purse>, readonly appServiceV2: AppServiceV2, moduleRef: ModuleRef, @InjectRepository private @InjectRepository private private // <-- step 2 super // ... And here is the request code: // ... . , . , . . ($ ) = $ ($ ) . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT And with the error: // ... . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 ROLLBACK But you already saw it at the very beginning. To make this magic work, you need to complete two steps: Our service must inherit from the class TransactionFor <ServiceType> Our service must have a special class in the list of constructor dependencies moduleRef: ModuleRef It's all. By the way, since dependency injection by the framework itself has not gone anywhere - you don't have to explicitly throw . moduleRef For testing only. You might be thinking - If you thought, then I suggest calculating how many of your services are inherited from other classes and are used in transactions. Why should I inherit from this class? What if my service will have to inherit from some other one? Now how does it work? The appeared method - recreates your service for this transaction, as well as all the dependencies of your service and the dependencies of dependencies - everything, everything, everything. It follows that if you somehow store some state in your services ( ) - then it will not be there when creating a transaction in this way. The original instance of your service still exists and when you call it, everything will be as before. withTransaction but what if? In addition to the previous example, I also added a greedy method: transfer with commission, which uses two services at once in one controller action: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTransactionAndFee( () remittanceDto: RemittanceDto) { .connection.transaction( manager => { transactionAppService = .appService.withTransaction(manager); result = transactionAppService.makeRemittance(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); result.fromBalance -= ; senderPurse = transactionAppService.getPurse(remittanceDto.userIdFrom); senderPurse.balance -= ; .appServiceV2.withTransaction(manager).savePurse(senderPurse); result; }); } // ... @Post 'remittance-with-transaction-and-fee' @ApiResponse type async @Body return this async const this // <-- this is interesting new thing const await 1 // <-- transfer fee const await 1 // <-- transfer fee, for example of using several services in one transaction in controller await this return // ... This method makes the following requests: // ... . , . , . . ($ ) = $ ($ ) . , . , . . ($ ) = $ ($ ) // this requests fee: . , . , . . = $ . , . , . . ($ ) = $ ($ ) START TRANSACTION SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 is new for SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "userId" 1 LIMIT 1 SELECT "Purse" "id" AS "Purse_id" "Purse" "balance" AS "Purse_balance" "Purse" "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" "id" IN 1 UPDATE "purse" SET "balance" 2 WHERE "id" IN 1 COMMIT From which we can see that all requests still occur in one transaction and it will work correctly. Summing up, I would like to say - when using this package in several real projects, I got a much more convenient way of writing transactions, of course, within the Nest.js + TypeORM stack. I hope you find it useful too. If you like this package and decide to give it a try, little wish - give it an asterisk on . It's not difficult for you, but it's useful for me and this package. I will also be glad to hear constructive criticism and possible ways to improve this solution. GitHub Previously published here .