Nestjs-cls: The Easiest Way to Use Continuation-Local Storage in Nest

NestJS CLS (Async Context)

A continuation-local storage module compatible with NestJS' dependency injection based on AsyncLocalStorage.

Notice: The documentation has been moved to a dedicated website.

Continuation-local storage allows to store state and propagate it throughout callbacks and promise chains. It allows storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages.

Some common use cases that this library enables include:

  • Tracking the Request ID and other metadata for logging purposes
  • Keeping track of the user throughout the whole request
  • Making the dynamic Tenant database connection available everywhere in multi-tenant apps
  • Propagating the authentication level or role to restrict access to resources
  • Seamlessly propagating the transaction object of your favourite ORM across services without breaking encapsulation and isolation by explicitly passing it around.
  • Using "request" context in cases where actual REQUEST-scoped providers are not supported (passport strategies, cron controllers, websocket gateways, ...)

Most of these are to some extent solvable using REQUEST-scoped providers or passing the context as a parameter, but these solutions are often clunky and come with a whole lot of other issues.

Motivation

NestJS is an amazing framework, but in the plethora of awesome built-in features, I still missed one.

I created this library to solve a specific use case, which was limiting access to only those records which had the same TenantId as the request's user in a central manner. The repository code automatically added a WHERE clause to each query, which made sure that other developers couldn't accidentally mix tenant data (all tenants' data were held in the same database) without extra effort.

AsyncLocalStorage is still fairly new and not many people know of its existence and benefits. Here's a nice talk from NodeConf about the history. I've invested a great deal of my personal time in making the use of it as pleasant as possible.

While the use of async_hooks is sometimes criticized for making Node run slower, in my experience, the introduced overhead is negligible compared to any IO operation (like a DB or external API call). If you want fast, use a compiled language.

Also, if you use some tracing library (like otel), it most likely already uses async_hooks under the hood, so you might as well use it to your advantage.

Highlights

New: Version 3.0 introduces Proxy Providers as an alternative to the imperative API. (Minor breaking changes were introduced, see Migration guide).

Version 2.0 brings advanced type safety and type inference. However, it requires features from typescript >= 4.4 - Namely allowing symbol members in interfaces. If you can't upgrade but still want to use this library, install version 1.6.2, which lacks the typing features.

Installation

Install as any other NPM package using your favorite package manager.

npm install nestjs-cls
yarn add nestjs-cls
pnpm add nestjs-cls

INFO

This module requires additional peer deps, like the @nestjs/core and @nestjs/common libraries, but it is assumed those are already installed.


Quick Start

Background

This library exposes a dynamic ClsModule which exposes the injectable ClsService and provides means to setting up and interacting with the CLS context.

The CLS context is a storage that wraps around a chain of function calls. It can be accessed anywhere during the lifecycle of such chain via the ClsService.

Example

Below is an example of using this library to store the client's IP address in an interceptor and retrieving it in a service without explicitly passing it along.

NOTE

This example assumes you are using HTTP and therefore can use middleware. For usage with non-HTTP transports, see Setting up CLS context.

Register the ClsModule

Register the ClsModule and automatically mount the ClsMiddleware which wraps the entire request in a shared CLS context on all routes.

app.module.ts

@Module({
    imports: [
        ClsModule.forRoot({
            global: true,
            middleware: { mount: true },
        }),
    ],
    providers: [AppService],
    controllers: [AppController],
})
export class AppModule {}

Create IP-address interceptor

Create an interceptor that

  • injects the ClsService to get access to the current shared CLS context,
  • extract the users's IP address from the request and stores it into the CLS context,

user-ip.interceptor.ts

@Injectable()
export class UserIpInterceptor implements NestInterceptor {
    constructor(private readonly cls: ClsService) {}

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest();
        const userIp = request.connection.remoteAddress;
        this.cls.set('ip', userIp);
        return next.handle();
    }
}

Mount interceptor to controller

By mounting the UserIpInterceptor on the controller, it gets access to the same shared CLS context that the ClsMiddleware set up.

Of course, we could also bind the interceptor globally with APP_INTERCEPTOR.

app.controller.ts

@UseInterceptors(UserIpInterceptor)
@Injectable()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get('/hello')
    hello() {
        return this.appService.sayHello();
    }
}

Access CLS context in service

In the AppService, we can retrieve the user's IP from the CLS context without explicitly passing in anything, and without making the AppService request-scoped!

app.service.ts

@Injectable()
export class AppService {
    constructor(private readonly cls: ClsService) {}

    sayHello() {
        const userIp = this.cls.get('ip');
        return 'Hello ' + userIp + '!';
    }
}

That's it

This is pretty much all there is to it. This library further provides more quality-of-life features, so read on!

INFO

If your use-case is really simple, you can instead consider creating a custom implementation with AsyncLocalStorage. Limiting the number of dependencies in your application is always a good idea!


Documentation

➡️ Go to the documentation website 📖


Contributing

Contributing to a community project is always welcome, please see the Contributing guide :)


Download Details:

Author: Papooch
Source Code: https://github.com/Papooch/nestjs-cls 
License: MIT license

#nest #nestjs #async #context 

Nestjs-cls: The Easiest Way to Use Continuation-Local Storage in Nest
1.05 GEEK