Skip to content

Commit 11683a5

Browse files
committed
docs(recipes): Add recipe for NestJS CLS
1 parent e28a635 commit 11683a5

File tree

1 file changed

+151
-15
lines changed

1 file changed

+151
-15
lines changed

content/recipes/async-local-storage.md

Lines changed: 151 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
### Async Local Storage
22

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 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.
44

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.
66

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.
88

99
#### Custom implementation
1010

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:
1212

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.
1416

1517
```ts
1618
/** als.module.ts */
@@ -32,7 +34,7 @@ export const asyncLocalStorage = new AsyncLocalStorage();
3234
export class AlsModule {}
3335
```
3436

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.
3638

3739
```ts
3840
/** main.ts */
@@ -50,43 +52,177 @@ async function bootstrap() {
5052

5153
// Here we can bind the middleware to all routes,
5254
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,
5457
const store = {
5558
userId: req.headers['x-user-id'],
5659
};
5760
// 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.
5962
als.run(store, () => next());
6063
});
6164
}
6265

6366
bootstrap();
6467
```
6568

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.
6770

6871
```ts
6972
/** cat.service.ts */
7073

7174
export class CatService {
7275
constructor(
76+
// We can inject the provided ALS instance.
7377
private readonly als: AsyncLocalStorage,
7478
private readonly catRepository: CatRepository,
7579
) {}
7680

7781
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.
84+
const userId = this.als.getStore()["userId"] as number;
8185
return this.catRepository.getForUser(userId);
8286
}
8387
}
8488
```
8589

86-
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
8795

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.
8997

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).
9199

92100
> 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+
export class AppModule {}
139+
```
140+
141+
2. And then can use the `ClsService` to access the store values.
142+
143+
```ts
144+
/** cat.service.ts */
145+
146+
export class CatService {
147+
constructor(
148+
// We can inject the provided ClsService instance,
149+
private readonly cls: ClsService,
150+
private readonly catRepository: CatRepository,
151+
) {}
152+
153+
getCatForUser() {
154+
// and use the "get" method to retrieve any stored value.
155+
const userId = this.cls.get('userId');
156+
return this.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+
declare module `nestjs-cls` {
165+
interface ClsStore {
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`.
176+
177+
```ts
178+
describe('CatService', () => {
179+
let service: CatService
180+
let cls: ClsService
181+
const mockCatRepository = createMock<CatRepository>()
182+
183+
beforeEach(async () => {
184+
const module = await Test.createTestingModule({
185+
// Set up most of the testing module as we normally would.
186+
providers: [
187+
CatService,
188+
{
189+
provide: CatRepository
190+
useValue: mockCatRepository
191+
}
192+
],
193+
imports: [
194+
// Import the static version of ClsModule which only provides
195+
// the ClsService, but does not set up the store in any way.
196+
ClsModule
197+
],
198+
}).compile()
199+
200+
service = module.get(CatService)
201+
202+
// Also retrieve the ClsService for later use.
203+
cls = module.get(ClsService)
204+
})
205+
206+
describe('getCatForUser', () => {
207+
it('retrieves cat based on user id', async () => {
208+
const expectedUserId = 42
209+
mockCatRepository.getForUser.mockImplementationOnce(
210+
(id) => ({ userId: id })
211+
)
212+
213+
// Wrap the test call the `runWith` method
214+
// in which we can pass hand-crafted store values.
215+
const cat = await cls.runWith(
216+
{ userId: expectedUserId },
217+
() => service.getCatForUser()
218+
)
219+
220+
expect(cat.userId).toEqual(expectedUserId)
221+
})
222+
})
223+
})
224+
```
225+
226+
#### More information
227+
228+
Visit the [NestJS CLS GitHub Page](https://github.com/Papooch/nestjs-cls) for the full API documentation and more code examples.

0 commit comments

Comments
 (0)