Dynamic Reference Data in NestJS

Dynamic Reference Data in NestJS

Sunny Sun Lv4

Implement a Future-Proof Solution for Your Reference Data Needs

In most API applications, providing reference data is a common need. The provision of reference data ensures we use consistent values across our application. These data include countries, product categories, or any other categorized information needed by our application.

A common approach for implementing a reference data API endpoint involves using a switch case within a single service to handle different data types. However, it can quickly become cumbersome and inflexible as the application grows.

I will explore building a dynamic and maintainable reference data endpoint in NestJS in the article.

The Switch Case Dilemma

Let’s imagine a scenario where a switch case is used within our ReferenceDataService to handle different data types. Here’s a simplified example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// app.controller.ts  
@Get(':dataType')
async getReferenceData(@Param('dataType') dataType: string): Promise<Country[]> {
return await this.referenceDataFactory.getReferenceData(dataType);
}


// reference-data.service.ts
@Injectable()
export class ReferenceDataService {
constructor( private readonly countryService: CountryService,
private readonly industryService: IndustryService ) { }


async getReferenceData(type: string): Promise<any[]> {
switch (type) {
case 'country':
return await this.countryService.getReferenceData();
case 'industry':
return await this.industryService.getReferenceData();
default:
throw new Error(`Unsupported reference data type: ${type}`);
}
}
}

While this works for a few data types, it becomes messy and less maintainable as more types are added. Here are the two main issues:

  • Code Clutter: With each new data type, we need to inject a new service into the constructor and add a corresponding case in the switch statement within getReferenceData. This leads to a bloated constructor and cluttered logic, making the code harder to read and maintain.
  • Tight Coupling: The service becomes tightly coupled to the specific concrete services (CountryService, IndustryService). If we introduce new data types with different service implementations, we’d need to modify the ReferenceDataService constructor and switch-case statements, making the code harder to read and reason about.

Improved reference data service with ModuleRef and Token Providers

A more scalable and flexible approach is to leverage NestJS’s dependency injection capabilities and dynamic service retrieval. Here’s how we can achieve this:

Interface and Concrete Services

Firstly, we define an interface for our reference data service.

1
2
3
4
5
6
7
8
9
10
11
12
13
export interface ReferenceDataService<T extends ReferenceDataItem> {  
getReferenceData(): Promise<T[]>;
}


// Concrete Service Example (country.service.ts)
@Injectable()
export class CountryService implements ReferenceDataService<Country> {
async getReferenceData(): Promise<Country[]> {
// Implement logic to fetch country data
return [];
}
}

We can implement the concert services based on the ReferenceDataService interface whenever a new data type is introduced.

Register the concert services with token providers

In NestJS, we can define token providers to identify services dynamically. Here, we define two constants for token identifiers (COUNTRY_DATA_TOKEN and INDUSTRY_DATA_TOKEN), and register a token for each concrete service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const COUNTRY_DATA_TOKEN = 'country';  
const INDUSTRY_DATA_TOKEN = 'industry';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
ReferenceDataFactory,
{
provide: COUNTRY_DATA_TOKEN,
useClass: CountryService
},
{
provide: INDUSTRY_DATA_TOKEN,
useClass: IndustryService
}]
})
export class AppModule { }

By registering services with specific tokens, we decouple the service implementation from its usage.

Reference Data Service Refactoring

Now, we can refactor our ReferenceDataService to retrieve the specific service based on the requested data type as below.

1
2
3
4
5
6
7
8
9
10
@Injectable()  
export class ReferenceDataFactory {
constructor(private readonly moduleRef: ModuleRef) { }


async getReferenceDataService(type: string) {
const service = await this.moduleRef.resolve(type) as ReferenceDataService<ReferenceDataItem>;
return await service.getReferenceData();
}
}

In the above code, we inject ModuleRef into the service constructor. Then we use moduleRef.get with the retrieved token to dynamically get the corresponding service instance, and the instance is used to fetch the actual data.

By utilizing ModuleRef and token providers to dynamically retrieve a specific reference data service instance, we eliminate the need for individual service injection in the constructor and the switch case. When introducing a new data type, we don’t need to change the ReferenceDataFactory service!

Consuming the Service:

Now, in our controller, we can inject the ReferenceDataFactory and dynamically retrieve the desired data:

1
2
3
4
@Get(':dataType')  
async getIndustries(@Param('dataType') dataType: string): Promise<ReferenceDataItem[]> {
return await this.referenceDataFactory.getReferenceDataService(dataType);
}

This approach is much easier to maintain, allowing us to handle new data types as our application evolves.

Although NestJS is used in this article, the same pattern applies to other programming languages and frameworks. I will write a new post to provide a similar implementation using .Net later. You can find the source code in the post here .

I hope you find this post useful. Happy programming!

  • Title: Dynamic Reference Data in NestJS
  • Author: Sunny Sun
  • Created at : 2024-07-04 00:00:00
  • Updated at : 2024-07-07 13:43:51
  • Link: http://coffeethinkcode.com/2024/07/04/dynamic-referencedata/
  • License: This work is licensed under CC BY-NC-SA 4.0.