Dans l’article précédent, une architecture à été proposée pour faciliter le développement et le maintient du webhook de notre chatbot (cf Chatbot : Coder Proprement Un Webhook Avec NestJs).
En partant de cette base, on va voir comment faciliter l’ajout de nouveaux handlers en automatisant nos injections.
Pour le moment, pour charger les handlers dans le DispatchService
, ce que nous faisons est d’injecter les handlers un à un et de les stocker dans la liste de handlers :
@Injectable()
export class DispatchService {
// Liste des handlers disponibles.
private handlers: ActionHandler[] = [];
// On injecte les handlers et on les stocke dans
// la liste de handlers.
constructor(
@InjectCreateDoerHandler createDoerHandler: CreateDoerHandler,
@InjectGetMissionHandler getMissionHandler: GetMissionHandler,
) {
this.handlers = [createDoerHandler, getMissionHandler];
}
// ...
}
Ça se fait, mais est-ce qu’il n’y aurait pas un moyen qui permettrait de le faire automatiquement pour ne pas se casser la nénette ? L’idée c’est de ne plus avoir à toucher la logique du DispatchService
seulement pour ajouter un handler dans la liste. Par exemple, est-ce qu’il y’a moyen de charger les handlers automatiquement une fois qu’ils ont été ajoutés dans le module ?

Tkt frelot(te) à la compote, j’ai tout prévu.
Commençons par
La découverte du DiscoveryModule
de NestJs 🛰🔍
Le DiscoveryModule
fait partie du core package de NestJs. C’est le module qui permet au framework de trouver les bons services à injecter en se basant sur leurs metadata.
Par exemple, NestJs sait qu’une classe est injectable grâce à l’annotation @Injectable()
. En allant voir dans le code source, on s’aperçoit que ce décorateur ne fait qu’ajouter des metadata à la classe annotée :
// Ça se code à une main.
export function Injectable(options?: InjectableOptions): ClassDecorator {
return (target: object) => {
// NestJs se base sur la Metadata Reflection API
// pour ajouter des metadonnées à la classe
// qu'on veut rendre injectable.
Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
};
}
Je rentre pas dans les détails de la Metadata Reflection API parce que ça ferait un article bien trop gros.
Partant de là, on peut utiliser la même mécanique pour marquer les classes qui sont des handlers. Il suffit de leurs ajouter une metadata maison puis de les récupérer grâce au DiscoveryService
et sa méthode getProviders()
.
Aparté sur la méthode getProviders()
Comme son nom l’indique, la méthode récupère l’ensemble des providers injectés dans l’application. Et elle fait ça sans distinction de module. ✊🏻✊🏼✊🏽✊🏾✊🏿
Par exemple, si on considère les modules suivants :
@Module({
providers: [ProviderA]
})
export class ModuleA {}
@Module({
providers: [ProviderB]
})
export class ModuleB {}
Et qu’on les injecte dans notre AppModule
:
@Module({
imports: [ModuleA, ModuleB]
})
export class AppModule {}
Même si ProviderA
et ProviderB
n’ont pas été exportés depuis leur module respectif, on les retrouvera quand même dans la liste de handlers retournée par la méthode getProviders()
.
Pour être précis, la méthode retourne une liste de InstanceWrapper
qui possèdent 2 propriétés intéressantes :
metatype
: contient les métadonnées de la classe providée. On va utiliser la Metadata Reflection API pour aller chercher la métadonnée qui nous intéresse.instance
: contient une instance de la classe providée. Dans notre cas, ça sera une instance de handler si le provider possède la bonne métadonnée.
Ceci étant dit, on peut faire une première ébauche pour trouver les providers qui sont des handlers en utilisant le DiscoveryService
:
@Injectable()
export class DispatchService {
// Liste des handlers disponibles.
private handlers: ActionHandler[] = [];
// Le DiscoveryService de NestJs nous permet
// de trouver des services en se basant sur leurs
// metadata.
// Plus besoin d'injecter les handlers à la main.
constructor(private readonly discoverySvc: DiscoveryService) {}
// On cherche les handlers une fois que le
// ChatbotModule a été initialisé.
onModuleInit(): void {
this.discoverHandlers();
}
// Cherche les handlers grâce à leur metadata.
private discoverHandlers(): void {
this.handlers = this.discoverySvc
// On récupère la liste de l'ensemble des
// providers injectés dans l'application.
.getProviders()
// On ne garde que les providers qui
// possèdent la bonne metadata.
.filter((wrapper) => !!wrapper.metatype && !!Reflect.getMetadata(NOTRE_METADATA_MAISON, wrapper.metatype))
// Enfin, on récupère l'instance du provider.
.map((wrapper: InstanceWrapper<ActionHandler>) => wrapper.instance);
}
// ...
}
L’injection sans pression 💉🍻
Maintenant qu’on a notre stratégie, on peut passer à l’implémentation.
En reprenant la logique plus haut, on va commencer par créer notre propre décorateur pour injecter notre metadata maison :
import { CustomDecorator, SetMetadata } from '@nestjs/common';
// Notre metadata maison.
export const ACTION_HANDLER_METADATA_KEY = 'CHATBOT_HANDLER';
// Et notre décorateur maison qui se contente
// d'associer un simple booléen à notre metadata.
export const ActionHandlerDecorator = (): CustomDecorator => SetMetadata(ACTION_HANDLER_METADATA_KEY, true);
On peut ensuite l’appliquer à chacun de nos handlers.
En reprenant ceux de l’article précédent, ça nous donne :
@Injectable()
@ActionHandlerDecorator()
export class CreateDoerHandler {
// ...
}
@Injectable()
@ActionHandlerDecorator()
export class GetMissionHandler {
// ...
}
Il ne nous reste plus qu’à adapter le DispatchService
pour filtrer les providers selon la bonne metadata :
@Injectable()
export class DispatchService {
// ...
// Cherche les handlers grâce à leur metadata.
private discoverHandlers(): void {
this.handlers = this.discoverySvc
.getProviders()
// On ne garde que les providers qui
// possèdent la bonne metadata.
.filter((wrapper) => !!wrapper.metatype && !!Reflect.getMetadata(ACTION_HANDLER_METADATA_KEY, wrapper.metatype))
.map((wrapper: InstanceWrapper<ActionHandler>) => wrapper.instance);
}
}
Épilogue
Pour résumer, au lancement du programme, le DispatchService
va parcourir l’ensemble des providers répertoriés dans l’application pour ne garder que ceux qui ont notre metadata maison ACTION_HANDLER_METADATA_KEY
et les stocker dans sa liste de handlers.
Créer un nouveau handler est maintenant plus simple que jamais : il suffit de
- Créer une nouvelle classe injectable.
- Annoter la classe avec notre décorateur
ActionHandlerDecorator
. - Ajouter la classe dans les providers de notre application.
Le DispatchService
se charge du reste, sans qu’on ait à le modifier.
I have been surfing online more than 2 hours today, yet I never found any interesting article like
yours. It’s pretty worth enough for me. In my opinion, if all website
owners and bloggers made good content as you did,
the web will be a lot more useful than ever before.
Thanks for the feedback 👌 What is the most interesting part to you?