Scenario: Application at work stores data lists in database tables and they never change after initial population. These data lists are used for dropdown selection in frontend, for example, users select their salutation in user profile page and click to make the change.
Problem: When user opens the page, UI makes a HTTP request to retrieve the salutation list from the server. NestJS API connects the database to query the table and returns the salutation array in the response. Since the list is static, I want to cache the data in memory such that subsequent database calls can be avoided.
Solution #1: When application starts, store data lists in Redis and the API fetches the data from the in-memory cache.
Cons: This solution will require DevOps engineers to provision resources for the Redis instances in the cloud platform and I cannot justify the cost for setting them up for few data lists.
Solution #2: Use useFactory to create a custom provider to store the data lists in memory and use @Inject() to inject the cache in controller to return the cached data
Pros:
- The implementation is done within application level and no DevOps help is required.
- If data list changes in the future, TypeORM migration script will be deployed to the environment and application restarts to recreate the cache.
Lets start with the naive approach that queries database upon every http request
Naive Approach
Step 1: Create a service that waits 2 seconds before returning a list of salutations
async getSalutations(): Promise<Salutation> {
const salutations = ['Mr.', 'Mrs.', 'Miss']
return new Promise((resolve) => setTimeout(resolve, 2000)).then(() => {
console.log('Wait 2 seconds before returning salutations array')
const now = Date.now()
return {
now,
salutations,
}
})
}
The promise returns Data.now() to indicate that the result is obtained at real time.
Step 2: Create an endpoint to return the array of salutations
@Get('salutations-realtime')
async getRealtimeSalutations(): Promise<Salutation> {
return this.appService.getSalutations()
}
Step 3: Curl http://localhost:3000/salutations-realtime to show the timestamp changes every time
Take note that the timestamp is different each time because each call waits 2 seconds before the array is returned in the response.
Next, I will use useFactory to define a custom provider such that getSalutations() is called only once and the 2-second wait is a fixed cost.
Return data as fast as a cheetah
Custom Provider Approach
Step 1: Define interface of the map of data lists
export interface Salutation {
now: number
salutations: string[]
}
export interface DataMap {
salutations: Salutation
}
Step 2: Add a new function in AppService to return the map of data lists
async getDataMap(): Promise<DataMap> {
return {
salutations: await this.getSalutations(),
}
}
I used await this.getSalutations() to wait for the result and to store it in salutations property of the map.
Step 3: Create a custom provider for the map and register it in AppModule
const DataMapFactory = {
provide: 'DataMap',
useFactory: (appService: AppService) => {
return appService.getDataMap()
},
inject: [AppService],
}
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, DataMapFactory],
exports: [DataMapFactory],
})
export class AppModule {}
Whenever I inject the provider name ‘DataMap‘, I have access to the map and use ES6 de-structuring to obtain salutations array
Step 4: Inject ‘DataMap’ in AppController and add a new /salutations endpoint to verify that it actually does what I intend
constructor(private readonly appService: AppService, @Inject('DataMap') private dataMap: DataMap) {}
@Get('salutations')
async getSalutations(): Promise<Salutation> {
const { salutations } = this.dataMap
return salutations
}
Last step: CURL /salutations and verify the timestamp does not change
All CURL responses have the same timestamp and Date.now() can only increase; therefore, /salutations endpoint returns the array from the cached map.
Final thoughts
In this post, I have shown one use case of useFactory of NestJS but useFactory can cache any resource, for example, database connection and instances of third-party libraries. In the future, I can write an article on how to create a custom provider for Node Stripe library in NestJS.
Resources: