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
- What is a Database Transaction?
- Why Use Transactions in NestJS?
- Using TypeORM’s
@Transaction
Decorator (Deprecated) - Using
QueryRunner
for Manual Transactions - Using
DataSource#transaction()
Method - Best Practices for Transactions
- 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:
- Avoid long-running logic inside a transaction — keep it fast.
- Always release
QueryRunner
if using it manually. - Wrap only critical sections that require atomicity.
- Use repository methods inside transactions rather than raw queries where possible.
- 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.