DEV Community

Micael Levi L. C.
Micael Levi L. C.

Posted on • Updated on

NestJS tip: multi-value providers almost like `multi` from Angular

for NestJS v8, v9 and v10

You can see and play with the full code here.


Angular's dependency injection mechanism has a feature called Multi Providers, which looks like this:

// =============== Angular code  =============== //
import { Component, InjectionToken, Inject } from '@angular/core'
const TOKEN = new InjectionToken<string>('MyToken')
@Component({
  // ...
  providers: [
    { provide: TOKEN, useValue: 'foo', multi: true },
    { provide: TOKEN, useValue: 'bar', multi: true },
  ],
})
class AppComponent {
  constructor(@Inject(TOKEN) public myValues: string[]) {
    console.log(myValues) // outputs ["foo", "bar"]
  }
}
Enter fullscreen mode Exit fullscreen mode

NestJS doesn't come with that capability but we can implement it (kinda).

The final version I'm proposing here looks like this:

final result

module definition

terminal output

we have a factory function called provide that receives an array of 'enhanced' providers and return providers array but registering all providers with the same token under a multi-value provider as long as it has multi: true.

Disclaimer

The solution I'm about to show is intended to be as simple as possible, thus it doesn't work exactly like multi providers from Angular. I encourage you to use this mental model to build a better solution :)

Known limitations:

  • Only merges providers registered within the same module
  • Does not validate if there is a multi provider with the same token as one non-multi
  • Does not take in count providers with scopes other than the default (singleton lifestyle). I didn't tested such scenario
  • Dependency not found errors may look a bit cryptic now due to how the provider is being registered under the hood
  • Not work with forwardRef, but it should be feasible to have

Problem

NestJS providers consists of two main parts: a value and a unique token. We can see that with custom providers, which are just objects that have the properties provide (provider's token) and some value that will be defined based on which kind of provider you got (namely: useClass, useExisting, useValue or useFactory). You can learn more on this subject here.

To retrieve some provider NestJS's DI system uses its token (in a given context). So although we can declare multiple providers with the same token, only the latest one will be returned when we inject that provider into others providers.

We want to register multiple providers under the same token, and we should have an easy API for this. When retrieving that provider, we should get an array of values.

Solution

Leverage on multi: true API from Angular and implement one version of that feature by using the useFactory custom provider as suggested by Kamil here.

The overall ideia here is:

  1. Group all providers marked with multi: true by their common token
  2. For each provider in that group, register it using some unique token to avoid naming collisions. And save this token in that group for later usage
  3. For each entry in that group, register a new provider with useFactory that injects all of the collected providers at step (2) and return an array of them

I'll do only 2 iterations for performance sake: one to collect and save multi-value providers while registering non-multi ones, as usual; and other to create the final factory provider for each group.

The provide helper function is defined as follows:

// provide.util.ts
import type {
  Type,
  ClassProvider,
  ValueProvider,
  FactoryProvider,
  ExistingProvider,
  Provider,
  InjectionToken,
} from "@nestjs/common"

type EnhancedProvider<T = any> =
  Type<T>
| ((
    ClassProvider<T> |
    ValueProvider<T> |
    FactoryProvider<T> |
    ExistingProvider<T>
  ) & { multi?: true })

export function provide(providers: EnhancedProvider[]): Provider[] {
  /** The final providers list that we should pass to some module. */
  const providersToRegister: Provider[] = []
  /** A map with all multi-value providers with their common token. */
  const multiValueProviderTokensByGroupToken = new Map<InjectionToken, InjectionToken[]>()

  for (const provider of providers) {
    if ('multi' in provider && provider.multi) {
      const currProviderToken = provider.provide

      const providerTokens = multiValueProviderTokensByGroupToken.get(currProviderToken) || []

      // Create a unique token for this provider so that it can be injected later
      const uniqueToken: InjectionToken = `multi-provider at idx ${providerTokens.length + 1} with token ${currProviderToken.toString()}`
      providerTokens.push(uniqueToken)
      multiValueProviderTokensByGroupToken.set(currProviderToken, providerTokens)

      // Register the provider but using the unique token instead
      providersToRegister.push({
        ...provider,
        provide: uniqueToken,
      })
    } else {
      // Non-multi provider, so just register it as-is
      providersToRegister.push(provider)
    }
  }

  for (const [providerGroupToken, tokensToInject] of multiValueProviderTokensByGroupToken) {
      providersToRegister.push({
      provide: providerGroupToken,
      inject: tokensToInject,
      useFactory: (...providers) => providers,
    })
  }

  return providersToRegister
}
Enter fullscreen mode Exit fullscreen mode

Top comments (4)

Collapse
 
kostyatretyak profile image
Костя Третяк • Edited

Another limitation of NestJS that limits the modularity of this framework. Custom provider as suggested by Kamil - it's about a known number of multi-providers. And what about a developer who publishes his code on npmjs.com? How does he know this number?

For example, in Ditsmod, multi-providers are used to support internalization (i18n). This module does not know the number of languages that will be added by users of this module.

Collapse
 
helveg profile image
Robin De Schepper • Edited

You don't show a usage example, but from the source code it looks like you need to call provide(myProviders), and it will simply group those with multi: true and the same token, but that ultimately doesn't offer much advantages over just providers: [{provide: token, useFactory: (...args) => args, inject: [...myProviders]}] and its associated drawbacks: it doesn't work across module boundaries, and requires you to know all the providers beforehand, correct me if I'm wrong.

I created a package that works more closely to Angular's multi provider, you can provide the same token multiple times in different modules, and then call collect(TOKEN) in any module where you'd like to have a TOKEN provider that is equal to the array of individual provided values.

github.com/Helveg/nestjs-multi-pro...

Collapse
 
kostyatretyak profile image
Костя Третяк

@micalevisk, do you know the reason for NestJS's low popularity here (on dev.to)? You are one of the main maintainers of NestJS, and this framework is downloaded 2 million times a week, but you have only one reaction on your post. I can't understand where those fans who downloaded these two million are.

It seems that the dev.to resource itself is visited by 14 million users last month, but is there such a small concentration of NestJS fans among them? Please share the view statistics of this post. Is it measured in tens or hundreds of views?

I don't think the situation is better on medium.com. Maybe you know of sites where there are a lot of NestJS fans?

Collapse
 
micalevisk profile image
Micael Levi L. C. • Edited

I don't have a clue, actually.

We have this linked at github.com/nestjs/awesome-nestjs and I try to bring more popularity to here in nestjs telegram groups.

Maybe its because people aren't used with dev.to tags or dev.to in general (I didn't checked how's the popularity of express here)?

Also, we should keep in mind that the downloads rate at NPM didn't exclude CIs.

Maybe you know of sites where there are a lot of NestJS fans?

I don't :/ dev.to is my favorite one.

My view stats on this post is too low (< 25). But as of now I got 433 at the first post I've made.