Un guide sur l'injection de dépendances dans NestJS
L'auteur a sélectionné Open Source Initiative pour recevoir un don dans le cadre du programme Write for DOnations.
Introduction
L'injection de dépendance (DI) signifie injecter une dépendance. Il fait référence à un modèle dans lequel les dépendances d'un composant sont fournies en entrée par une entité externe généralement appelée injecteur. NestJS utilise le DI comme fonctionnalité principale sur laquelle son architecture a été construite. DI permet la création d'objets dépendants en dehors d'une classe provider
et fournit ces objets créés à la classe qui en a besoin (en tant que dépendance).
Conditions préalables
Pour suivre ce guide, vous avez besoin des éléments suivants :
- Node.js (version >= 16) installé sur votre système d'exploitation. Vous pouvez suivre le guide Comment installer Node.js et créer un environnement de développement local.
- Un éditeur de code et un outil de test d'API comme VSCode et Postman.
- Se familiariser avec les bases de NestJS telles que les Fournisseurs, les Contrôleurs et les Modules.
Un rappel rapide sur les classes JavaScript
Les classes
sont comme un plan/modèle pour créer des objets en JavaScript. Les Classes
créent des objets à l'aide de l'opérateur new
. Lorsque vous définissez une classe et utilisez l'opérateur new
dessus, vous créez une instance de cette classe. Chaque instance est un objet instancié en fonction de la structure de la classe définie. Les objets créés ont des propriétés qui peuvent être les accessoires de données ou de méthodes ajoutés par la classe.
class Greeting {
sayGoodMorning() {
return "Hello, Good Morning";
}
}
const morningGreeting = new Greeting().sayGoodMorning();
console.log(morningGreeting);
Lorsque vous exécutez ce morceau de code, vous remarquerez le résultat suivant :
Hello, Good Morning
Ici, la classe Greeting
a été définie à l'aide du mot-clé class
avec une méthode sayGoodMorning
qui renvoie une chaîne lorsqu'elle est appelée. Lorsque vous créez une instance de la classe Greeting
, elle renvoie un objet créé sur la base de la structure de classe Greeting
définie précédemment. Désormais, l'objet morningGreeting
créé a accès à la méthode encapsulée sayGoodMorning
définie dans la structure de classe. C'est pourquoi vous pouvez faire Greeting().sayGoodMorning()
.
Une autre méthode importante est le constructeur
qui est utilisé pour créer et initialiser un objet créé avec une classe.
Remarque : Il ne peut y avoir qu'une seule méthode de constructeur.
class Greeting {
constructor(message) {
this.message = message;
}
sayGoodMorning() {
return this.message;
}
}
const morningGreeting = new Greeting(
"Hello, Good Morning to you."
).sayGoodMorning();
console.log(morningGreeting);
Dans cet exemple, une méthode constructeur est introduite qui crée et initialise l'instance de la classe Greeting
. Dans le premier exemple, une instance de la classe Greeting
a été créée même lorsque la méthode constructeur n'a pas été définie car, par défaut, JavaScript fournit un constructeur si aucun n'est spécifié. De plus, cette instance encapsule des données spécifiques message
car la méthode constructeur a été définie pour accepter un paramètre et définir l'instance de la propriété de message, contrairement au premier exemple où l'instance n'a encapsulé aucune donnée spécifique sans propriété. a été défini dans l'objet. Le deuxième exemple avec un constructeur permet de renvoyer un message flexible et dynamique, contrairement au premier exemple qui renverra toujours un Hello Good Morning statique.
const morningGreeting = new Greeting(
"Hello, Good Morning to you."
).sayGoodMorning();
const morningGreeting2 = new Greeting(
"Hi, Good Morning to you."
).sayGoodMorning();
Si vous enregistrez ces deux instances d'objet, vous les aurez dans votre console :
Hello, Good Morning to you.
Hi, Good Morning to you.
Pour une meilleure compréhension, examinez un scénario réel d'un système de blog qui n'utilise pas DI. Supposons qu'il existe un fichier appelé classe BlogService
qui gère l'opération CURD pour les articles de blog. Cette classe dépendra d'une classe de service de base de données (DatabaseService
) pour pouvoir interagir avec la base de données.
class DatabaseService {
constructor() {
this.connectionString = "mongodb://localhost:27017";
}
connect() {
console.log(`Database Connection initiated${this.connectionString}`);
}
createPost(post) {
console.log("Post created");
return `Creating ${post.title} to the database`;
}
getAllPosts() {
console.log("All posts returned");
return [];
}
}
class BlogService {
constructor() {
this.databaseService = new DatabaseService();
}
createPost(title, content) {
return this.databaseService.createPost({ title, content });
}
getAllPosts() {
return this.databaseService.getAllPosts();
}
}
const blogService = new BlogService();
const createdPost = blogService.createPost("DI NestJS", "Hello World");
const posts = blogService.getAllPosts();
console.log("blogService", blogService);
console.log("createdPost", createdPost);
Dans ce code, il y a deux actions principales à noter. Premièrement, le BlogService
est étroitement couplé au DatabaseService
, ce qui signifie que le BlogService
ne pourra pas fonctionner sans le DatabaseService
. >. Et deuxièmement, le BlogService
crée directement une instance du DatabaseService
, qui restreint le BlogService
à un seul module de base de données.
Maintenant, voici comment le code précédent peut être reformé à l'aide de DI dans NestJS :
import { Injectable } from '@nestjs/common';
@Injectable()
class DatabaseService {
constructor() {
this.connectionString = 'db connection string';
}
createPost(post) {
console.log(`Creating post "${post.title}" to the database.`);
}
getAllPosts() {
console.log("Fetching all posts from the database.");
return [{ title: 'My first post', content: 'Hello world!' }];
}
}
@Injectable()
class BlogService {
constructor(private databaseService: DatabaseService) {}
createPost(title, content) {
const post = { title, content };
this.databaseService.save(post);
}
listPosts() {
return this.databaseService.getAllPosts();
}
}
Dans NestJS, vous n'instanciez pas manuellement les classes avec le mot-clé new
, le conteneur DI du framework NestJS le fait pour vous sous le capot. Dans ce morceau de code, vous utilisez le décorateur @Injectable()
pour déclarer le DatabaseService
et le BlogService
indiquant qu'ils sont tous deux des fournisseurs que NestJS peut injecter.
Inversion de contrôle (IoC) et injection de dépendances (DI)
L'architecture Nest est construite autour de modèles de conception solides appelés injection de dépendances. L'injection de dépendances (DI) est un modèle que NestJS utilise pour atteindre l'IoC. DI permet la création d'objets dépendants en dehors d'une classe et fournit ces objets à une autre classe qui en dépend par injection au moment de l'exécution plutôt que par la classe dépendante qui les crée. L’avantage est que cela crée un code plus modulaire et plus maintenable.
D'après l'exemple de code précédent, DatabaseService
est une dépendance de BlogService
. Avec DI dans NestJS, vous pouvez créer une instance de l'objet DatabaseService
en dehors du BlogService
et la fournir au BlogService
via le constructeur injection plutôt que d'instancier le DatabaseService
directement dans la classe BlogService
.
IoC est une technique utilisée pour inverser le flux de contrôle d'un programme. Au lieu que l'application contrôle le flux et la création des objets, NestJS contrôle l'inversion. Le conteneur NestJS IoC gère l'instanciation et l'injection de dépendances, où il crée une architecture faiblement couplée en gérant les dépendances entre les objets.
En bref, IoC inverse le flux de contrôle pour la conception du programme. Au lieu que votre code appelle et gère chaque dépendance, vous externalisez le contrôle vers un conteneur ou un framework, pour permettre à votre application d'être plus modulaire et plus flexible face aux changements dus à son faible couplage.
Injection de dépendances dans NestJS
Assurez-vous que Node est installé sur votre ordinateur. De plus, vous devrez installer globalement Nest CLI à l'aide de la commande :
npm i -g @nestjs/cli
Créez un nouveau projet Nest à l'aide de Nest CLI :
nest new nest-di
Accédez au répertoire de votre projet :
cd nest-di
Par défaut, vous disposez d'un AppModule
qui a AppService
comme fournisseur et AppController
comme contrôleur.
Générez une ressource supplémentaire appelée players
à l'aide de la commande :
nest g resource players
Cela configurera la ressource players
en générant un code passe-partout pour une ressource CRUD. Il crée les fichiers players.module
, players.controller
et player.service
par défaut.
import { Controller, Get } from '@nestjs/common';
import { PlayersService } from './players.service';
@Controller('players')
export class PlayersController {
constructor(private readonly playersService: PlayersService) {}
@Get()
getPlayers(): string {
return this.playersService.getPlayers();
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class PlayersService {
private readonly players = [
{ id: 1, name: 'Lionel Messi' },
{ id: 2, name: 'Christiano Ronaldo' },
];
getPlayers(): any {
return this.players;
}
}
Dans les ensembles de codes précédents, vous pouvez voir que le PlayerController
dépend de la classe PlayersService
pour terminer l'opération d'obtention de la liste des joueurs. Cela signifie que PlayersService
est une dépendance de PlayerController
. Dans le fichier players.module
, le PlayersService
est répertorié dans le tableau des fournisseurs. NestJS traite les fournisseurs comme des classes qui peuvent être instanciées et partagées dans l'application. Ici, en répertoriant PlayersService
comme fournisseur, NestJS crée une instance de PlayersService
qui peut être injectée dans d'autres composants (dans ce cas, PlayerController
). .
import { Module } from "@nestjs/common";
import { PlayersService } from "./players.service";
import { PlayersController } from "./players.controller";
@Module({
controllers: [PlayersController],
providers: [PlayersService],
})
export class PlayersModule {}
PlayersController
est répertorié dans le tableau des contrôleurs à l'intérieur du fichier players.module.ts. NestJS crée également une instance de ce contrôleur lorsque le PlayerModule
est chargé. Comme mentionné précédemment, le PlayersController
dépend du PlayersService
comme spécifié dans le paramètre constructeur :
constructor(private readonly playersService: PlayersService) {}
Lorsque NestJS instancie le PlayerController
, il voit le paramètre constructeur et comprend immédiatement qu'il dépend de PlayersService
. NestJS recherche ensuite le PlayersService
dans le PlayersModule
et résout cette dépendance en créant une instance de PlayersService
et en l'injectant dans le PlayersController
. instance.
En règle générale, NestJS instancie d'abord PlayersService
car il s'agit d'une dépendance de PlayersController
. Une fois instancié, NestJS conserve l'instance de PlayersService
dans le conteneur d'injection de dépendances de l'application. Ce conteneur gère les instances de toutes les classes créées par NestJS et est la clé du système DI de Nest.
Lorsque l'instance PlayersService
est prête, Nest instancie le PlayersController
et injecte l'instance PlayersService
dans son constructeur. Cette injection permettra à PlayersController
d'utiliser PlayersService
afin qu'il puisse gérer les requêtes HTTP pour récupérer la liste des joueurs lorsque /players
est appelé. Demandez cet itinéraire et surveillez la réponse :
Le AppModule
importe le PlayersModule
. Ainsi, lorsque l'application démarre, NestJS charge et traite le PlayersModule
, analyse ses importations, ses contrôleurs, ses fournisseurs et ses exportations vers comprendre comment ils doivent être instanciés et se rapporter les uns aux autres.
@Module({
imports: [PlayersModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Lorsque vous effectuez une demande au point de terminaison /players
, Nest achemine la demande vers la méthode getPlayers()
du PlayersController
. Le contrôleur appelle à son tour la méthode getPlayers()
sur son instance de dépendance PlayersService
pour obtenir la liste des joueurs et renvoyer les données en réponse.
Remarque : Le tableau import
prend en compte la liste des modules importés.
Décorateurs
Le framework Nest utilise largement les décorateurs TypeScript pour améliorer sa modularité et la maintenabilité du code. Dans TypeScript, les décorateurs fournissent un moyen d'exploiter par programme le processus de définition d'une classe. Comme expliqué précédemment, lorsqu'une instance d'une classe est créée, les propriétés et méthodes des classes deviennent disponibles sur l'instance de classe. Les décorateurs vous permettent ensuite d'injecter du code dans la définition réelle d'une classe avant même la création de l'instance de classe.
Voici un exemple :
function exampleDecorator(constructor: Function) {
console.log("exampleDecorator invoked");
}
@exampleDecorator
class ClassWithExampleDecorator {}
Dans ce code, une fonction exampleDecorator
prend un seul paramètre constructor
et enregistre un message sur la console pour indiquer qu'elle a été invoquée. La fonction exampleDecorator
est ensuite utilisée comme fonction de décorateur de classe. Le ClassWithExampleDecorator
est annoté avec le exampleDecorator
en utilisant le symbole @
suivi du nom du décorateur. Lorsque vous exécutez ce morceau de code, vous obtenez ce résultat :
exampleDecorator invoked
Ici, vous pouvez voir que même sans créer une instance de ClassWithExampleDecorator
, la fonction exampleDecorator
a été invoquée. Idéalement, vous configureriez et exécuteriez cet exemple de code simple en créant un projet de nœud de base avec prise en charge de TypeScript. Vous pouvez en savoir plus sur les décorateurs dans TypeScript.
Dans NestJS, les décorateurs sont utilisés pour annoter et modifier les classes pendant la conception. Ils définissent également les métadonnées que NestJS utilise pour organiser la structure et les dépendances de l'application. Jetez un œil aux principaux décorateurs de NestJS :
Injectable
Le décorateur @Injectable()
est utilisé dans NestJS pour marquer une classe en tant que fournisseur pouvant être géré par le système NestJS DI. Il indique à NestJS que cette classe particulière est une dépendance et qu'elle peut être injectée par la classe qui l'utilise.
Lorsque vous annotez une classe dans NestJS avec le décorateur @Injectable()
, vous indiquez à NestJS que la classe particulière doit être disponible pour être instanciée et injectée en tant que dépendance là où elle est nécessaire. Le conteneur NestJS IoC gère toutes les classes d'une application NestJS marquées par @Injectable()
. Lorsqu'une instance de cette classe est nécessaire, NestJS examine le conteneur IoC et résout toutes les dépendances que la classe pourrait avoir et instancie la classe, si elle n'a pas encore été instanciée, puis fournit la classe instanciée là où elle est requise.
Jetez un œil à ce code :
@Injectable()
export class AppService {
getHello(): string {
return "Hello";
}
}
Ici, le fichier AppService
est marqué du décorateur @Injectable()
qui le rend disponible pour l'injection par le système NestJS DI. NestJS utilise une bibliothèque appelée bibliothèque reflect-metadata
pour définir les métadonnées de la classe AppService
(décorée par le @Injectable()
) afin qu'elle peut être géré par le système NestJS DI.
Ces métadonnées incluent les informations sur la classe, ses paramètres de constructeur (dépendances) et les méthodes que le système DI utilise pour résoudre et injecter les dépendances requises au moment de l'exécution.
Dans l'exemple ci-dessus, les métadonnées contiendront des informations sur la classe AppService
et sa méthode. En supposant que AppService
ait une dépendance transmise à sa méthode constructeur, les métadonnées incluront également ces informations.
Module
Le décorateur @Module()
fournit des métadonnées que NestJS utilise pour organiser la structure de l'application. Le décorateur @Module()
prend un objet qui peut avoir des propriétés telles que des importations, des contrôleurs, des fournisseurs et des exportations.
NestJS utilise le conteneur IoC pour l'injection de dépendances et la gestion des dépendances. Les prestataires sont enregistrés dans le conteneur et injectés dans leurs personnes à charge selon les besoins. Par exemple, AppService
sera enregistré et injecté dans le AppController
qui en a besoin.
Voici un exemple pour bien comprendre le décorateur @Module()
:
@Module({
imports: [PlayersModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Comme indiqué ici, il s'agit du module racine spécifié dans le fichier main.ts
lorsque vous démarrez l'application NestJS. Le module racine possède un module importé appelé PlayerModule
, un contrôleur AppController
et un fournisseur AppService
.
Lorsque vous démarrez votre application, Nest examine l'accessoire d'importation de AppModule
pour connaître les autres modules qui doivent être chargés. Dans ce cas, PlayersModule
est importé afin que NestJS charge et configure le PlayersModule
. Supposons que PlayersModule
ait également des modules importés qui sont transmis dans le tableau PlayersModule
imports
, NestJS chargera également ces modules de manière récursive afin que tous les modules et leurs dépendances sont chargés et configurés. Une fois tous les modules chargés, NestJS instancie les fournisseurs qui ont été spécifiés dans les propriétés des fournisseurs pour chaque module. Cela signifie que le AppService
et le PlayersService
seront instanciés et ajoutés au conteneur IoC. Ensuite, NestJS gère l'injection de dépendances en injectant des fournisseurs dans les contrôleurs et les services à l'aide de l'injection de constructeur.
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
@Injectable()
export class AppService {
getHello(): string {
return "Hello";
}
}
Comme mentionné précédemment, NestJS injecte le AppService
qui est un fournisseur dans le AppController
à l'aide de l'injection du constructeur. La même chose se produira entre PlayersService
et PlayersControllers
.
Une fois les dépendances injectées, Nest initialise les contrôleurs spécifiés dans la propriété du contrôleur de chaque module pour gérer les demandes entrantes et renvoyer les réponses.
Manette
Le décorateur @Controller()
dans NestJS est utilisé pour définir et organiser les itinéraires et la logique de gestion des demandes dans votre application. Les contrôleurs aident à séparer la gestion des requêtes HTTP de la logique métier de l'application, ce qui rend la base de code plus modulaire et maintenable.
Lorsque vous décorez une classe avec @Controller()
, vous fournissez des métadonnées à NestJS qui indiquent que la classe sert de contrôleur. Nest, à son tour, inspectera les méthodes au sein des contrôleurs et recherchera des décorateurs de méthodes HTTP comme @Get()
, @Post()
etc. NestJS crée une table de routage interne basé sur les décorateurs appliqués aux méthodes du contrôleur. Cette table de routage mappe les requêtes entrantes aux méthodes de contrôleur appropriées en fonction de la route demandée et de la méthode HTTP. Par exemple, en fonction de votre base de code actuelle, si vous effectuez une requête GET à localhost:3000
, la table de routage mappe votre requête GET entrante au contrôleur approprié qui dans ce cas est AppController
. Il recherche le contrôleur et recherche le décorateur @Get()
, traite la requête, interagit avec sa dépendance appService
et renvoie une réponse.
Remarque : Le champ imports
utilisé dans les métadonnées du module sert à importer des modules internes et externes. Lorsque vous importez un module, vous importez le contexte du module qui inclut ses fournisseurs, contrôleurs et entités exportées. Cela vous permet de composer votre application pour qu'elle soit modulaire et maintenable.
Métadonnées des composants de journalisation
Voici comment enregistrer les métadonnées d’un module :
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//log metadata
const metadata = Reflect.getMetadataKeys(AppModule);
console.log(metadata);
await app.listen(3000);
}
bootstrap();
Dans ce code, l'objet Reflect
, qui fournit des capacités de réflexion pour inspecter les métadonnées, est utilisé pour obtenir les clés de métadonnées associées au AppModule
à l'aide de getMetadataKeys
méthode.
Le journal résultant renvoie un tableau des valeurs clés du décorateur de module :
[ 'imports', 'controllers', 'providers' ]
D'autres méthodes peuvent être appelées sur l'objet Reflect
comme : getMetadata
, getOwnMetadata
, getOwnMetadataKeys
.
Dans la propriété imports
du module :
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//log metadata
const metadata = Reflect.getMetadataKeys(AppModule);
console.log(metadata);
for (const key of metadata) {
if (key === "imports") {
const imports = Reflect.getMetadata(key, AppModule);
console.log("Imports", imports);
}
}
await app.listen(3000);
}
Ici, vous obtenez les clés de métadonnées associées au AppModule
, puis parcourez ces clés et vérifiez si l'une des métadonnées à l'intérieur du tableau est égale aux importations. Si une clé nommée imports
est trouvée, vous utilisez alors la fonction getMetadata
pour obtenir le tableau des modules importés. Vous pouvez faire de même pour les contrôleurs, les fournisseurs et les entités exportées en modifiant la valeur de la clé.
Dépannage
Jetez un œil à ce code :
import { Module } from "@nestjs/common";
import { PlayersService } from "./players.service";
import { PlayersController } from "./players.controller";
@Module({
controllers: [PlayersController],
providers: [],
})
export class PlayersModule {}
Lorsque vous exécutez ce code, vous remarquerez l'erreur suivante :
Cette erreur est courante pour les développeurs qui découvrent NestJS. Cette erreur indique qu'il ne peut pas résoudre la dépendance de PlayersControllers
. La dépendance dans cette instance est le PlayersService
injecté via l'injection du constructeur. Pour résoudre cette erreur, vérifiez si le PlayersModule
est un module NestJS valide. Ensuite, puisque PlayersService
est un fournisseur, vérifiez s'il est répertorié comme faisant partie des fournisseurs dans le PlayersModule
. La troisième option consiste à vérifier si un module tiers fait partie du module importé dans l'AppModule
.
Conclusion
Dans ce tutoriel, vous avez appris les bases de l'injection de dépendances, de l'inversion de contrôle et comment elles s'appliquent au contexte de NestJS. Vous avez également appris ce que sont les décorateurs et ce qu’ils signifient chaque fois qu’ils sont utilisés pour décorer une classe.
Vous trouverez le code source complet de ce tutoriel ici sur GitHub.