Per-Request Database Transactions with NestJS and TypeORM

Intro

I started my web dev career way back in 2011 with a little bit of PHP and raw queries to MySQL. This landed me a job at Missouri State University’s residence life center: ResNet where I continued to do web dev with Python and Django. I learned about transactions from my databases class and I was aware of an atomic unit of work, but in practice I had never really used them. Fast forward to my job today where I’m currently working with Python, Flask, SQLAlchemy, and Postgres. Something that I wasn’t aware of before I started my current job was that it’s possible for a transaction to be created and used even though all you may want to do is query for data. As you may or may not know, SQLAlchemy creates a new transaction on each new Session instantiation and any query (or update, insert, etc.) issued will be a part of that session’s transaction. After using SQLAlchemy for a while, I’ve found myself enjoying that extra transactional atomicity.

The Problem

I am currently writing a project with TypeScript and NodeJS and I’ve settled on TypeORM as my DB interface. One thing about TypeORM is that it felt like transactions were an afterthought. It’s wonderfully set up to be able to pull a Connection, EntityManager, or Repository from the base getter functions (e.g. getRepository()), however, the moment you have to use a transaction, all of that goes out the window because the transactionalEntityManager that is created has to be passed around to everything that should be contained within the transaction. This is very unfriendly when you can have a series of business logic calls that all need to be included in the same unit of work, but at the same time, these business logic calls should also be available to be called in their own right. The only solution to this problem is to require that every single service that you write should accept a transactional entity manager-like thing that knows how to get a repository within a transaction or not.

Request Scoped Dependency Injection

At first I started off with plain TypeORM along with TypeGraphQL and after learning how to use dependency injection with TypeGraphQL, I quickly found it to be very tedious to maintain all of the dependency injection myself. I found myself wanting something more: enter NestJS.

NestJS is a web framework that tries to remove a bunch of the monotony involved with backend web services while also providing a guiding hand for maintaining a consistent and modular codebase. One of the things that NestJS makes super easy is dependency injection and on top of that, they provide a mechanism for creating a new instance of a dependency per request. This is super important as the transaction should for sure be closed before the request is resolved.

On with the code

We’ll start with the base provider, the UnitOfWork class.

import { Injectable, Scope } from "@nestjs/common";
import { Connection, EntityManager } from "typeorm";

@Injectable({ scope: Scope.REQUEST })
export class UnitOfWork {
    private transactionManager: EntityManager | null;

    constructor(private connection: Connection) {}

    getTransactionManager(): EntityManager | null {
        return this.transactionManager;
    }

    getConnection(): Connection {
        return this.connection;
    }

    async withTransaction<T>(work: () => T): Promise<T> {
        const queryRunner = this.connection.createQueryRunner();
        await queryRunner.startTransaction();
        this.transactionManager = queryRunner.manager;

        try {
            const result = await work();
            await queryRunner.commitTransaction();
            return result;
        } catch (error) {
            await queryRunner.rollbackTransaction();
            throw error;
        } finally {
            await queryRunner.release();
            this.transactionManager = null;
        }
    }
}

The first thing to note is that this class has been decorated with the @Injectable() decorator specifically with the options to set the scope of the dependency to be per-request. This ensures that a new instance of UnitOfWork will be created on each new request (but only if it needs to be used within the request). This provider requires a Connection from TypeORM which is used in the withTransaction() helper function to create a new queryRunner and from that, a transaction.

The withTransaction() function itself is where the unit of work takes place. It works by setting up a transaction, setting the transactionManager attribute (which is an instance of EntityManager), and then calling the passed in work callback function. Anything called within that function that is using our UnitOfWork dependency will all be within the context of this transaction. The whole thing is surrounded with a typical try...catch block that commits the transaction on success or rolls back if an error is caught and finally releasing the query runner connection at the end.

It is very important that queryRunner.release() is called, otherwise the connection will remain open and things like tests will hang.

Next up is the TransactionalRepository:

import { Injectable, Scope } from "@nestjs/common";
import { getRepository, Repository, EntitySchema, ObjectType } from "typeorm";
import { RepositoryFactory } from "typeorm/repository/RepositoryFactory";
import { UnitOfWork } from "./unit-of-work.provider";

@Injectable({ scope: Scope.REQUEST })
export class TransactionalRepository {
  constructor(private uow: UnitOfWork) {}

  /**
   * Gets a repository bound to the current transaction manager
   * or defaults to the current connection's call to getRepository().
   */
  getRepository<Entity>(
    target: ObjectType<Entity> | EntitySchema<Entity> | string
  ): Repository<Entity> {
    const transactionManager = this.uow.getTransactionManager();
    if (transactionManager) {
      const connection = this.uow.getConnection();
      const metadata = connection.getMetadata(target);

      return new RepositoryFactory().create(transactionManager, metadata);
    }

    return getRepository(target);
  }
}

Just like the UnitOfWork, this dependency is also scoped per-request. It takes in a UnitOfWork as a dependency and has only one method: getRepository(). This method should be used similar to the global getRepository() function provided with TypeORM. Pass in an Entity reference and it will return a repository for that entity. If getRepository() is called within a UnitOfWork.withTransaction() block, then the returned repository will be created with the current transaction EntityManager instance, otherwise, it’s just a pass-through to TypeORM’s getTransaction() function.

Lastly, we package all of this up into a re-usable NestJS module like so:

import { Module, Global } from "@nestjs/common";
import { UnitOfWork } from "./unit-of-work.provider";
import { TransactionalRepository } from "./transactional-repository.provider";

@Global()
@Module({
  providers: [UnitOfWork, TransactionalRepository],
  exports: [UnitOfWork, TransactionalRepository],
})
export class UnitOfWorkModule {}

The @Global() decorator makes the providers available globally by anything that imports this module. This is intended so that the main AppModule imports it once and then everything else can access it.

Using the UnitOfWork

Now, within our main app module, we can import and set this up:

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";

import { UnitOfWorkModule } from "./common/database";

import { UsersModule } from "./users/users.module";
import { getConfig } from "./config";

const config = getConfig();

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

One of the things that the UnitOfWork relies on is access to the TypeORM Connection instance. We have taken care of that dependency here with the TypeOrmModule.forRoot() call.

Now that the main app is configured, we can use the TransactionactionalRepository in the UsersService (provided by the UsersModule above).

import { TransactionalRepository } from "../common/database";
import { User } from "./users.entity";

@Injectable()
export class UsersService {
    constructor(private transactionRepo: TransactionalRepository) {}

    get usersRepository() {
        return this.transactionRepo.getRepository(User);
    }

    async findUserById(id: string): Promise<User | null> {
        const user = await this.usersRepository.findOne(id);
        return user ?? null;
    }
}

The UsersService is now ready to be either transactional or not!

On to the resolvers!

import {
    Resolver,
    Args,
    Mutation,
} from "@nestjs/graphql";

import { UnitOfWork } from "../common/database";
import { User } from "./users.entity";
import { UsersService } from "./users.service";
import { CreateUserPayload } from "./create-user/create-user.payload";
import { CreateUserInput } from "./create-user/create-user.input";

@Resolver(User)
export class UsersResolver {
    constructor(
        private usersService: UsersService,
        private uow: UnitOfWork,
    ) {}

    @Mutation(() => CreateUserPayload)
    async createUser(
        @Args("input") input: CreateUserInput
    ): Promise<CreateUserPayload> {
        return this.uow.withTransaction(() => 
            this.usersService.createUser(input)
        );
    }
}

Here is an example usage of a GraphQL resolver taking advantage of the UnitOfWork. The createUser mutation sets up a new transaction with the UnitOfWork.withTransaction()method and then calls the UsersService.createUser() method inside of that unit of work. For the sake of brevity, I’ve not shown the createUser()code, but it performs a few different actions and all should be done within the same unit of work, so this solves my problem nicely and doesn’t add any extra special calls for retrieving a transaction-aware repository!

Conclusion

NestJS provides a very powerful dependency injection mechanism that allows us to create a new transaction, scoped by request and then allow for other services to create repositories using the current transaction EntityManager if set so that large blocks of business logic can be executed atomically.

2 thoughts on “Per-Request Database Transactions with NestJS and TypeORM”

  1. Thanks very much for posting this. I had been looking for a solution to transactions with Nest & TypeORM for ages. All the proposed solutions involved either passing around the EntityManager all the way around the stack (yuk), or using solutions based on continuation-local-storage, which from what I can tell is a bit risky due to using unstable Node APIs.

    I just implemented a proof-of-concept using this example, and it seems to work very nicely with a minimal impact on my existing (pretty large) codebase!

    Reply

Leave a Reply