typescript 37 lines · 7 steps

Wrapping requests in a transaction with NestJS

A NestJS interceptor opens a database transaction per request and commits, rolls back, or releases it based on the handler's outcome.

Explained by highlit
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable, catchError, concatMap, finalize } from 'rxjs';
3import { DataSource, QueryRunner } from 'typeorm';
4 
5declare module 'express' {
6 interface Request {
7 queryRunner?: QueryRunner;
8 }
9}
10 
11@Injectable()
12export class TransactionInterceptor implements NestInterceptor {
13 constructor(private readonly dataSource: DataSource) {}
14 
15 async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>> {
16 const request = context.switchToHttp().getRequest();
17 const queryRunner = this.dataSource.createQueryRunner();
18 
19 await queryRunner.connect();
20 await queryRunner.startTransaction();
21 request.queryRunner = queryRunner;
22 
23 return next.handle().pipe(
24 concatMap(async (data) => {
25 await queryRunner.commitTransaction();
26 return data;
27 }),
28 catchError(async (err) => {
29 await queryRunner.rollbackTransaction();
30 throw err;
31 }),
32 finalize(() => {
33 void queryRunner.release();
34 }),
35 );
36 }
37}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Interceptors can wrap the entire request lifecycle, running setup before the handler and teardown after its result stream resolves.
  2. 2RxJS operators let you branch cleanup logic on success versus error while guaranteeing a final release regardless of outcome.
  3. 3Attaching a per-request QueryRunner to the request object hands the same transactional connection to downstream handlers.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Wrapping requests in a transaction with NestJS — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code