Better Programming

Advice for programmers.

Follow publication

Handling Transactions in TypeORM and Nest.js With Ease

A convenient and simple way of working with database transactions in TypeORM and Nest.js

Dmitry Shamshurin
Better Programming
Published in
5 min readApr 6, 2022

--

Photo by panumas nikhomkhai

There are many cases when several pieces of data must be created/updated simultaneously as a single unit of logic (i.e. creating one entity doesn’t make sense and should not happen without updating another because they depend on each other). Databases such as PostgreSQL provide us with an appropriate tool for this case: transactions (such databases are called “transactional databases”).

According to the Wikipedia, Transactions essentially are multiple operations treated as a single unit of work with “all-or-nothing” effect. It begins, does some database changes, and then either is committed or, in case of any failure, is rolled back. This way database either has a new state with all of the changes applied, or is returned to its original state as if nothing happened.

Transactions in TypeORM

TypeORM supports database transactions and its documentation provides a pretty good explanation of how to use them:

As noted in the documentation, it is extremely important to use the provided instance of EntityManager, do not ever use the global manager, otherwise you will have errors and/or unpredictable results.

Drawbacks of using TypeORM directly

We see that TypeORM makes our life a lot easier since we don’t have to deal with SQL transactions themselves, but using it in the business code still has some drawbacks:

  1. If we follow the DataSource / EntityManager approach, we have to put all of our database operations into this one callback, however complex they are and however many of them you have.
  2. If we follow the QueryRunner approach, we have to write a lot of boilerplate code and make sure that we didn’t miss anything in it.
  3. In both cases it makes our code a bit harder to read, maintain, and reuse.

So what other options do we have (considering we’re using the Nest.js framework)?

  1. Use decorators to control the transaction (@Transaction() and @TransactionManager()), which is not recommended by the Nest.js docs.
  2. Include some 3rd party library like typeorm-transactional-cls-hooked (which is based on this cool “Continuation Local Storage” concept) or nest_transact.
  3. Build something relatively simple yourself that will suit our needs and will abstract away working with TypeORM transaction interfaces.

As you might have already guessed, I will cover the third option in this post.

Abstract Transaction Class

My friend (Anton Pavlov) and I started thinking on how we can hide all of the inner workings of transactions away from the business logic in our Nest.js application. We came up with an abstract transaction class that handles all of transactional stuff and provides us with a simple way to write transactions and reuse their code anywhere in the codebase:

As you can see, this class contains that boilerplate code for QueryRunner , so we don’t have to deal with it anymore while leaving everything related to actual operations to this class’ descendants. If we need to create a transaction, we inherit from this base class, and then just implement the execute function, putting all of our logic there.

The simplest usage example

Now let’s see how we can use this transaction class that we’ve created. Please bear in mind that this code serves only one purpose — to show how to work with transactions, and we don’t care about some of the implementation details (or proper variable names).

  1. First we create the CreateUserTransaction class which inherits from our BaseTransaction, providing typed arguments for input and output of the transaction.
  2. The transaction itself is pretty straightforward: we just create a User and then create Balance entity which is connected to our User using the manager that was created behind the scenes in the base class.
  3. Then, in our UserService we just inject the CreateUserTransaction and call the run method on it.
  4. The transaction will be commited only after the run method returns. We can be 100% sure that our user will be created only with the balance, and not in any other way. If there is any error, it will be caught and transaction will be rolled back, meaning nothing will be changed in the database.

A more advanced example

Let’s say, we have two scenarios: we can create a simple user with an empty balance, or a premium user with some bonus money.

Also, imagine that we have some sort of abstraction layer in our project that works with the database and handles all the details about entities.

For brevity, I will provide just one superficial example of such abstraction implementation, and the rest should be pretty straightforward and similar.

In this example:

  1. Our UserService starts the “big” main CreateUserTransaction to create a user.
  2. Inside of the main transaction we first call the runWithinTransaction method of our CreateBasicUserTransaction, passing the manager that was created by the outer transaction. This is very important because this manager will be that one piece that holds everything together.
  3. Then we call our database abstraction layer classes such as DbUserService, DbBalanceService that handle everything that we need (in relation to entities), be it a simple call to the TypeORM repository, several of such calls, some fetching and transforming data, or virtually anything that you see fit in this layer. Once again, it’s crucial to use that passed EntityManager for every database operation.
  4. After the CreateBasicUserTransaction is finished with user creation, our main transaction continues with checking if a user is premium, does some additional work if needed, commits the changes to the database (via BaseTransaction class), and returns the CreatedUserData for our UserService.
  5. As in the previous example, if anything goes wrong at any of the steps above, the transaction will be rolled back, and our database will be safe from any “partial”changes.

Conclusion

The described way of working with transactions using TypeORM and Nest.js allows us to split any connected business-related database logic into manageable and reusable parts, and we don’t have to care about the transaction handling itself.

The only downside of this approach is the necessity of “dragging” the transactional EntityManager everywhere along the methods that interact with our database.

We couldn’t find a simple way around it because we didn’t want to overcomplicate our code, e.g. by including the aforementioned “continuation local storage” concept, or inventing some smart containers for managers. If you think of one, please leave a comment to this article, and we will gladly have a look at any of your suggestions.

Additional reading and links from the article

  1. Transactions in Wikipedia
  2. TypeORM documentation on transactions
  3. Nest.js documentation on working with databases
  4. Nest.js transaction library built with continuation local storage concept
  5. Great article by Mikhail Alfa providing a different take on handling transactions with TypeORM and Nest.js and his library

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Dmitry Shamshurin
Dmitry Shamshurin

Written by Dmitry Shamshurin

An aspiring team lead TS/JS at Deel with some experience in Android (Kotlin). Learning TS, Go, trying to get better myself, and help others.

Responses (6)

Write a response