Construire Un Serveur Backend Robuste Avec NestJS đ§
Il est temps dâen parler⊠Pas Next, ni Nuxt, mais Nest !
P r o l o g u e
RĂ©cemment, je me suis challengĂ© pour mettre en Ćuvre une solution backend robuste en JavaScript. Beaucoup de dĂ©veloppeurs ont pour prĂ©jugĂ© de croire que le JavaScript nâest pas assez sĂ©curisĂ©, performant ou tout simplement organisĂ© pour gĂ©rer des donnĂ©es via des APIs. Je vais vous persuader du contraireâŠ
Pour ce nouveau projet, jâai choisi des technologies qui ont le vent en poupe, notamment PostgreSQL mais surtout NestJS. Ainsi, jâai abandonnĂ© ma base de donnĂ©es de toujours quâest MongoDB, au profit dâune base de donnĂ©es plus traditionnelle.
NB : JâĂ©tais un peu rouillĂ© au dĂ©but, mais le retour Ă la base de donnĂ©es relationnelle Ă©tait une vraie bonne idĂ©e ; ne serait-ce que pour rĂ©cupĂ©rer des donnĂ©es dans plusieurs tables en un appel, au contraire du NoSQL qui nĂ©cessite autant dâappels quâil y a de documents Ă interroger⊠Finalement, le SQL câest comme le vĂ©lo, cela ne sâoublie pas đ
NestJS est un framework permettant de crĂ©er des applications serveurs (propulsĂ©es par NodeJS) Ă haute scalabilitĂ©. Ce dernier sâappuie notamment sur TypeScript et combine trois paradigmes de dĂ©veloppement :
- La Programmation Orientée Objet #POO
- La Programmation Fonctionnelle #PF
- La Programmation RĂ©active Fonctionnelle #PRF
NestJS part du constat que les frameworks Web (notamment ceux orientĂ©s composants), ne sont pas assez structurĂ©s⊠Sauf un : Angular ! Il sâinspire donc de celui-ci afin de fournir une librairie complĂšte pour les dĂ©veloppements backend.
NB : En effet, Angular dispose dâune organisation similaire, mais contrairement Ă NestJS, la structure de celui-ci est (Ă mon avis) plus contraignante quâautre chose⊠Je prĂ©fĂšre ses homologues React et Vue (et Svelte) qui offrent plus de flexibilitĂ© dans la conception dâune application frontend. Cependant, je suis persuadĂ© que cette âlourdeâ architecture peut ĂȘtre une aide significative Ă ceux qui dĂ©butent en dĂ©veloppement frontend.
Enfin, NestJS encapsule (au choix) le moteur ExpressJS ou celui de Fastify, pour livrer un serveur HTTP robuste et efficace. On peut donc parler dâun framework de frameworks (meta-framework !? đ€) Pour ce projet, jâai choisi dâutiliser Fastify qui semble plus que prometteurâŠ
I n i t i a l i s a t i o n
Il est temps de coder ! đ§âđ» Commençons par installer la dĂ©pendance principale de NestJS, puis initialisons un nouveau projet : hello-community
npm i -g @nestjs/cli
nest new hello-community
Alors, vous la voyez la ressemblance avec Angular ? đ En utilisant lâinterface en ligne de commande de NestJS, on se retrouve avec une architecture âmodules â controllers â servicesâ :
- Les controllers se chargent dâexposer les routes de lâapplication
- Les services sâoccupent de la communication avec la / les source(s) de donnĂ©es
- Les modules mettent en relation les services avec les controllers
NB : En plus de lâinitialisation du projet, lâoutil CLI de NestJS va nous permettre de âscaffolderâ individuellement ces mĂȘmes fichiers (
nest generate <module|controller|service>
). PlutĂŽt pratique !
Maintenant, intéressons-nous davantage au fichier main.ts
, point d'entrĂ©e de l'application. Par dĂ©faut, NestJS va monter un serveur HTTP en se basant sur ExpressJS. Or, dans mon cas, je vais avoir besoin de Fastify, qui (pour rappel) est censĂ© ĂȘtre plus rapide et lĂ©ger que son homologue.
Pour cela, il est nĂ©cessaire dâajouter la dĂ©pendance @nestjs/platform-fastify
au projet, puis d'adapter le code présent dans le fichier main.ts
.
Et voilĂ ! Il vous suffit de vous rendre Ă lâadresse http://localhost:3000
pour admirer votre premier Hello World!
(ou bien Hello Community!
si vous avez Ă©tĂ© curieux) đ
NB : En précisant la chaßne de caractÚres
0.0.0.0
en tant que deuxiĂšme paramĂštre de la fonctionlisten()
, vous pourrez accĂ©der Ă vos endpoints dans un mĂȘme rĂ©seau local.
Difficile de voir une vraie diffĂ©rence entre ExpressJS et Fastify avec un simple appel. Quoique⊠Jâai remarquĂ© une lĂ©gĂšre diffĂ©rence au moment du chargement. DâaprĂšs Google Chrome (confirmĂ© par Mozilla Firefox), ce premier Hello World!
charge 239 octets avec ExpressJS, contre 176 octets via Fastify. Force est de constater que ce framework est (au moins) plus léger que son prédécesseur.
C R U D
Rendons les choses plus intĂ©ressantes / plus concrĂštes en mettant en Ćuvre notre premiĂšre API REST(ful) avec NestJS. Pour cela, on peut utiliser lâoutil CLI pour crĂ©er des fichiers indĂ©pendamment (module / controller / service), ou bien gĂ©nĂ©rer tout lâensemble directement (câest-Ă -dire une nouvelle ressource CRUD).
La commande nest g resource
fait cela pour nous. En renseignant seulement le nom de la ressource (users
) et en prĂ©cisant qu'il s'agit d'une API REST, on obtient rapidement un controller et un service prĂȘts Ă l'emploi.
Lâun des avantages de ce mode de fonctionnement, câest que NestJS gĂ©nĂšre Ă©galement certains patterns de dĂ©veloppement, notamment le DTO (Data Transfer Object) bien connu des dĂ©veloppeurs. #POO
Pour cette premiĂšre ressource, je vous propose de conserver le controller en lâĂ©tat (users.controller.ts
), mais d'adapter le code du service (users.service.ts
), afin de simuler une source de données.
NB : Pour des soucis pratiques (rĂ©duction de la taille de cet article), jâai choisi de faire apparaitre le type de mes donnĂ©es. Dans un cas rĂ©el, je vous conseille de ranger cela aux bons endroits, Ă savoir dans des dossiers
interfaces
etenums
(ou tout simplementmodels
).
Assurez-vous dâavoir enregistrĂ© votre module users.module.ts
au niveau du module principal (app.module.ts
), puis lancer l'application (npm run start
). Vous devriez pouvoir commencer Ă jouer avec votre API via le protocole REST (et Postman bien sĂ»r đ).
S Ă© c u r i t Ă©
Les choses sĂ©rieuses dĂ©butent ici⊠Si on reviens un peu en arriĂšre, on remarque que nos endpoints sont tous accessibles avec le mĂȘme niveau de sĂ©curitĂ© (câest-Ă -dire aucun). De plus, le mot de passe de lâutilisateur est sauvegardĂ© en clair en base de donnĂ©es⊠Il convient donc de sĂ©curiser tout cela !
PremiĂšre Ă©tape : lâoffuscation du mot de passe. NestJS supporte le chiffrement de donnĂ©es (grĂące au module crypto de NodeJS), mais aussi le hachage de donnĂ©es. Ici, jâai choisi dâutiliser la librairie bcrypt pour âhasherâ mes chaines de caractĂšres, afin de garantir une sĂ©curitĂ© unidirectionnelle (et optimale) ! đ
Installons les dépendances (npm i --save bcrypt && npm i --save-dev @types/bcrypt
) et modifions le service de création d'utilisateur pour sécuriser le mot de passe.
DeuxiĂšme Ă©tape : sĂ©curiser les appels en utilisant un protocole dâauthentification. Cette seconde tĂąche est plus complexe puisquâelle nĂ©cessite de mettre en place une ou plusieurs stratĂ©gies dâauthentification. Fort heureusement, NestJS dispose dĂ©jĂ dâun concept de âguardâ permettant âde valider / dâinvaliderâ les routes de lâapplication. Câest partie !
npm install --save @nestjs/passport passport passport-local passport-jwt
npm install --save-dev @types/passport-local @types/passport-jwt
Le meilleur moyen dâĂ©tablir ce genre de fonctionnement (avec NodeJS) est dâutiliser Passport. CĂŽtĂ© conception, je vais devoir crĂ©er une nouvelle route dâauthentification qui me fournira un jeton permettant de valider mes endpoints /users/:id
(GET, PATCH et DELETE). Voilà pourquoi, je récupÚre (ci-dessus) deux stratégies :
- La stratégie LOCAL, permet une authentification simple (basé sur le couple
username
/password
) - La stratĂ©gie JWT, sâappuie sur la validitĂ© dâun jeton dans le temps
Ă nouveau, il va falloir utiliser lâinterface en ligne de commande pour instancier un module, un controller ainsi quâun service (nest g <module|controller|service> auth
). De plus, nous allons devoir créer deux nouveaux fichiers correspondants respectivement aux stratégies de Passport.
NB : Vous noterez ici que je surcharge le comportement par défaut de la stratégie LOCAL, pour utiliser le couple
password
.
NB : Dans un cas rĂ©el, je vous dĂ©conseille dâexposer le secret JWT ! Il est prĂ©fĂ©rable de le rĂ©cupĂ©rer autrement, via le fichier
.env
(le module@nestjs/config
peut alors ĂȘtre utile) ou un autre procĂ©dĂ©...
AprĂšs tout cela, quelques explications sâimposent. Commençons avec le controllerâŠ
Lors de lâappel du endpoint /login
, l'utilisateur va "POSTer" ses identifiants au format email
/ password
.
NestJS va alors protĂ©ger la route par la âguardâ LOCAL qui sâoccupera de vĂ©rifier la prĂ©sence de lâutilisateur en base. Si lâutilisateur portant lâadresse email unique est trouvĂ©, le mot de passe va ĂȘtre vĂ©rifiĂ© Ă son tour par bcrypt (auth.service.ts
).
Si tout se passe bien, la fonction login()
va ĂȘtre jouĂ©e avec les informations relatives Ă l'utilisateur (notamment l'identifiant unique et l'email) qui seront nĂ©cessaires Ă la gĂ©nĂ©ration d'un jeton d'une validitĂ© de 5 minutes (300s
).
Enfin, au retour du endpoint, nous devrions récupérer un jeton JWT (access_token
) qui servira Ă authentifier les autres appels de notre API REST. Pour ce faire, deux derniĂšres choses :
- Ajouter la âguardâ JWT sur les endpoints
/users/:id
(users.controller.ts
) - Ajouter le
Bearer <token>
dans le header de(s) l'appel(s)
P R I S M A
Ăvoquons maintenant la partie concernant les donnĂ©es. Ă partir de maintenant il va falloir se connecter Ă notre base de donnĂ©es (prĂ©alablement installĂ© sur notre machine : sudo apt install postgresql
), puis l'interroger de maniĂšre REST(ful), Ă savoir en GET, POST, PATCH, DELETE, etc...
Une derniĂšre fois, un choix cornĂ©lien sâimpose, entre :
- Le driver de la base de donnĂ©es (requĂȘtage de bas niveau)
- Le âQuery Builderâ (tel que KnexJS)
- LâORM â Object Relational Mapping (requĂȘtage de haut niveau)
NB : Ă titre dâexemple, avec MongoDB, jâai lâhabitude de dĂ©velopper avec Mongoose (qui est un ODM â Object Document Mapper) pour requĂȘter ma source de donnĂ©es NoSQL. Ayant fait le choix du SQL avec Postgres, il va ĂȘtre indispensable dâutiliser une nouvelle librairieâŠ
Pas de panique ! NestJS est lĂ đ Et il dispose dĂ©jĂ de âconnecteursâ pour certaines librairies, dont : Sequelize, TypeORM ou encore Prisma. Jâai prĂ©fĂ©rĂ© Ă©carter Sequelize (malgrĂ© sa popularitĂ©) de la liste des challengers, puisque je souhaite pouvoir rĂ©-utiliser âlâĂ©luâ pour un dĂ©veloppement futur avec NoSQL (je ne peux vivre sans Mongo đ ). De ce fait, le seul choix possible nâest autre que Prisma. Dâailleurs, jâai Ă©tĂ© agrĂ©ablement surpris par ses concepts et son API intuitive ! Il est grand temps dâinstaller cette nouvelle dĂ©pendance, puis dâinitialiser le schĂ©ma de la base de donnĂ©es.
npm install prisma --save-dev
npx prisma init
En exécutant la commande init
de Prisma, on devrait voir apparaitre un nouveau dossier à la racine de notre projet, avec un premier brouillon du schéma. Je vous invite donc à le compléter de la sorte :
Aussi explicite soit-il, le schĂ©ma de base de donnĂ©es va nous permettre dâinitialiser notre base PostgreSQL. Clairement, en exĂ©cutant ce dernier (avec la commande migrate
), Prisma va transcrire les informations ci-dessus en script SQL, afin de crĂ©er nos tables (avec les bons champs et les bons types) et ajouter nos relations (clĂ©s primaires et Ă©trangĂšres). Il finira par exĂ©cuter la requĂȘte directement en base.
npx prisma migrate dev --name init
NB : Ă partir de lĂ , notre base est crĂ©Ă©e. Vous pouvez vous y connecter et constater le rĂ©sultat ; ou bien aller jeter un coup dâoeil Ă la requĂȘte SQL prĂ©sent dans le fichier
migration.sql
(dans le dossierprisma
).
npm i @prisma/client
nest g module prisma
nest g service prisma
La base de donnĂ©es et donc correctement crĂ©Ă©e, mais il nous reste encore Ă faire le lien entre PostgreSQL (via Prisma) et NestJS. En utilisant lâoutil CLI de NestJS (ci-dessus), jâai gĂ©nĂ©rĂ© deux nouveaux fichiers :
prisma.module.ts
prisma.service.ts
Une derniĂšre Ă©tape : lâutilisation ! Vous vous souvenez de notre CRUD ci-dessus ? Faisons le Ă©voluer ! DorĂ©navant, plus question de simuler notre base de donnĂ©es, requĂȘtons lĂ directement. Pour cela, lâAPI de Prisma est tout simplement magique ! Elle gĂ©nĂšre (grĂące au schĂ©ma de la base de donnĂ©es) les types correspondant aux models
, et nous fournit des fonctions intuitives pour interroger notre base SQL. Voyez par vous-mĂȘme le rĂ©sultat avec les fonctions de crĂ©ation et de rĂ©cupĂ©ration d'un utilisateur.
NB : Nâoubliez pas dâenregistrer votre nouveau module (
prisma.module.ts
) dans les imports du moduleusers
, sinon impossible de requĂȘter votre source de donnĂ©es...
NB : Pour des soucis pratiques (réduction de la taille de cet article), je vous présente ici seulement deux des fonctions présentes dans le service, mais vous pouvez consulter le reste du code sur GitHub.
E p i l o g u e
NestJS est une pĂ©pite du dĂ©veloppement Web ! Je me suis Ă©clatĂ© Ă dĂ©velopper cette nouvelle solution backend en TypeScript. Tout est trĂšs bien structurĂ© ! Par ailleurs, la CLI est dâune grande aide lorsquâil sâagit de âscaffolderâ tout ou partie dâune ressource REST.
Globalement, jâai Ă©tĂ© agrĂ©ablement surpris par son organisation qui nâest pas sans rappeler les frameworks que sont SpringBoot (en Java) ou encore Django (en Python). Les fichiers sont rangĂ©s Ă leur place et on sây retrouve. Comme quoi, le pattern MVC est toujours une valeur sĂ»reâŠ
NB : De maniĂšre plus objective, jâai mĂȘme Ă©tĂ© jusquâĂ demander lâavis dâun confrĂšre dĂ©veloppeur spĂ©cialisĂ© en Java, et je vous assure il nâa eu aucun problĂšme Ă se plonger dans le code (merci TypeScript).
Ă propos de la sĂ©curitĂ©, NestJS respecte sa part du contrat sans pour autant rĂ©inventer les choses, puisquâil se repose majoritairement sur la dĂ©pendance Passport pour consolider ses endpoints. Il peut aussi dĂ©finir la sĂ©curitĂ© des headers HTTP en intĂ©grant simplement des middlewares fournis par la librairie Helmet. De mĂȘme pour le chiffrement et le hachage des donnĂ©es, ces concepts ne sont pas une nouveautĂ© et existent Ă©galement dans dâautres frameworks backend.
Je nâai pas Ă©voquĂ© les tests unitaires prĂ©cĂ©demment, mais ils sont bien prĂ©sents dans ce projet. NestJS fait (lĂ encore) un bon choix en âabandonnantâ le couple Karma / Jasmine au profit de Jest. Il est trĂšs simple de mocker les sources de donnĂ©es Prisma, pour se concentrer sur les appels dâun controller, ou bien sur la fonction dâun service.
En disposant de concepts puissants et dâune documentation bien faite, NestJS sâimpose comme une rĂ©fĂ©rence dans le dĂ©veloppement backend en JavaScript. Câest un framework personnalisable (en acceptant le moteur ExpressJS, ou en changeant pour celui de Fastify), et pourtant trĂšs complet avec son lot de plugins : tel que la possibilitĂ© dâajouter de la documentation OpenAPI via Swagger, ou encore dâajouter lâabstraction GraphQL Ă nos appels REST(ful), etcâŠ
Ă lâavenir, je nâhĂ©siterai plus dans mon choix de socle technique pour NodeJS, jâopterai simplement pour NestJS đ