Database Transactions in NestJS: A Complete Guide

Database transactions are critical when you need to ensure that a series of operations either all succeed or all fail together. This becomes especially important when handling complex logic such as financial transfers, user registrations involving multiple tables, or updating related records.

In this article, we’ll explore how to implement database transactions in NestJS using TypeORM, including manual and transactional entity manager approaches. We’ll also discuss the benefits and use cases of using transactions in your backend logic.


Table of Contents

  1. What is a Database Transaction?
  2. Why Use Transactions in NestJS?
  3. Using TypeORM’s @Transaction Decorator (Deprecated)
  4. Using QueryRunner for Manual Transactions
  5. Using DataSource#transaction() Method
  6. Best Practices for Transactions
  7. Conclusion

What is a Database Transaction?

A database transaction is a sequence of operations performed as a single logical unit of work. These operations either all succeed (commit) or fail (rollback), preserving data integrity.

The four key properties of a transaction (ACID) are:

  • Atomicity: All changes succeed or none do.
  • Consistency: Data remains valid and consistent.
  • Isolation: Transactions do not interfere with each other.
  • Durability: Once committed, changes are permanent.

Why Use Transactions in NestJS?

In a NestJS application, transactions are useful when:

  • Creating related records in multiple tables.
  • Updating and rolling back on failure.
  • Preventing partial writes that corrupt data.
  • Coordinating multiple repository operations within a single request.

Using TypeORM’s @Transaction Decorator (Deprecated)

TypeORM previously supported the @Transaction() decorator, but it has been deprecated and should be avoided in newer applications. Instead, prefer using QueryRunner or DataSource.transaction().


Using QueryRunner for Manual Transactions

QueryRunner provides fine-grained control over database transactions. You can start a transaction, execute queries, and either commit or rollback manually.

Example: Manual Transaction with QueryRunner

typescriptCopyEditimport { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User } from './user.entity';
import { Profile } from './profile.entity';

@Injectable()
export class UserService {
  constructor(private dataSource: DataSource) {}

  async createUserWithProfile(userData: any, profileData: any) {
    const queryRunner = this.dataSource.createQueryRunner();

    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const user = queryRunner.manager.create(User, userData);
      await queryRunner.manager.save(user);

      const profile = queryRunner.manager.create(Profile, {
        ...profileData,
        user,
      });
      await queryRunner.manager.save(profile);

      await queryRunner.commitTransaction();

      return { user, profile };
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}

Explanation

  • We create and connect a QueryRunner.
  • Start a transaction using startTransaction().
  • Perform operations using queryRunner.manager.
  • Commit or rollback depending on whether an error occurs.
  • Always release the query runner in finally.

Using DataSource#transaction() Method

The modern and cleaner way to handle transactions is using the DataSource.transaction() method. This method takes care of connection management and error handling internally.

Example: DataSource.transaction() Usage

typescriptCopyEdit@Injectable()
export class UserService {
  constructor(private dataSource: DataSource) {}

  async createUserWithProfile(userData: any, profileData: any) {
    return await this.dataSource.transaction(async (manager) => {
      const user = manager.create(User, userData);
      await manager.save(user);

      const profile = manager.create(Profile, {
        ...profileData,
        user,
      });
      await manager.save(profile);

      return { user, profile };
    });
  }
}

Why Use DataSource.transaction()?

  • Simplifies boilerplate code.
  • Handles rollback automatically on failure.
  • Ensures transactional integrity with cleaner syntax.

Best Practices for Transactions

Here are some tips to follow when using transactions in NestJS:

  1. Avoid long-running logic inside a transaction — keep it fast.
  2. Always release QueryRunner if using it manually.
  3. Wrap only critical sections that require atomicity.
  4. Use repository methods inside transactions rather than raw queries where possible.
  5. Log failures and always handle rollback scenarios.

Conclusion

Database transactions are a vital feature when working with critical data in NestJS applications. With TypeORM’s QueryRunner or DataSource.transaction(), you can implement reliable and consistent transactional logic that maintains data integrity even in complex scenarios.

In this module, you’ve learned the different ways to handle transactions and the best practices for implementing them correctly in NestJS.