Provider Tokens
Separate a provider's token from its recipe: inject a value under a symbol token, bind an abstract class to its implementation, and export a token to another module.
What you'll learn
- Read the long form of a provider, { provide, useClass }, and explain what the shorthand hides
- Provide a value under a symbol token and inject it with @Inject()
- Use an abstract class as the token for an implementation, and export a token to another module
Every providers: [CatsService] you have written so far was shorthand. Underneath, a module's providers array is a table from tokens to instances, and CatsService in that list is both the token (the key Nest looks up when a constructor asks for CatsService) and the recipe (construct this class). Most of the time the two coincide, which is why the shorthand exists. This lesson is about the times they do not: when the thing to inject is a value rather than a class, when the type a constructor names is an interface that no longer exists at runtime, and when one module's provider must reach another under a name rather than a class.
The long form
providers: [CatsService] expands to:
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
provide is the token. useClass is one of four recipes, and the other three are useValue, useFactory and useExisting; the next lesson covers the last two. Once the token and the recipe are separate, they can differ: the token stays CatsService, the recipe becomes a different class, and every constructor asking for CatsService receives the other class without knowing. That is how a test swaps a real service for a fake, and how an application swaps an implementation without touching the code that uses it.
Values
useValue injects a ready-made object, a constant or a library instance:
@Module({
providers: [
{
provide: MailerService,
useValue: { send: async () => undefined },
},
],
})
export class NotificationsModule {}
Here the token is still a class, and anything asking for MailerService receives the object. Nest never constructs it; it is handed over as is.
Tokens that are not classes
A token can be a string or a Symbol, which is the natural choice for things that have no class: configuration, a connection, an API key.
export const API_KEY = 'API_KEY';
@Module({
providers: [{ provide: API_KEY, useValue: process.env.API_KEY }],
})
export class PaymentsModule {}
A constructor cannot name a string as a parameter type, so the token goes in @Inject():
@Injectable()
export class PaymentsClient {
constructor(@Inject(API_KEY) private readonly apiKey: string) {}
}
@Inject(token) tells Nest which token to look up for that parameter, whatever the parameter's type says. Symbols are preferred over strings for anything shared across modules, because two libraries cannot accidentally pick the same Symbol('CONFIG').
Interfaces vanish, abstract classes do not
Lesson 3 of Basics said interfaces cannot be injected, because TypeScript erases them and Nest looks the type up at runtime. There are two ways round it. One is a token in @Inject(), as above. The other is to write the contract as an abstract class, which exists at runtime and can serve as both the type and the token:
export abstract class Mailer {
abstract send(to: string, body: string): Promise<void>;
}
@Injectable()
export class SmtpMailer extends Mailer {
async send(to: string, body: string) { ... }
}
@Module({
providers: [{ provide: Mailer, useClass: SmtpMailer }],
})
export class NotificationsModule {}
A constructor then asks for Mailer with no decorator at all, and receives an SmtpMailer. The class that uses the mailer never learns which implementation it got, and swapping it is one line in the module.
Exporting by token
A custom provider is private to its module like any other. To share it, export the token, or the whole provider object:
@Module({
providers: [{ provide: API_KEY, useValue: process.env.API_KEY }],
exports: [API_KEY],
})
export class PaymentsModule {}
Your task
The cats API gains two things it should not construct itself: a logger, and its configuration.
logging/logger.tsdeclares the abstractLogger;MemoryLoggerimplements it by keeping lines in memory. InCatsModule, provideLoggerso that anything asking for it receives aMemoryLogger.cats/cats.config.tsdeclares theCATS_CONFIGsymbol and theCatsConfigshape. Provide the value{ maxCats: 3 }under that token, and giveCatsServiceits config through the constructor, so the shelter refuses a fourth cat.LogsControllerlives inAppModuleand wants the sameLogger. Share it.
Run once after step 1 and read what Nest says about the argument at index 1. It cannot even name what it was looking for, because the interface left nothing behind, and that is why the token is needed.
When it fails
Nest can't resolve dependencies of the CatsService (Logger, ?)andthe argument at index [1], with no name for it: the config parameter has no@Inject(). TypeScript erased the interface, so Nest has nothing to look up and nothing to print.... argument Symbol(CATS_CONFIG) at index [1] is available ...: the token is right, and nothing is provided under it.Nest can't resolve dependencies of the CatsService (?, ...), argumentLoggerat index [0]:Loggeris a class, so no decorator is needed, but nothing provides it. Register the abstract class as the token.Nest can't resolve dependencies of the LogsController (?):Loggeris provided inCatsModuleand not exported.
Remember
- A provider is a token plus a recipe; the shorthand uses the class as both.
useValueinjects an object as is; a string or symbol token needs@Inject(token)in the constructor.- An abstract class is a contract that survives compilation, so it can be the token for its implementations.
- Export the token to share a custom provider.
Stuck? Show a hint
Two entries in CatsModule's providers: { provide: Logger, useClass: MemoryLogger } and { provide: CATS_CONFIG, useValue: { maxCats: 3 } }. In CatsService, decorate the config parameter with @Inject(CATS_CONFIG). Then add Logger to CatsModule's exports so LogsController, which lives in AppModule, can ask for it.