You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`AsyncLocalStorage` is a [Node.js API](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage) (based on the `async_hooks` API) that provides an alternative way of propagating state through the application without needing to explicitly pass it as a function parameter relying on REQUEST scoped providers and their limitations. It is similar to a thread-local storage in other languages.
3
+
`AsyncLocalStorage` is a [Node.js API](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage) (based on the `async_hooks` API) that provides an alternative way of propagating local state through the application without the need to explicitly pass it as a function parameter. It is similar to a thread-local storage in other languages.
4
4
5
-
The main idea of AsyncLocalStorage is that we can _wrap_ some function call with the `AsyncLocalStorage#run` call. All code that is invoked within the wrapped call gets access to the same `store`, which will be unique to each call chain.
5
+
The main idea of Async Local Storage is that we can _wrap_ some function call with the `AsyncLocalStorage#run` call. All code that is invoked within the wrapped call gets access to the same `store`, which will be unique to each call chain.
6
6
7
-
In the context of NestJS, that means if we can find a place within the request's lifecycle, where we can wrap the rest of the request's code, we will be able to access and modify state visible only to that request.
7
+
In the context of NestJS, that means if we can find a place within the request's lifecycle where we can wrap the rest of the request's code, we will be able to access and modify state visible only to that request, effectively eliminating the need for REQUEST scoped providers and their limitations.
8
8
9
9
#### Custom implementation
10
10
11
-
While NestJS itself does not provide a built-in support for it, there is a [3rd party package](#nestjs-cls) which supports many use-cases. However, let's first walk through how we could implement it ourselves for the simplest HTTP case, so we can get a better understanding of the whole concept:
11
+
NestJS itself does not provide any built-in abstraction for `AsyncLocalStorage`, so let's walk through how we could implement it ourselves for the simplest HTTP case to get a better understanding of the whole concept:
12
12
13
-
1. First, create a new instance of the `AsyncLocalStorage` in some shared source file. Since we're using NestJS, let's also turn it into a custom provider.
13
+
> info **info** For a ready-made [dedicated package](#nestjs-cls), continue reading below.
14
+
15
+
1. First, create a new instance of the `AsyncLocalStorage` in some shared source file. Since we're using NestJS, let's also turn it into a module with a custom provider.
14
16
15
17
```ts
16
18
/** als.module.ts */
@@ -32,7 +34,7 @@ export const asyncLocalStorage = new AsyncLocalStorage();
32
34
exportclassAlsModule {}
33
35
```
34
36
35
-
2.Since we're only concerned with HTTP, let's use a middleware to wrap the `next`call with the `AsyncLocalStorage#run` call. This will make the `store` available in all enhancers and the rest of the system, since a middleware is the first thing that the request hits.
37
+
2.We're only concerned with HTTP, so let's use a middleware to wrap the `next`function with `AsyncLocalStorage#run`. Since a middleware is the first thing that the request hits, this will make the `store` available in all enhancers and the rest of the system.
36
38
37
39
```ts
38
40
/** main.ts */
@@ -50,43 +52,177 @@ async function bootstrap() {
50
52
51
53
// Here we can bind the middleware to all routes,
52
54
app.use((req, res, next) => {
53
-
// populate the store with some default values,
55
+
// populate the store with some default values
56
+
// based on the request,
54
57
const store = {
55
58
userId: req.headers['x-user-id'],
56
59
};
57
60
// and and pass the "next" function as callback
58
-
// to the "als.run" method with the default store.
61
+
// to the "als.run" method with together the store.
59
62
als.run(store, () =>next());
60
63
});
61
64
}
62
65
63
66
bootstrap();
64
67
```
65
68
66
-
3. Now, anywhere within the lifecycle of a request, we can access the local store instance
69
+
3. Now, anywhere within the lifecycle of a request, we can access the local store instance.
67
70
68
71
```ts
69
72
/** cat.service.ts */
70
73
71
74
exportclassCatService {
72
75
constructor(
76
+
// We can inject the provided ALS instance.
73
77
privatereadonlyals:AsyncLocalStorage,
74
78
privatereadonlycatRepository:CatRepository,
75
79
) {}
76
80
77
81
getCatForUser() {
78
-
//the "getStore" method will always return the
79
-
// instance associated with the given request
80
-
const userId =this.als.getStore().userId;
82
+
//The "getStore" method will always return the
83
+
//store instance associated with the given request.
4) That's it. Now you have a way to share request related state without needing to inject the whole `REQUEST` object.
90
+
4. That's it. Now we have a way to share request related state without needing to inject the whole `REQUEST` object.
91
+
92
+
> warning **warning** Please be aware that while the technique is useful for many use-cases, it inherently obfuscates the code flow (creating implicit context), so use it responsibly and especially avoid creating contextual "[God objects](https://en.wikipedia.org/wiki/God_object)".
93
+
94
+
### NestJS CLS
87
95
88
-
#### NestJS CLS
96
+
The [nestjs-cls](https://github.com/Papooch/nestjs-cls) package provides several DX improvements over using plain `AsyncLocalStorage` (`CLS` is an abbreviation of the term _continuation-local storage_). It abstracts the implementation into a `ClsModule` that offers various ways of initializing the `store` for different transports (not only HTTP), as well as a strong-typing support.
89
97
90
-
The `nestjs-cls` package abstracts `AsyncLocalStorage`
98
+
The store can then be accessed with an injectable `ClsService`, or entirely abstracted away from the business logic by using [Proxy Providers](https://www.npmjs.com/package/nestjs-cls#proxy-providers).
91
99
92
100
> info **info**`nestjs-cls` is a third party package and is not managed by the NestJS core team. Please, report any issues found with the library in the [appropriate repository](https://github.com/Papooch/nestjs-cls/issues).
101
+
102
+
#### Installation
103
+
104
+
Apart from a peer dependency on the `@nestjs` libs, it only depends on the built-in Node.js API. Install it as any other package.
105
+
106
+
```bash
107
+
npm i nestjs-cls
108
+
```
109
+
110
+
#### Usage
111
+
112
+
A similar functionality as described [above](#custom-implementation) can be implemented using `nestjs-cls` as follows:
113
+
114
+
1. Import the `ClsModule` in the root module.
115
+
116
+
```ts
117
+
/** app.module.ts */
118
+
119
+
@Module({
120
+
imports: [
121
+
// Register the ClsModule,
122
+
ClsModule.forRoot({
123
+
middleware: {
124
+
// automatically mount the
125
+
// ClsMiddleware for all routes
126
+
mount: true,
127
+
// and use the setup method to
128
+
// provide default store values.
129
+
setup: (cls, req) => {
130
+
cls.set('userId', req.headers['x-user-id']);
131
+
},
132
+
},
133
+
}),
134
+
],
135
+
providers: [CatService],
136
+
controllers: [CatController],
137
+
})
138
+
exportclassAppModule {}
139
+
```
140
+
141
+
2. And then can use the `ClsService` to access the store values.
142
+
143
+
```ts
144
+
/** cat.service.ts */
145
+
146
+
exportclassCatService {
147
+
constructor(
148
+
// We can inject the provided ClsService instance,
149
+
privatereadonlycls:ClsService,
150
+
privatereadonlycatRepository:CatRepository,
151
+
) {}
152
+
153
+
getCatForUser() {
154
+
// and use the "get" method to retrieve any stored value.
155
+
const userId =this.cls.get('userId');
156
+
returnthis.catRepository.getForUser(userId);
157
+
}
158
+
}
159
+
```
160
+
161
+
3. To get strong typing of the store values managed by the `ClsService`, we can use an optional type parameter (`ClsService<MyStoreType>`), or use application-wide typescript module augmentation.
162
+
163
+
```ts
164
+
declaremodule`nestjs-cls` {
165
+
interfaceClsStore {
166
+
userId:number
167
+
}
168
+
}
169
+
```
170
+
171
+
#### Testing
172
+
173
+
Since the `ClsService` is just another injectable provider, it can be entirely mocked out in unit tests.
174
+
175
+
However, in certain integration tests, we might still want to use the real `ClsService` implementation. In that case, we will need to wrap the context-aware piece of code with a call to `ClsService#run` or `ClsService#runWith`.
0 commit comments