0

I have created a generic service for my HTTP APIs and I want to provide this service for different endpoint without creating a class for each of my endpoints. My service looks like this

export class ApiService<T> {
  constructor(private httpClient: HttpClient, private otherDependency: OtherDependency, private subPath: string, private defaultHeaders = []) { }
}

And I am injecting it using tokens and providers like these ones

import { InjectionToken } from '@angular/core';

export const XXXToken = new InjectionToken<ApiService<XXX>('MyService');
export const XXXProvider = {
  provide: XXXToken,
  useFactory: (httpClient: HttpClient, dep: OtherDependency) => new ApiService<XXX>(httpClient, dep, 'xxx'),
  deps: [ HttpClient, OtherDependency]
};

Previous code will actually fail when trying to use my provider in the declarations of a NgModule and building in production mode (only in production mode). Error looks like

 Error during template compile of 'AppModule'
 Consider changing the function expression into an exported function.

Issue is coming from (httpClient: HttpClient, dep: OtherDependency) => new ApiService<XXX>(httpClient, dep, 'xxx')

I can make it work if I do something like

export function myFactory(httpClient: HttpClient, dep: OtherDependency) => new ApiService<XXX>(httpClient, dep, 'xxx');
export const XXXProvider = {
  provide: XXXToken,
  useFactory: myFactory,
  deps: [ HttpClient, OtherDependency]
};

However, because the provider and factory is more complicated than that, I've created an helper function to not have to repeat that code any time I am adding a new endpoint.

export func getProvider(token, subPath) {
  return {
    provide: token,
  useFactory: (httpClient: HttpClient, dep: OtherDependency) => new ApiService<XXX>(httpClient, dep, subPath),
  deps: [ HttpClient, OtherDependency]
  }
}

And in there I cannot use the export function trick because I want my subpath to be different everytime I am creating a new service.

Is there a way to get around this or am I stuck to repeating my provider creation every time I need it. I would be very sad since that would mean for example that I would have to change every provider creation anytime I need a new dependency on that service. === Edit === I want to be able to inject my services easily in any service/component without having to bin those other objects to the service creation or implementation detailes and there can be many services in one component. Ideally I would like something like that

export class MyComponent implements OnInit {
  constructor(private userService: ApiService<User>, private countryService: ApiService<countryService>) { }

  ngOnInit() {
    userService.get();
    countryService.get();
  }
}

But because javascript does not realy support generics, this cannot work because ApiService and ApiService are actually the same class. Therefore I currently need injection token anyway so I would be OK with

export class MyComponent implements OnInit {
  constructor(@Inject(UserToken) private userService: ApiService<User>, @Inject(CountryToken) private countryService: ApiService<countryService>) { }
Adrien Buet
  • 191
  • 10

1 Answers1

1

since you want to provide same service with different end points, providing your end point config as an InjectionToken and injecting that token in your service would make your life easier. so change your service definition as follows;

export const MY_API_ENDPOINT = new InjectionToken<string>('MY_API_ENDPOINT');

@Injectable()
export class ApiService<T> {
  constructor(@Inject(MY_API_ENDPOINT) private endPoint: string) { }
}

when you inject ApiService through out your application, configure MY_API_ENDPOINT at provider level (doesn't matter whether it is at Component or Module level)

in component level

@Component({
  selector: 'app-component1',
  templateUrl: './component1.component.html',
  styleUrls: ['./component1.component.css'],
  providers: [ApiService, { provide: MY_API_ENDPOINT, useValue: "api.end.point.1" }]
})
export class Component1Component implements OnInit {}

or in module level

@NgModule({
  imports: [CommonModule],
  declarations: [Component3Component],
  exports: [Component3Component],
  providers: [ApiService, { provide: MY_API_ENDPOINT, useValue: "api.end.point.3" }]
})
export class App2Module { }

i created a demo that demonstrates both Component and Module level providers with different endpoints

https://stackblitz.com/edit/angular-4b5yb7

===== SECOND SOLUTION =====

above solution will not work if a component needs same service with two different end points. so we need a way to create same service instances with different parameters. following you original approach of creating a factory that creates service instances; i modified the factory function that returns a function which takes endPoint as parameter, and that function creates instances with unique endPoints.

{
    provide: ApiService, useFactory:
      (someOtherService: SomeOtherService) => {
        return (endPoint: string) => {
          return new ApiService(endPoint, someOtherService);
        }
      }, deps: [SomeOtherService /** this is just an example to show how to inject other dependencies*/]
}

and use it as follows in component

export class Component1Component {
  apiService1 = this.apiFactory<string>("end.point.1");
  apiService2 = this.apiFactory<number>("end.point.2");

  constructor(@Inject(ApiService) private apiFactory: <T>(endPoint: string) => ApiService<T>) {}
}

here is a working demo https://stackblitz.com/edit/angular-6kmy8w

ps. i got the idea from this post. i improved typing and incorporated changes for making the service generic

ysf
  • 4,634
  • 3
  • 27
  • 29
  • Endpoint needs to have all kind of value (one for every API endpoint) and a single component could very much needs two different endpoints. I don't understand how I am able to do that with your solution. – Adrien Buet Jun 24 '19 at 11:03
  • Anyway, that would add a dependency regarding service creation at component level which I am really not a fan of. Service constructor was of course simplified: there is a bunch of injected dependencies + two parameters that need to change dependending on the endpoint (+ whatever evolution that could arrive later on) – Adrien Buet Jun 24 '19 at 11:10
  • if a single component needs same service with two different endpoints, it is not possible to achieve it with a single provider token. because when you inject `ApiService` to your component, you tell the injector to find/create `ApiService` and provide it. if you need the same service in your component with different config, how can injector know which config to use if it is asked with same token? actually that's the same point where you stuck in your attempt `getProvider(token, subPath)`. you are unable to tell injector which subPath to use. – ysf Jun 24 '19 at 13:04
  • only solution i can think of is to use [Aliased class providers](https://angular.io/guide/dependency-injection-providers#aliased-class-providers) Such that, you still have one service implementation `ApiService` that takes a configuration object as parameter such as `EndPointConfig` which holds all config parameters that `ApiService` needs. And finally you define aliases such as `Api_1_Service` `Api_2_Service` etc. where each alias has it is own unique configuration. İf you think that this would solve your problem i can make an example. – ysf Jun 24 '19 at 13:09
  • In my current solution I indeed have one token per endpoint. And because it is a pain to create, I have helper function to create those. And that's this helper function that causes the issue in production build. I am not sure to understand how aliased class would help me since I only have 1 class. I currently thinking of a solution were I would create a new sub-class per endpoint which would have hard-coded parameters in its constructor. That would remove the need of token entirerly but I find it really sad to create a bunch of class to inject parameters in constructor... – Adrien Buet Jun 27 '19 at 09:24
  • i added a second solution. hope this one helps :) – ysf Jun 27 '19 at 15:37
  • Thanks a lot for your help, I appreciate it, but I am not yet convinced :(. This approach would require every component to know how to create each of the endpoint. Like if I need to change the way one single endpoint is created, I would have to update every single component/service using that endpoint. I don't want my components to handle service creation logic. That's the whole point of DI. – Adrien Buet Jun 28 '19 at 07:36
  • i don't give up that easy, sorry :) can you give me an example/stub that shows how you would like to define and inject your service to a component. preferably an example that requires two end points in one component. – ysf Jun 28 '19 at 07:56
  • I've updated my question :) For now in my project, I went for declaring a new class for each service which is OK because it gets me rid of all the tokens but forces me to repeat constructor many times. If at some point I need a new dependency, I would have to update all API services (and I'm gonna have tens of them). – Adrien Buet Jul 01 '19 at 07:31