I am writing a small micro-service in typescript with NestJS. This microservice is responsible of giving a user access to one of our company's systems trial access.
At first I used hubspot integration to add new contacts and send emails, but then I ended up using SendGrid. Both of these services can do almost the same thing (create and add contacts to different lists, send emails automatically, API to send mails and such).
So I decided to write something abstractly, which can be helpful if one day I or someone else decides to move from sendgrid back to hubspot or even write a new implementation of another similar service.
So let's get to the problem
I have created an interface which is called TrialUserManager
, this interface will contain methods on the high-level of what should be done:
export interface TrialUserManager<T> {
createAccessListContactRequest(userInformation: RegisterRequestDto): Promise<any> | any;
sendEmail(data: T);
}
So right now, there are two functions:
createAccessListContactRequest
- Should create a new contact in a specific list. A list is just a collection of contacts (users) in Sendgrid, which is also a feature in Hubspot. So all it does is adding the given user information to that list using the API of Sendgrid or other based on the implementation. RegisterRequestDto
is something globally shared in the microservice, so everyone should know about it, and all services (sendgrid, or hubspot) should have no issue to use it because it's mainly key-value data of the user like first_name
, last_name
, email
, etc.
sendEmail
- Basically just sends an email using the API of the implementation.
Now the issue is sendEmail
, because the excepted function input is different for Sendgrid and for Hubspot, for instance, in Sendgrid you must send a templateId
while in Hubspot you don't, it's some other inputs.
So that's the reason of the generic type T
in the interface, which represents the email options input.
So that's how it's used:
First of all, for these who don't know NestJS, I assign the instance of the service in the right module, to provide either SendgridService
or HubspotService
, both implements TrialUserManager
like this:
const getTrialManagerServiceByProviderType = (type: TrialManagerProviders) => {
switch(type) {
case TrialManagerProviders.HUBSPOT:
return HubspotService;
case TrialManagerProviders.SENDGRID:
return SendgridService;
default:
return undefined;
}
}
And in the Module providers I use it as this:
providers: [
{
provide: 'TrialUserManager',
useClass: getTrialManagerServiceByProviderType(TRIAL_MANAGER_SERVICE)
}
],
So now the injectable string TrialUserManager
will provide the right instance, TRIAL_MANAGER_SERVICE
is a global static variable that is assigned to Sendgrid
.
So back to the implementation usage, I inject TrialUserManager
into the place that it's going to be used, in the constructor:
@Inject('TrialUserManager') private readonly trialUserManager: TrialUserManager<SendgridEmailOptions>,
SendgridEmailOptions
is the object, dedicated to Sendgrid
only, that I pass as a generic type to TrialUserManager
.
But I have a feeling that I am over-complicating it and that this is not right to do, because anyone that uses TrialUserManager
should never care what data type you should pass to it, it's an high level interface and you should not change the types every time you switch the service (sendgrid or hubspot).
I would love to get an advice, best practice about this because this will also help me with other daily problems.
I hope I elaborated properly, and put in enough information and thanks in advance!