In earlier chapters, we touched on various aspects of Dependency Injection (DI) and how it is used in Nest. One example of this is the constructor based dependency injection used to inject instances (often service providers) into classes. You won't be surprised to learn that Dependency Injection is built in to the Nest core in a fundamental way. So far, we've only explored one main pattern. As your application grows more complex, you may need to take advantage of the full features of the DI system, so let's explore them in more detail.
Dependency injection is an inversion of control (IoC) technique wherein you delegate instantiation of dependencies to the IoC container (in our case, the NestJS runtime system), instead of doing it in your own code imperatively. Let's examine what's happening in this example from the Providers chapter.
First, we define a provider. The
@Injectable() decorator marks the
CatsService class as a provider.
Then we request that Nest inject the provider into our controller class:
Finally, we register the provider with the Nest IoC container:
What exactly is happening under the covers to make this work? There are three key steps in the process:
@Injectable()decorator declares the
CatsServiceclass as a class that can be managed by the Nest IoC container.
CatsControllerdeclares a dependency on the
CatsServicetoken with constructor injection:
app.module.ts, we associate the token
CatsServicewith the class
cats.service.tsfile. We'll see below exactly how this association (also called registration) occurs.
When the Nest IoC container instantiates a
CatsController, it first looks for any dependencies*. When it finds the
CatsService dependency, it performs a lookup on the
CatsService token, which returns the
CatsService class, per the registration step (#3 above). Assuming
SINGLETON scope (the default behavior), Nest will then either create an instance of
CatsService, cache it, and return it, or if one is already cached, return the existing instance.
*This explanation is a bit simplified to illustrate the point. One important area we glossed over is that the process of analyzing the code for dependencies is very sophisticated, and happens during application bootstrapping. One key feature is that dependency analysis (or "creating the dependency graph"), is transitive. In the above example, if the
CatsService itself had dependencies, those too would be resolved. The dependency graph ensures that dependencies are resolved in the correct order - essentially "bottom up". This mechanism relieves the developer from having to manage such complex dependency graphs.
Let's take a closer look at the
@Module() decorator. In
app.module, we declare:
providers property takes an array of
providers. So far, we've supplied those providers via a list of class names. In fact, the syntax
providers: [CatsService] is short-hand for the more complete syntax:
Now that we see this explicit construction, we can understand the registration process. Here, we are clearly associating the token
CatsService with the class
CatsService. The short-hand notation is merely a convenience to simplify the most common use-case, where the token is used to request an instance of a class by the same name.
What happens when your requirements go beyond those offered by Standard providers? Here are a few examples:
- You want to create a custom instance instead of having Nest instantiate (or return a cached instance of) a class
- You want to re-use an existing class in a second dependency
- You want to override a class with a mock version for testing
Nest allows you to define Custom providers to handle these cases. It provides several ways to define custom providers. Let's walk through them.
useValue syntax is useful for injecting a constant value, putting an external library into the Nest container, or replacing a real implementation with a mock object. Let's say you'd like to force Nest to use a mock
CatsService for testing purposes.
In this example, the
CatsService token will resolve to the
mockCatsService mock object.
useValue requires a value - in this case a literal object that has the same interface as the
CatsService class it is replacing. Because of TypeScript's structural typing, you can use any object that has a compatible interface, including a literal object or a class instance instantiated with
So far, we've used class names as our provider tokens (the value of the
provide property in a provider listed in the
providers array). This is matched by the standard pattern used with constructor based injection, where the token is also a class name. (Refer back to DI Fundamentals for a refresher on tokens if this concept isn't entirely clear). Sometimes, we may want the flexibility to use strings or symbols as the DI token. For example:
In this example, we are associating a string-valued token (
'CONNECTION') with a pre-existing
connection object we've imported from an external file.
We've previously seen how to inject a provider using the standard constructor based injection pattern. This pattern requires that the dependency be declared with a class name. The
'CONNECTION' custom provider uses a string-valued token. Let's see how to inject such a provider. To do so, we use the
@Inject() decorator. This decorator takes a single argument - the token.
info Hint The
@Inject()decorator is imported from
While we directly use the string
'CONNECTION' in the above examples for illustration purposes, for clean code organization, it's best practice to define tokens in a separate file, such as
constants.ts. Treat them much as you would symbols or enums that are defined in their own file and imported where needed.
useClass syntax allows you to dynamically determine a class that a token should resolve to. For example, suppose we have an abstract (or default)
ConfigService class. Depending on the current environment, we want Nest to provide a different implementation of the configuration service. The following code implements such a strategy.
Let's look at a couple of details in this code sample. You'll notice that we define
configServiceProvider with a literal object first, then pass it in the module decorator's
providers property. This is just a bit of code organization, but is functionally equivalent to the examples we've used thus far in this chapter.
Also, we have used the
ConfigService class name as our token. For any class that depends on
ConfigService, Nest will inject an instance of the provided class (
ProductionConfigService) overriding any default implementation that may have been declared elsewhere (e.g., a
ConfigService declared with an
useFactory syntax allows for creating providers dynamically. The actual provider will be supplied by the value returned from a factory function. The factory function can be as simple or complex as needed. A simple factory may not depend on any other providers. A more complex factory can itself inject other providers it needs to compute its result. For the latter case, the factory provider syntax has a pair of related mechanisms:
- The factory function can accept (optional) arguments.
- The (optional)
injectproperty accepts an array of providers that Nest will resolve and pass as arguments to the factory function during the instantiation process. The two lists should be correlated: Nest will pass instances from the
injectlist as arguments to the factory function in the same order.
The example below demonstrates this.
useExisting syntax allows you to create aliases for existing providers. This creates two ways to access the same provider. In the example below, the (string-based) token
'AliasedLoggerService' is an alias for the (class-based) token
LoggerService. Assume we have two different dependencies, one for
'AliasedLoggerService' and one for
LoggerService. If both dependencies are specified with
SINGLETON scope, they'll both resolve to the same instance.
While providers often supply services, they are not limited to that usage. A provider can supply any value. For example, a provider may supply an array of configuration objects based on the current environment, as shown below:
Like any provider, a custom provider is scoped to its declaring module. To make it visible to other modules, it must be exported. To export a custom provider, we can either use its token or the full provider object.
The following example shows exporting using the token:
Alternatively, export with the full provider object: