# NestJS en production — injection de dépendances et structuration d'un backend TypeScript > Retour sur notre utilisation de NestJS 7 en production : injection de dépendances inspirée d'Angular, structuration modulaire, custom providers, guards injectables, et comparaison avec les patterns équivalents en Java (Spring, Guice). Exemples tirés de l'API centrale MyMove. Date : 22/09/2020 Auteur : Aurélien N. Tags : TypeScript, NestJS, Node.js, Architecture, Backend --- Dans l'[article sur MyMove](/blog/mymove-comparateur-vtc-microservices-cloud-run), j'ai effleuré le sujet de l'API centrale NestJS sans creuser. Pourtant c'est le morceau du projet qui m'a le plus fait réfléchir. Pas parce que le code est compliqué — au contraire, NestJS rend les choses assez naturelles une fois qu'on a compris le modèle. Mais parce que retrouver côté backend les patterns que je connaissais d'Angular (et avant ça, de Spring) m'a obligé à repenser la façon dont je structure une API Node.js. Cet article revient sur l'injection de dépendances dans NestJS et la structuration modulaire qu'elle impose, avec des exemples tirés du code de MyMove et une comparaison avec ce qui se fait en Java. ## NestJS 7 : le contexte NestJS 7 est sorti en mars 2020. Le framework existe depuis fin 2017, mais c'est en 2019 que l'adoption a vraiment décollé — les téléchargements npm ont quadruplé en un an. La version 7 apporte un module GraphQL réécrit de zéro (plus de dépendance sur `type-graphql`), des pipes de validation améliorés, et surtout un système de décorateurs custom qui reçoivent maintenant un `ExecutionContext`, ce qui unifie l'API entre REST et GraphQL. Pour MyMove, on est sur du REST pur avec TypeORM et MySQL. Pas de GraphQL. Mais les fondamentaux — modules, providers, injection de dépendances — sont les mêmes quel que soit le transport. ## L'injection de dépendances, d'Angular au backend ### Le modèle Angular Angular a popularisé l'injection de dépendances dans l'écosystème JavaScript. Le principe : au lieu d'instancier ses dépendances soi-même, on déclare ce dont on a besoin dans le constructeur, et le framework se charge de fournir les instances. ```typescript // Angular — un service déclare ses dépendances dans le constructeur @Injectable() constructor(private http: HttpClient) } ``` L'injecteur d'Angular est hiérarchique : il suit l'arbre des composants. Un service déclaré dans un module parent est disponible dans tous les modules enfants. Un service déclaré dans un composant est instancié une fois par composant. Ce modèle a du sens dans une UI où la hiérarchie des composants est un arbre. ### L'adaptation NestJS NestJS reprend le même mécanisme, mais adapté au backend. Pas d'arbre de composants — à la place, un graphe de modules. Chaque module déclare ses providers (les services qu'il instancie) et ses exports (les services qu'il rend disponibles aux autres modules). ```typescript @Module() ``` L'`AuthModule` importe `UsersModule` et `StripeModule` pour accéder à leurs services exportés. Il déclare `CurrentCustomerService` comme provider et l'exporte pour que d'autres modules puissent l'injecter. Le conteneur IoC de NestJS résout le graphe de dépendances au démarrage et instancie tout dans le bon ordre. Côté service, le pattern est identique à Angular : ```typescript @Injectable() constructor( @InjectRepository(SearchEntity) private searchesRepository: Repository, private providerResultsService: ProviderResultsService, private connection: Connection, private logger: Logger, ) } ``` `SearchesService` déclare quatre dépendances dans son constructeur. Le conteneur injecte le repository TypeORM (via le token `@InjectRepository`), le `ProviderResultsService` (résolu par type), la `Connection` TypeORM, et le `Logger`. Aucune instanciation manuelle. Aucun `new`. ### Ce qui change par rapport à Angular La différence principale, c'est le cycle de vie. Angular crée et détruit des composants en permanence au fil de la navigation. NestJS, par défaut, instancie chaque provider une seule fois au démarrage (scope singleton) et le réutilise pour toutes les requêtes. Mais il y a des cas où on a besoin d'un état par requête. Dans MyMove, le `CurrentCustomerService` est scopé à la requête HTTP : ```typescript @Injectable() private _customer?: Stripe.Customer constructor( @Inject(REQUEST) private request: any, private stripeService: StripeService, private usersService: UsersService, private logger: Logger, ) async fetchOrCreateCustomer(): Promise { if (!this._customer) { const = this.request this._customer = (await this.fetchExistingCustomer()) || (await this.createNewCustomerForUser(user)) } return this._customer } } ``` `Scope.REQUEST` instancie un nouveau `CurrentCustomerService` à chaque requête HTTP. Le service injecte l'objet `REQUEST` pour accéder à l'utilisateur authentifié, puis cache le client Stripe pour éviter de faire deux appels API dans la même requête. C'est l'équivalent du scope `request` de Spring. ## Comparaison avec Spring et Guice J'ai bossé sur des projets Java avant de passer au full-stack TypeScript. La comparaison s'impose. ### Spring : convention over configuration Spring Boot, c'est la référence de l'injection de dépendances en Java. Le mécanisme repose sur le component scanning : Spring parcourt le classpath, trouve les classes annotées `@Component`, `@Service`, `@Repository`, et les enregistre automatiquement dans le conteneur. ```java // Spring Boot — le composant est découvert par scanning @Service public class SearchesService { private final SearchesRepository searchesRepository; private final ProviderResultsService providerResultsService; // injection par constructeur (recommandée depuis Spring 4.3) public SearchesService( SearchesRepository searchesRepository, ProviderResultsService providerResultsService ) } ``` Avec Spring, pas besoin de déclarer explicitement les providers dans un module. Le scanner les trouve. C'est puissant quand le projet grossit, mais ça rend le wiring implicite — il faut connaître les conventions pour comprendre d'où vient une dépendance. NestJS est plus explicite. Chaque module déclare ses imports, ses providers et ses exports. On voit le graphe de dépendances en lisant les décorateurs `@Module()`. C'est plus verbeux, mais il n'y a pas de magie. ### Guice : le binding explicite Google Guice prend l'approche inverse de Spring. Pas de scanning, pas de convention — tout est déclaré dans des modules programmatiques : ```java // Guice — on déclare les bindings dans un module public class SearchesModule extends AbstractModule { @Override protected void configure() @Provides Connection provideConnection(DataSource dataSource) } ``` NestJS se situe entre les deux. Plus explicite que Spring (on déclare les providers par module), moins verbeux que Guice (pas besoin de `bind().to()`, le type suffit dans la plupart des cas). Le meilleur des deux mondes, à mon avis. ### Le tableau comparatif | | NestJS | Spring Boot | Guice | |---|---|---|---| | **Enregistrement** | `providers` dans `@Module()` | Component scanning | `bind().to()` dans `AbstractModule` | | **Injection** | Constructeur + `@Inject()` | Constructeur (+ `@Autowired`) | Constructeur + `@Inject` | | **Modules** | `@Module()` avec imports/exports | `@Configuration` + auto-config | `AbstractModule.configure()` | | **Scopes** | Singleton, Request, Transient | Singleton, Prototype, Request, Session | Singleton, no-scope | | **Custom providers** | `useClass`, `useValue`, `useFactory` | `@Bean` dans `@Configuration` | `@Provides`, bindings | | **Discovery** | Explicite | Scanning implicite | Explicite | ## Structuration du projet : les modules comme frontières Ce que j'apprécie dans NestJS, c'est que la structuration n'est pas optionnelle. Le framework impose des modules, et ces modules créent des frontières dans le code. ### L'AppModule comme chef d'orchestre Dans MyMove, l'`AppModule` est un point d'entrée minimal qui assemble les modules métier : ```typescript @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => (), }), StripeModule, UsersModule, OrdersModule, AuthModule, NotificationsModule, SearchesModule, ValidationCodeModule, ], controllers: [ UsersController, CardsController, OrdersController, SearchesController, ], }) ``` Deux patterns intéressants ici. D'abord, `ConfigModule.forRoot()` avec `isGlobal: true` : le module de configuration est enregistré une seule fois et accessible partout sans import explicite. C'est le pattern des modules globaux — à utiliser avec parcimonie, mais la config en est un cas légitime. Ensuite, `TypeOrmModule.forRootAsync()` avec `useFactory` : la connexion base de données est configurée dynamiquement à partir du `ConfigService`. Le `inject: [ConfigService]` dit au conteneur d'injecter le service de config dans la factory. C'est l'équivalent du `@Bean` de Spring ou du `@Provides` de Guice. ### Un module métier : SearchesModule ```typescript @Module() ``` `TypeOrmModule.forFeature()` enregistre le repository pour `SearchEntity` dans ce module uniquement. Le `SearchesService` est le seul provider métier. Il est exporté pour que d'autres modules puissent l'utiliser. La structure de fichiers suit le module : ``` searches/ ├── searches.module.ts # déclaration du module ├── searches.service.ts # logique métier ├── searches.controller.ts # routes HTTP ├── dao/ │ └── search.entity.ts # entité TypeORM ├── dto/ │ ├── selection.dto.ts # validation des entrées │ ├── place.dto.ts │ ├── provider-result.dto.ts │ └── IsGreaterThan.ts # validateur custom └── interfaces/ └── search.interface.ts # contrats TypeScript ``` Chaque module est un répertoire autonome. Si je dois toucher la logique de recherche, tout est au même endroit. Pas de `services/` global, pas de `models/` fourre-tout. ## Les patterns utiles au quotidien ### Controllers minces, services épais Les controllers NestJS ne font que du routage HTTP. Zéro logique métier : ```typescript @UseGuards(JwtAuthGuard) @Controller("searches") constructor(private searchesService: SearchesService) @Get() async list(@CurrentUser() user: User, @Query("all") all?: boolean) @Put(":searchID") async saveSelection( @CurrentUser() user: User, @Param("searchID") searchID: number, @Body() selection: SelectionDto, ) } ``` Le controller reçoit la requête, extrait les paramètres via des décorateurs (`@CurrentUser()`, `@Param()`, `@Body()`), et délègue au service. Rien d'autre. La validation est gérée par le `ValidationPipe` global et les DTOs. L'authentification est gérée par le guard `JwtAuthGuard`. Ce pattern est identique à ce qu'on fait dans un controller Spring `@RestController`. Le fait que NestJS l'impose structurellement (un controller ne peut pas accéder à un repository directement s'il n'est pas dans ses providers) évite la dérive du "controller qui fait tout". ### Guards injectables : quand la sécurité participe au DI Les guards NestJS ne sont pas de simples middlewares. Ce sont des classes injectables avec leurs propres dépendances : ```typescript @Injectable() constructor(private currentCustomerService: CurrentCustomerService) async canActivate(context: ExecutionContext) } ``` Le `LoadStripeCustomerGuard` injecte le `CurrentCustomerService` (qui est lui-même scopé à la requête). Avant d'atteindre le controller, le guard charge ou crée le client Stripe et l'attache à la requête. Si l'opération échoue, le guard retourne `false` et la requête est rejetée. On empile les guards sur un endpoint : ```typescript @UseGuards(JwtAuthGuard, LoadStripeCustomerGuard) @Post() async createOrder(@StripeCustomer() customer: Stripe.Customer) ``` C'est comparable aux `HandlerInterceptor` de Spring ou aux filtres de servlet, mais avec l'injection de dépendances intégrée. Pas besoin de lookup dans un contexte global. ### DTOs et validation déclarative La validation des entrées passe par des DTOs avec des décorateurs `class-validator` : ```typescript @ValidateNested() readonly selectedResult: ProviderResultDto @ValidateNested() readonly otherResults: ProviderResultDto[] @IsPositive() @IsOptional() readonly co2EmissionsGrams?: number @IsOptional() @ValidateNested() readonly fromPlace?: PlaceDto @IsOptional() @ValidateNested() readonly toPlace?: PlaceDto } ``` Le `ValidationPipe` global valide automatiquement chaque `@Body()` contre les décorateurs du DTO. Si la validation échoue, NestJS retourne une 400 avec les détails. Zéro code de validation dans le controller. On peut même écrire des validateurs custom : ```typescript @ValidatorConstraint() implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) } ``` L'équivalent en Spring, c'est les annotations de Bean Validation (`@NotNull`, `@Min`, `@Valid`) avec un `@Validated` sur le controller. Le fonctionnement est quasi identique — NestJS a clairement calqué l'approche. ### Custom decorators : extraire la plomberie Les décorateurs de paramètres custom permettent d'extraire les informations de la requête de manière réutilisable : ```typescript (_data: unknown, ctx: ExecutionContext) => , ) ``` Au lieu de `@Req() req` suivi de `req.user` dans chaque méthode de controller, on écrit `@CurrentUser() user: User`. Le controller ne sait même pas que l'utilisateur vient de l'objet request — il reçoit un `User` typé. ## Ce que ça change pour les tests L'injection de dépendances rend le mocking trivial. Pour tester `SearchesService` en isolation, on remplace ses dépendances par des mocks : ```typescript const module = await Test.createTestingModule({ providers: [ SearchesService, { provide: getRepositoryToken(SearchEntity), useValue: , }, { provide: ProviderResultsService, useValue: , }, { provide: Connection, useValue: , }, { provide: Logger, useValue: , }, ], }).compile() ``` Le `provide/useValue` remplace un provider par un mock. Le `SearchesService` reçoit des faux repositories et services sans savoir qu'ils sont faux. C'est le même pattern que `@MockBean` de Spring Boot Test ou les `bind().toInstance()` de Guice en test. Sans injection de dépendances, tester un service qui parle à MySQL, Stripe et Firebase en isolation, c'est un cauchemar de monkey-patching. Avec, c'est trois lignes de mock par dépendance. Pour les tests end-to-end, on monte l'application complète et on tape dessus avec `supertest` : ```typescript const app = await NestFactory.create(AppModule) await app.init() const response = await request(app.getHttpServer()) .get("/api/searches") .set("Authorization", `Bearer $`) .expect(200) ``` L'application démarre avec toutes ses vraies dépendances (ou avec des modules de test qui overrident la config base de données). On teste le wiring complet, les guards, la validation, la sérialisation. ## L'interface comme contrat : leçons du SDK MySam En parallèle de l'API NestJS, j'ai écrit un SDK TypeScript pour l'API MySam. Le SDK n'utilise pas NestJS, mais il applique le même principe d'injection via une interface : ```typescript get(path: string, params?: any): Promise getBinary(path: string, params?: any): Promise post(path: string, params?: any): Promise put(path: string, params?: any): Promise } ``` Le `APIClient` accepte soit un objet `` (et il crée un client Axios), soit directement un `RESTClient` custom : ```typescript constructor(params: APIClientParams) } ``` Ce pattern permet de tester chaque endpoint du SDK sans faire de requêtes HTTP : on passe un `RESTClient` mock qui retourne des réponses prédéfinies. L'injection est manuelle (pas de conteneur IoC), mais le principe est identique à ce que fait NestJS avec ses providers. C'est aussi ce que fait Guice à plus grande échelle : on définit une interface, on bind une implémentation, et le code consommateur ne connaît que l'interface. La différence, c'est que TypeScript efface les interfaces au runtime. Dans NestJS, quand on a besoin d'injecter par interface plutôt que par classe, on passe par des tokens d'injection : ```typescript // token symbolique const REST_CLIENT = Symbol("REST_CLIENT") // dans le module providers: [ , ] // dans le service constructor(@Inject(REST_CLIENT) private restClient: RESTClient) ``` C'est plus verbeux, mais ça préserve l'inversion de dépendance. Le service ne connaît que `RESTClient`, pas `AxiosClient`. ## Ce qu'on en retient NestJS n'a pas inventé l'injection de dépendances. Spring le fait depuis 2003, Guice depuis 2007, Angular depuis 2016. Ce que NestJS fait, c'est amener ce modèle dans l'écosystème Node.js, avec TypeScript comme langage de premier plan. Pour un projet comme MyMove, une API qui orchestre Stripe, des comptes utilisateurs, du push Firebase et des réservations chez un fournisseur tiers, la structuration modulaire n'est pas du luxe. Un service ne connaît que les interfaces de ses dépendances. Remplacer Stripe par un autre PSP, c'est changer un provider dans un module. Tester un service en isolation, c'est trois lignes de mock par dépendance, pas du monkey-patching. Et le graphe de dépendances est lisible directement dans les `@Module()`, sans scanning implicite à deviner. Un développeur qui connaît Angular ou Spring retrouve ses repères en quelques heures. Si vous venez d'Express et que vous commencez à souffrir du manque de structure sur un projet qui grossit, c'est une porte d'entrée naturelle, sans changer de langage.