for NestJS v8, v9 and v10
What?
Let's say you want to use multiple "versions" of the same NestJS provider that is created by a 3rd-party lib that you don't control.
For example, when using the HttpModule
module from @nestjs/axios
we can use the dynamic module HttpModule.register()
to configure our Axios instance. That module exposes a HttpService
that has such configuration. But what if we want to have many Axios instances, each one with its own config and import everything in one module?
We can't do the following because there's no way to distinguish between the two HttpService
providers registered since they live under the same injection token, the class reference HttpService
, although we have two instances of that HttpService
class:
@Module({
imports: [
HttpModule.register({
baseURL: 'https://api.example.com',
}),
HttpModule.register({
baseURL: 'https://api.another.example.com',
}),
],
})
export class AppModule {
constructor(
private readonly httpService: HttpService,
) {
console.log(this.httpService.axiosRef.defaults.baseURL) // https://api.example.com
}
}
The only idiomatic way to achieve that AFIAK is by creating a wrapper module for each configuration. That module will import HttpModule.register()
and expose the HttpService
provider via another injection token, so we could inject that provider along with others instances of HttpService
as usual.
How?
Like this:
-
app.module.ts
-- we want to inject multiple HTTP services here
import { Module } from '@nestjs/common'
import { CatApiModule, CatApiHttpService } from './cat-api'
@Module({
imports: [
CatApiModule,
DogApiModule,
],
})
export class AppModule {
constructor(
private catApi: CatApiHttpService,
private dogApi: DogApiHttpService,
) {}
}
-
cat-api.module.ts
-- our wrapper module. Responsible for creating a HTTP service client for the cats API
import { Module, OnModuleInit } from '@nestjs/common'
import { HttpModule, HttpService } from '@nestjs/axios'
import { CatApiHttpService } from './cat-api-http.service'
@Module({
imports: [
HttpModule.register({ // line A
timeout: 1000,
maxRedirects: 2,
baseURL: 'https://http.cat',
}),
],
providers: [
{
provide: CatApiHttpService,
useExisting: HttpService, // line B
}
],
exports: [CatApiHttpService], // line C
})
export class CatApiModule implements OnModuleInit {
constructor(private readonly httpService: HttpService) {}
// Defining others configuration for our Axios instance
onModuleInit() {
this.httpService.axiosRef.defaults.headers.common['Accept'] = 'application/json'
}
}
-
cat-api-http.service.ts
-- the interface on which consumers ofCatApiModule
will rely on. The implementation itself is up toHttpService
import { HttpService } from '@nestjs/axios'
export abstract class CatApiHttpService extends HttpService {}
The DogApiHttpService
one would follow the same pattern as shown above.
How it works
When importing the dynamic module HttpModule.register()
(line A), we'll have the HttpService
provider available to use in the CatApiModule
module.
Since we don't want to expose that provider with the same injection token due to posible name collisions, we can leverage on the useExisting
alias provider (line B) to tell to NestJS that we now have 2 providers, one is just an alias to the other.
Then we are exposing the CatApiHttpService
token instead of HttpService
as a proxy to consume the HttpService
provider (line C).
You can see that CatApiHttpService
is an abstract class (a TypeScript feature). This is a way to tell to developers that that class isn't supposed to be initialized. Also, we are using concrete classes here to avoid using the @Inject()
utility while injecting the HttpService
.
Top comments (7)
Yes, a provider with the
useExisting
property is used for aliases. In my opinion, the name of the propertyuseExisting
is not very clear describing the behavior of such a provider. At one time, I proposed to rename it to useToken, but the Angular team did not want to do it (it was done in tsyringe instead).Interesting use of the abstract class to guarantee an easier way to work with the automated dependency injection. More straightforward than needing to use
@Inject('CatApi') private readonly catApi: HttpService
. Very nice jobHow would you go about this if you needed separate Axios instances (with their own interceptors) for each of these
HttpService
extensions?The scenario I'm dealing with: I have an application that imports two separate of my own modules, each making its own HTTP requests. I implemented Axios interceptors for each, but since they are dependencies of the parent project, the Axios instance (and thus the interceptors) is the same for both, so all requests are intercepted by all the defined interceptors.
Is there a way not only inject different versions of the
HttpService
using your solution but that each of them has their own version of the Axios instance so interceptors can be defined independently?Thank you so much!
As you can see here, a new axios instance is created for each call of
HttpModule.register
/HttpModule.registerAsync
, which is later used byHttpService
exported byHttpModule
. Thus, what you want is already covered in this post.The issue was that I was importing
HttpModule
without registering withHttpModule.register
orHttpModule.registerAsync
. Now each is using its own Axios instance as you describe. Thank you!Thank you so much! This should really be in the official nestjs docs!
This saved me today! Thank you