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:
transaction
object of your favourite ORM across services without breaking encapsulation and isolation by explicitly passing it around.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.
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.
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 fromtypescript >= 4.4
- Namely allowingsymbol
members in interfaces. If you can't upgrade but still want to use this library, install version1.6.2
, which lacks the typing features.
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.
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
.
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
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 an interceptor that
ClsService
to get access to the current shared 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();
}
}
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();
}
}
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 + '!';
}
}
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!
Contributing to a community project is always welcome, please see the Contributing guide :)
Author: Papooch
Source Code: https://github.com/Papooch/nestjs-cls
License: MIT license