Per-Request Database Transactions with NestJS and TypeORM

UPDATE – 2024-07-12:

It’s been 4 years since this post was written and the AsyncLocalStorage API has been updated to be stable. I’ve written a new post that details how to take advantage of it in order to have a much simpler API for managing per-request database transactions.

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.

11 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
  2. Thank you so much for this. I have a question, hope you can answer me. In TransactionalRepository, how edit getRepository() function to return Custom Repositories in my project?
    Custom repositories such as UsersRepository extends Repository<UserEntity>{}.
    Because when i use getRepository(UsersRepository) it gives errors.
    Also, why do we return new RepositoryFactory().create(transactionManager, metadata); instead of using the current uow.connection.getRepository?
    I’ll appreciate the help a lot. This is literally the only place online that explains this approach for NestJs and TypeORM. Thanks in advance.

    Reply
    • It’s been a while since I’ve had the chance to play with all of this, but I’m thinking you may be able to do something similar to the Connection class’s existing getCustomRepository() method: https://github.com/typeorm/typeorm/blob/fde9f0772eef69836ff4d85816cfe4fd6f7028b4/src/connection/Connection.ts#L371

      I think you may be able to create your own getCustomRepository() method and, if this.uow.getTransactionManager() is not null, then you can return this.uow.getTransactionManager().getCustomRepository(UserRepository) (I hard-coded UserRepository here, but you’d make it generic, of course).

      That’s a really great question. I was literally re-reading through what I have written here as I’m tackling a new project at work and I’m wanting to borrow and potentially update what I have here. Off the top of my head, I think this.uow.connection.getRepository() would probably work since the Connection instance should be the same.

      Good luck!

      Reply
  3. because of the scope: Scope.REQUEST in the service, the module works with circular dependencies somewhat correctly. Dependencies fail to read and methods send undefined. Is it possible to somehow replace Scope.REQUEST. Any alternative? i found this solution await new Promise (resolve => process.nextTick (resolve)) it can be used before return this.uow.withTransaction

    Reply
    • I’m not sure there. That sounds like a problem with how NestJS resolves dependency injection in general and I’m not sure, but I would doubt that they support circular dependencies.

      Reply
  4. Aaron,

    I was about to invent the wheel with request scoped repositories for transactions and you saved a lot of time(more importantly to assure that it works). Thank you for the great post. I think this stuff should be part of @nestjs/typeorm or some such nestjs ecosystem package. Please move the issue and I am sure a lot more people will be grateful. Transactional security is a very important feature and not realizing that or implementing it defectively, or (as in my case) assuming that it is baked-in must have caused a lot of nestjs bugs already.

    IMO the best implementation to bake it in is having @InjectTransactionalRepository similar to @InjectRepository of @nestjs/typeorm(or with an argument for InjectRespositor, which is unlikely considering that current repos are app scoped). Also great would be giving a configurable app level option to always use transactional repositories(turning all current projects to transaction based code).

    Reply
  5. First of all thanks for sharing this solution!
    But there is one important thing…

    The problem is due to Scope.REQUEST and how Nestjs scope hierarchy works (https://docs.nestjs.com/fundamentals/injection-scopes#scope-hierarchy)

    So in this way all services dependent of TransactionalRepository and controllers/resolvers dependent of UnitOfWork become REQUEST scoped. And this is impact on performance – https://docs.nestjs.com/fundamentals/injection-scopes#performance

    Moreover some parts of application which should be instantiated only once (Scope.DEFAULT) but also should have some services dependency (which now are request scoped) can’t work in such way.
    For my case: I have EventHandler class with @OnEvent (using @nestjs/event-emitter) handlers. And it should have service dependencies. As result EventHandler never instantiated.

    Will be wondered if there is some workaround. Thanks!

    Reply
    • Awesome, thank you! I was wondering if we could do something like that.

      The 0.3.0 really put a kink into everything with its sudden removal of getCustomRepository() without warning or reason. This has kept us from updating our production API. It’s really made me re-evaluate the use of TypeORM as a serious ORM and I’ve started to look at MikroORM for any future projects.

      Reply

Leave a Reply