Nicolò Andronio

Nicolò Andronio

Full-stack developer, computer scientist, engineer
Evil Genius in the spare time

Per-request transactions with Nest.js and TypeORM

Nest.js is a widely used progressive framework for the node ecosystem. With its large and varied common library, it provides all the basic functions you’ll ever ask for when developing a node backend. In many ways, it’s the node equivalent of Java SpringBoot. It also offers integration with TypeORM out of the box. TypeORM is an object-relational-mapping for TypeScript and it supports several famous databases.

Getting started within the framework is a breeze Nest.js’s dependency injection makes it easy to share services across your codebase, even if its excess of poignancy can become mildly frustrating when declaring the module structure. If you disregard the heavily opinionated module/provider/export hierarchy, writing code comes natural, especially if you have a Java background.

Nonetheless, something is amiss. Sooner or later you will find out the lack of transactionality support for TypeORM queries. With that, I don’t mean you cannot write database transactions. You can, as shown by the documentation. However, there is no helper or out-of-the-box support for per-request transactions. In Java, you would achieve that with the @Transactional annotation. In Nest.js… well, you’ll need to read through the next paragraphs.

Transactional interceptor

Interceptors are useful Nest.js concepts that allow to bind before/after logic to any endpoint. While developing one of my latest projects at Mind Foundry, that is exactly what I was looking for. Opening a new transaction when a request start, closing it when the request ends. It sounds like an ideal way to achieve per-request transactionality. So here’s what I achieved so far:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Injectable()
export class TransactionInterceptor implements NestInterceptor {
private readonly logger = new Logger(TransactionInterceptor.name);

constructor(private readonly connection: Connection) {}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>> {
const httpContext = context.switchToHttp();
const req = httpContext.getRequest();

const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();

req.queryRunner = queryRunner;

return next.handle().pipe(
tap(() => {
queryRunner
.commitTransaction()
.catch((e: Error) => this.logger.error(`Could not commit transaction: ${e.toString()}`));
}),
catchError(e => {
queryRunner
.rollbackTransaction()
.catch((rollbackError: Error) => this.logger.error(`Could not rollback transaction: ${rollbackError.toString()}`));
return throwError(e);
}),
finalize(() => {
queryRunner
.release()
.catch((e: Error) => this.logger.error(`Could not release connection: ${e.toString()}`));
}),
);
}
}

The constructor takes advantage of Nest.js dependency injection. Connection comes from the TypeORM namespace and, contrary to its name, does not represent a single database connection but rather a connection pool, therefore it’s safe to use in this context.

A QueryRunner object is the main way you’ll execute queries within the TypeORM context. Once created, its connect method establishes a new database connection: yes, a new connection per request, as the interceptor is execute on a per-request basis. First, I want to reassure you. Worry not! Your database will generally be able to take several hundred active connections at any given time. In our case, we are using the good old PostgreSQL. Secondly, I would like you to consider your application workload and start worrying! If you expect to receive hundreds or thousands of concurrent requests, you should scale your database and make sure it can support an appropriate number of open connections. If the connection pool is exhausted, a request will remain pending until a new connection is available.

By leveraging RxJS’s observable, we can manage several different outcomes. If the request is successful, the transaction is committed. If an error is thrown, it is rolled back. And in any case, ALWAYS make sure to release the connection! Forgetting to do so will break your backend in a few minutes.

So far so good. We have an interceptor executed before and after each request, opening a transaction and closing it afterwards. How is that usable, though? It doesn’t seem to interact with any other code you have written.

In order for queries to take effect within a transaction, they must use the same query runner!

Hence why we are attaching the queryRunner object to the request. So that we’ll be able to access it from any controller.

The ugly bits

The fundamental problem at the bottom of this quest is that dependency injection in Nest.js works globally. Once a TypeORM repository is injected into a service, there is no way of controlling what it will do with the Connection object, which - I will remind you - represents a connection pool. Hence why relying on dependency injection, while useful, will just cause multiple connections to be opened even within the same request. If something breaks in the middle of the request, there is no way of rolling back inconsistent changes because of the lack of transactionality.

We need a way to connect services we write to the specific queryRunner attached to each request. So far, I only found a rather ugly way to achieve this. It first requires that every injectable service also declares a static method fromQueryRunner, which initializes another instance of the service object:

1
2
3
4
5
6
7
8
9
10
11
12
@Injectable()
export class TaskDao {
constructor(
@InjectRepository(Task)
private readonly taskRepository: Repository<Task>,
) {}


static fromQueryRunner(queryRunner: QueryRunner): TaskDao {
const taskRepository = queryRunner.manager.getRepository(Task);
return new TaskDao(taskRepository);
}
}

Unfortunately, declaring that method will incur in a fair bit of duplication, which is however balanced by an increased simplicity of usage of services within controllers, as I will show briefly.

The next bit employs Nest.js decorators. The intention is to use a decorator to annotate parameters of a controller method so that Nest.js will automatically inject them with our new queryRunner dependant instances.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface InjectedRequest {
queryRunner: QueryRunner;
}

interface DatabaseReliantComponentType<T> {
fromQueryRunner(queryRunner: QueryRunner): T;
}

export const Transactional = createParamDecorator(
<T>(data: DatabaseReliantComponentType<T>, ctx: ExecutionContext): T => {
const req = ctx.switchToHttp().getRequest<InjectedRequest>();
return data.fromQueryRunner(req.queryRunner);
},
);

createParamDecorator is an utility provided by Nest.js that builds a decorator that is correctly interpreted during a request and injects a specific value into a function parameter. As you can read above, the Transactional decorator grabs the current request from the context and instantiates a new instance of the given component using the request-specific queryRunner.

DatabaseReliantComponentType represents the type of a component (service, dao, or any class annotated with @Injectable). Thanks to the previous definition of fromQueryRunner, each service class now defines that method and thus satisfies the interface. Unfortunately, I couldn’t find any way to statically enforce the definition of a static method as TypeScript does not support “static interfaces”.

Finally, in any controller method you can add a new parameter and annotate it with @Transactional to obtain a transactional version of a service, tied to the request transaction.

1
2
3
4
5
6
7
8
9
10
11
@Controller("tasks")
export class TaskController {
@Get(":taskId")
async fetch(
@Transactional(TaskDao) taskDao: TaskDao,
@Param("taskId") taskId: TaskId,
): Promise<Task> {
const task = await taskDao.findById(taskId);
return toDto(task);
}
}

Of course this example doesn’t really need a transaction because it only entails a read operation. Yet in many other cases you will need different services that interact with each other. You can autowire them within the controller method by declaring multiple decorated parameters. All instances will be tied to the same queryRunner and thus belong to the same transaction! The following example demonstrates that a single service may depend on other services which define their own fromQueryRunner method, and so on and so forth:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable()
export class TaskDefinitionService {
constructor(
private readonly taskDefinitionDao: TaskDefinitionDao,
private readonly userRoleService: UserRoleService,
) {}


static fromQueryRunner(queryRunner: QueryRunner): TaskDefinitionService {
const taskDefinitionDao = TaskDefinitionDao.fromQueryRunner(queryRunner);
const userRoleService = UserRoleService.fromQueryRunner(queryRunner);
return new TaskDefinitionService(taskDefinitionDao, userRoleService);
}
}

We are thus implementing a second layer of dependency injection manually that works on a per-request basis. Hence the “ugliness”. We are sidestepping a facility that Nest.js already provides, albeit with a slightly different scope. At the end of the day, I think the code smells are worth if they make the application work and the code is easier to write and read for other developers.