NestJS : l'architecture Angular au service de vos applications NodeJS

Alors que Java et PHP continuent de prédominer sur le marché des applications web, le besoin récurrent de proposer des produits toujours plus innovants avec un time-to-market réduit nécessite quelquefois de se tourner vers des solutions de développement plus accessibles afin de sortir une application en un temps record.

Parmi ces nouvelles solutions, deux s’en dégagent nettement : Python avec Django et JavaScript avec NodeJS.

Lorsque l’on parle de NodeJS, l’exemple typique que l’on utilise est l’application de chat car elle permet d’illustrer deux des avantages de cette solution : sa simplicité avec JavaScript permettant d’écrire rapidement un service de websocket et son module de clustering, facilitant la mise à l’échelle horizontale de l’application (une application NodeJS étant monothread, le module de clustering multiplie les instances de l’application pour répartir au mieux sa charge globale sur un CPU multicoeur).

Avec Node, on peut alors créer rapidement une application nécessitant peu de calculs et pouvant accueillir un nombre de requêtes à la seconde très conséquent.

Cependant, dans un contexte de production, réaliser une application fonctionnelle et performante n’est pas suffisant. Il faut également s’assurer que son code soit correctement écrit, structuré et testé.

Dans cette optique, de nombreux frameworks Node ont vu le jour, chacun répondant plus ou moins à ces critères. Parmi les plus connus, nous pouvons citer :

  • Express: Développé par la fondation NodeJS, Express peut être vu comme la principale boîte à outils de Node car en plus d’être la solution la plus populaire c’est aussi la base de la quasi-totalité des frameworks Node alternatifs.
  • Loopback: Développé par StrongLoop, une société IBM,  Loopback est un framework spécialisé dans le création d’API REST. Son principal point fort étant son générateur de fichiers Typescript qui permet de générer toutes les couches de l’API à partir de la description de l’entité (comme le JDL de JHipster).
  • SailsJS: Permet d’écrire une application web JavaScript complète en se basant sur l’architecture MVC. Comme Loopback, l’idée derrière Sails est de proposer une solution calibrée pour du développement en entreprise par la génération automatique de fichiers.

Bien que ces solutions soient généralement une bonne aide pour aboutir à un projet de qualité, elles ne s'attardent pas toujours sur un aspect essentiel : proposer une architecture permettant de garder un projet correctement structuré quelle que soit sa taille.

Fort heureusement, il existe un framework tentant de répondre à cette problématique : NestJS.

La mise à l’échelle par l’architecture Angular.

Prenons un exemple de projet fraichement généré avec Express. Voici la structure du projet :

Et voici le point d’entrée de l’application :

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
 next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
 // set locals, only providing error in development
 res.locals.message = err.message;
 res.locals.error = req.app.get('env') === 'development' ? err : {};

 // render the error page
 res.status(err.status || 500);
 res.render('error');
});

module.exports = app;

Que ce soit au niveau de son arborescence ou de son point d’entrée, on note que le projet est globalement basique et peu structuré.

Avec cette stratégie, n’importe quel développeur va pouvoir s’approprier le code rapidement pour y faire des ajouts. Cependant, partir d’un ensemble faiblement structuré risque de desservir l’évolution du projet car d’une part, il faudra s’assurer que sa structure reste cohérente à chaque ajout de fonctionnalités et d’autre part, il faudra factoriser le code du point d’entrée afin de ne pas accentuer son aspect fourre-tout (où se mélangent configuration globale du projet, gestion des routes et gestion des erreurs).

Pour limiter ce problème, il convient alors de définir des règles de structure et de nommage en amont de la réalisation du projet. Mais étant donné qu’il n’existe pas de consensus sur la stratégie à adopter, ces règles sont tacites et peuvent donc être facilement transgressées à chaque arrivée d’une fonctionnalité majeure ou d’une nouvelle personne sur le projet.

Idéalement, il faudrait un système qui soit suffisamment rigide pour borner explicitement la structure d’un projet NodeJS/Express tout en restant suffisamment simple pour ne pas alourdir inutilement les développements.

Dans le monde du front-end JS, le framework Angular propose une solution à ce problème.

Pour créer une bonne application Angular, on subdivise notre base de code sous la forme de modules (les NgModules) dans lesquels on va pouvoir déclarer des scopes afin de définir quelle portion de code est accessible depuis l’extérieur du module et quelle portion est réservée à un usage interne. L’application Angular ne sera alors plus hiérarchisée selon un ensemble de fichiers mais selon une succession de modules, ce qui permet d’augmenter facilement le nombre de fonctionnalités tout en ayant un code correctement isolé.

Avec les NgModules vient aussi l’injection de dépendances (ou DI). La DI permet d’instancier un service une unique fois au cours du cycle de vie de l’application. On peut donc écrire du code métier facilement accessible et réutilisable dans toute l’application.

En combinant modules et DI, Angular propose alors une structure rigoureuse, incitant le développeur à constamment réfléchir sur la manière dont il doit agencer son code afin de gérer aussi facilement une application possédant une dizaine de composants / services qu’une application en possédant une centaine.

Mais quel rapport avec NestJS ?

L’idée de NestJS est tout simplement de faciliter l’accès au framework Express en proposant une couche d’abstraction reposant sur l'architecture fondamentale d’Angular : les modules et l’injection de dépendances. Avec ce principe, NestJS est alors capable de proposer une plateforme de développement NodeJS que l’on peut facilement maintenir et mettre à l’échelle.

Par ailleurs, si ce framework se base sur l’architecture Angular, il est totalement agnostique sur le front-end ce qui signifie que l’on peut combiner cette solution avec un moteur de templating (ejs, hbs, pug, etc...) ou une application web (Angular, React, Vue, etc…).

Mise en pratique

Voici, la structure de base d’un projet NestJS :

Avec le point d’entrée suivant (main.ts), où AppModule est le module d’entrée de l’application :

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 await app.listen(3000);
}
bootstrap();

Plutôt simple n’est-ce pas ? Admettons à présent que nous voulons créer une ressource REST avec un controller et un service.

Pour le service, nous aurons une classe munie du decorator @Injectable, permettant d’écrire une classe injectable :

import { Injectable } from '@nestjs/common';

@Injectable()
export class GreetingsApplicationService {
   greet() {
       return {
           message: 'Hello World !',
       };
   }
}

Du côté du controller nous aurons une classe annotée @Controller pour pouvoir faire une classe injectable qui va gérer l’endpoint de notre ressource :

import { Controller, Get } from '@nestjs/common';
import { GreetingsApplicationService } from './geetings.service';

@Controller('greetings')
export class GreetingsController {
   constructor(private readonly greetingsApplicationService: GreetingsApplicationService) {}

   @Get()
   greet() {
       return this.greetingsApplicationService.greet();
   }
}

Ici, notre ressource sera accessible dans une requête GET sur le endpoint /greetings. Afin de profiter au mieux de la modularité de notre application, on crée un module dédié :

import { Module } from '@nestjs/common';
import { GreetingsApplicationService } from './greetings.service';
import { GreetingsController } from './greetings.controller';

@Module({
   controllers: [GreetingsController],
   providers: [GreetingsApplicationService],
})
export class GreetingsModule {}

Que l’on injecte finalement dans le module principal :

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GreetingsModule } from './greetings/grettings.module';

@Module({
 imports: [GreetingsModule],
 controllers: [AppController],
 providers: [AppService],
})
export class AppModule {}

Ce qu’il faut surtout retenir de cet exemple, c’est la manière dont sont déclarés et agencés les services et les controllers. Car si on a fait le choix de présenter ici un exemple de service REST, sachez que cette structure sera aussi valable si vous décidez de faire de la génération de vues, des Websockets ou du GraphQL.

Si les modules et la DI sont la pierre angulaire de NestJS, l'exemple ci-dessus montre que le framework reprend également le concept des Decorators et des Pipes. Ces éléments permettent de valider ou transformer de la donnée en amont d’une méthode, ce qui participe à la simplification du code.

D'une manière générale, si NestJS reprend autant de concepts Angular, c'est qu'ils proviennent de pratiques qui ont déjà fait leur preuves sur d'autres frameworks (par exemple, la DI et les Decorators sont courrament utilisés sur Spring). Ainsi, l'adoption de NestJS est globalement aisée.

Grâce à sa modularité, NestJS propose aussi un mode de fonctionnement distribuant les modules d'une application sous la forme de microservices :

Sans avoir la robustesse et la maturité d’une stack telle que Spring Cloud Netflix (pas d’équivalence à Eureka ou Zull en JS par exemple), cette solution permet de générer rapidement des microservices NodeJS bien architecturés. NestJS supporte aussi quelques messages brokers dont MQTT, Redis ou RabbitMQ.

Dépendances modulaires pour applications modulaires

En observant l’exemple précédent, vous avez sûrement remarqué qu’un projet NestJS semble bien plus avare en fonctionnalités qu’un projet Express fraîchement généré. Cette stratégie est voulue et correspond à un besoin particulier lorsque l’on fait du JS : éviter de surcharger le projet en dépendances.

Très souvent, écrire une application avec un framework revient à embarquer un nombre impressionnant de dépendances dans le dossier node_modules :

Les développeurs de NestJS étant bien conscients de ce problème, ils ont adopté une stratégie où le projet de base contient le minimum vital pour écrire une application (Express inclus, ce qui est déjà beaucoup) et où chaque brique supplémentaire devra être ajoutée une à une afin que le développeur puisse garder la maîtrise des dépendances.

Ces briques sont, pour la plupart, des bibliothèques Node très populaires, pour lesquelles NestJS fournit des services injectables afin de pouvoir correctement les interfacer avec le code de l’application.

Parmis les extensions disponibles, on va trouver :

  • Des ORM/ODM avec TypeORM (pour le SQL/NoSQL) et Mongoose (pour Mongo).
  • Passport, pour la gestion de l'authentification.
  • Axios, pour faire des requêtes HTTP.
  • Socket.io pour le support des websocket.
  • Appolo et Prisma pour le support de GraphQL.
  • Swagger pour la documentation API.
  • Compodoc. Initialement créé pour Angular, cet outil permet de générer la documentation de votre application sous la forme d’une plateforme web. Si cet outil vous est inconnu, je vous invite très fortement à le découvrir.

Évidemment, le problème de ce système est que si le service injectable pour votre bibliothèque favorite est inexistant, il faudra alors le créer.

L’accessibilité avant tout

Au delà de la problématique d’architecture, l’objectif de NestJS est avant tout d’offrir une solution permettant de faire une maintenabilité globale d’une application NodeJS.

Dans ce contexte, on va donc retrouver :

  • TypeScript : une surcouche syntaxique permettant de faire du JavaScript typé afin de normer explicitement le code et faire de la détection d’erreur avant le runtime.
  • Jest : une plateforme de TU/TI développée par Facebook, plus d’info ici.
  • RxJS : La bibliothèque spécialisée dans la programmation réactive par le biais d’observables.
  • NestJS peut être également étendu avec Webpack pour faire du hot-reload.

L’inspiration de NestJS pour Angular se retrouve également dans le CLI qu’il met à disposition puisqu’en quelques lignes on peut générer un module complet :

nest g module greetings

nest g controller greetings

nest g service greetings

Le petit plus, c’est qu’en embarquant d’office Jest, le framework génère en même temps les fichiers sources et les fichiers de tests directement prêts à l’emploi.

Si vous avez l’habitude de travailler en TDD, vous aurez donc la possibilité d’écrire vos tests unitaires en amont de vos développements :

import { Test, TestingModule } from '@nestjs/testing';
import { GreetingsApplicationService } from './greetings.service';

describe('GreetingsService', () => {
 let service: GreetingsApplicationService;

 beforeEach(async () => {
   const module: TestingModule = await Test.createTestingModule({
     providers: [GreetingsService],
   }).compile();

   service = module.get<GreetingsApplicationService>(GreetingsApplicationService);
 });

 describe('greet', () => {
   it('should retrieve a greetings message', () => {
     const expectedGreet = {
       message: 'Hello World !',
     };
     const greet = service.greet();
     expect(expectedGreet).toEqual(greet);
   });
 });
});

En revanche, le générateur de fichiers de NestJS est bien plus limité que celui de Loopback ou Sails (pas de génération automatique d’API ou de page web) ce qui nécessite d’écrire plus de code manuellement.

NestJS, une solution pour des projets à grande échelle

Avec une bonne popularité et un support actif (16K+ étoiles sur le repo Github avec des releases hebdomadaires), l’orientation minimaliste de NestJS et sa scalabilité semblent être une solution de choix dès lors que l’on veut aborder NodeJS.

Mais pour chaque projet vous aurez besoin de mettre en relation vos objectifs de qualité avec le temps que vous allez allouer à vos développements.

Par exemple, si votre objectif est simplement d’écrire une API REST, adopter Loopback et son générateur d’entités vous permettra d’arriver à vos fins plus rapidement et tout aussi efficacement. De la même manière, Sails et son très grand panel d’outils permettent de produire une plateforme web en très peu de temps.

Alors, que faire avec NestJS ?

Et bien comme nous l’avons vu au cours de cet article, l'intérêt de ce framework se situe sur des projets où la rapidité du développement peut être mise un peu en retrait au profit d’une meilleure maintenabilité. Le cas typique serait le développement d’une application avec une base fonctionnelle simple qu’il faudra régulièrement incrémenter tout en assurant une bonne robustesse technique.

De plus, NestJS est également un très bon framework pour débuter dans le développement d’application NodeJS. En reprenant les concepts Angular, NestJS peut à la fois intéresser les développeurs front-end souhaitant s’initier au développement back-end, mais aussi les développeurs Java/PHP qui voudraient explorer une solution de développement simplifiée.

Si vous désirez approfondir le sujet, je vous renvoie vers la documentation officielle.

Pour finir, si vous souhaitez voir un projet NestJS en action, je vous propose une application exemple qui génère une API REST enrichie avec de nombreuses briques (Authentification, entités SQL, validation de formulaires, confs par environnement, etc...)  afin d’avoir un aperçu des possibilités offertes par ce framework.