How to Use Async Local Storage in NestJS

Updated: December 31, 2023 By: Guest Contributor Post a comment

Introduction

Async local storage in NestJS enables scope-bound storage, allowing you to maintain context across asynchronous operations. It’s essential in tracing requests and handling per-request data without passing parameters around.

Setting Up Async Local Storage

To begin, install the necessary packages:

npm install @nestjs/common @nestjs/core reflect-metadata

Then, set up the async local storage middleware:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AsyncLocalStorage } from 'async_hooks';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const asyncLocalStorage = new AsyncLocalStorage();
  app.use((req, res, next) => {
    asyncLocalStorage.run(new Map(), () => {
      next();
    });
  });
  await app.listen(3000);
}
bootstrap();

Injecting and Using the Storage

Here’s how you can inject and use the storage:

import { Injectable, Scope } from '@nestjs/common';
import { AsyncLocalStorage } from 'async_hooks';

@Injectable({ scope: Scope.REQUEST })
export class MyService {
  constructor(private asyncLocalStorage: AsyncLocalStorage) {}

  myMethod() {
    const store = this.asyncLocalStorage.getStore();
    // Use the store
  }
}

Providing Context

To provide context for dependencies, use interceptors or middleware:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { AsyncLocalStorage } from 'async_hooks';

@Injectable()
export class ContextInterceptor implements NestInterceptor {
  constructor(private asyncLocalStorage: AsyncLocalStorage) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable {
    const ctx = this.asyncLocalStorage.getStore();
    if (ctx) {
      ctx.set('key', 'value');
    }
    return next.handle();
  }
}

Advanced Usage: Per-Request Providers

Create providers that are new for each request like this:

import { REQUEST } from '@nestjs/core';
import { Provider } from '@nestjs/common';

export const myRequestProvider: Provider = {
  provide: 'MY_PROVIDER',
  useFactory: (request) => new MyProvider(request),
  inject: [REQUEST],
};

Integrating with Other Modules

To integrate async local storage with other modules:

import { DynamicModule, Module } from '@nestjs/common';
import { MyService } from './my.service';

@Module({})
export class MyModule {
  static forRoot(): DynamicModule {
    return {
      module: MyModule,
      providers: [
        {
          provide: 'ASYNC_CONTEXT',
          useClass: MyService,
        },
      ],
      exports: ['ASYNC_CONTEXT'],
    };
  }
}

This way, you’d be able to inject ‘ASYNC_CONTEXT’ as needed across your application.

Conclusion

Async local storage is a powerful feature that helps maintain a coherent state across asynchronous calls in NestJS applications. By following this guide, you’ve learned how to set up, use and integrate async local storage into your NestJS projects. Remember to use this feature judiciously to avoid memory leaks and ensure context-correct operations.