Beyond Static Data - The Power of Dynamic Reference Data in APIs

Beyond Static Data - The Power of Dynamic Reference Data in APIs

Sunny Sun Lv4

Compare two different appraches in implementing reference data

** This post is updated with new information on 25-July-2024

Many API applications share a common requirement: providing consistent reference data. This ensures all parts of the application use the same values for things like countries, product categories, or any other categorized information. Refer to best practices for RESTful APIs (http://coffeethinkcode.com/2022/11/28/4-best-practices-restful-api-you-should-know/) to learn more about the importance of consistency.

A traditional approach involves using a switch case statement within a single service to handle different reference data types. This method might seem convenient at first, but it can quickly become cumbersome and inflexible as the application grows.

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

Traditional approach: The Switch Case

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.

Better approach: 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.

Best Practices for Dynamic Reference Data

Here are some best practices to be considered when implementing reference data.

  • Versioning: Implement version control for tracking changes and facilitating rollbacks.
  • Caching: Employ caching mechanisms to optimize performance for frequently accessed data.
  • Access Controls: Enforce robust security measures to protect sensitive reference data.
  • Data Validation: Validate reference data to maintain data integrity and consistency.
  • Monitoring: Track data access patterns and usage to identify optimization opportunities.

Summary

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: Beyond Static Data - The Power of Dynamic Reference Data in APIs
  • Author: Sunny Sun
  • Created at : 2024-07-04 00:00:00
  • Updated at : 2024-07-27 15:04:51
  • Link: http://coffeethinkcode.com/2024/07/04/beyond-static-the-power-of-dynamic-reference-data-in-api/
  • License: This work is licensed under CC BY-NC-SA 4.0.