Bien qu’Express soit le framework NodeJS archi dominant, Fastify propose une alternative moderne tout en restant un framework dit ‘de bas niveau’, permettant aux développeurs de composer leurs applications avec les librairies de leurs choix.
L’écosystème JavaScript est connu pour sa richesse, pour ne pas dire son excès, de librairies et de frameworks. Côté backend, ExpressJS est de loin le framework NodeJS le plus populaire. Il existe toutefois de nombreuses autres options, moins connues, qui méritent toutefois notre attention. Il existe des framework dits haut niveau, avec des avis très tranchés, tels que NestJS ou Adonis. Ces frameworks vont embarquer plusieurs librairies et vous proposer à la fois des fonctionnalités, des implémentations de bonnes pratiques telles que l’injection de dépendance et proposer une architecture.
Ces frameworks de haut niveau viennent souvent avec des choix de librairies imposés, ce qui ne convient pas toujours aux développeurs. C’est pourquoi les frameworks bas niveau, à l’instar d’Express, existent et vous laissent avoir beaucoup de contrôle sur les librairies à condition que vous acceptiez de les implémenter vous-même.
Pourtant, pour une raison que j’ignore, Express et ces frameworks bas niveau sont encore très populaires dans l’écosystème JavaScript. Alors dans ce cas, pourquoi challenger le leader incontesté ?
Pourquoi regarder ailleurs qu’Express ?
Pour la première fois depuis son entrée au classement de State Of JS en 2017, Express a perdu sa place de favoris au profit de Next.js. Ces deux frameworks ne jouent absolument pas dans la même cour. Next étant plus orienté framework Fullstack avec une grosse dominante frontend et un backend inclus de façon à créer une API très simple dans le même projet. Express quant à lui est un framework purement backend, qui certes peut être couplé avec des moteurs de templating tels que Jade, Pug ou Handlebars afin de servir des pages HTML, mais en pratique il est bien plus souvent utilisé pour des process backend pur.
Alors pourquoi s’inquiéter d’express simplement parce qu’il y a (encore) un nouveau framework JS à la mode ?
Express est tellement populaire qu’il est automatiquement associé lorsqu’on parle de Node pour un serveur web, de la même façon qu’on associe Rails à Ruby ou Django à Python. Express a même pris sa place dans des acronymes de tech stacks tels que MEAN ou MERN.
Pourtant de plus en plus de développeurs JS semblent inquiets par Express pour les raisons suivantes:
Express n’implémente pas l’async/await
Tant aimée par les développeurs depuis son arrivée avec ES8, la syntaxe async/await
a simplifié la vie de nombreux de développeurs JavaScript. Sauf qu’Express lui est resté à l’utilisation de callbacks.
Au-delà de la syntaxe de votre serveur, qui certes peut vous déplaire mais on peut s’en remettre, il y a le fait d’utiliser des async/await
dans le code que vous développerez et que vous implémenterez dans des middlewares express.
Si vous utilisez async/await
dans votre controller ou dans un middleware, vous vous exposez à des erreurs UnhandledPromiseRejectionWarning
.
La librairie est devenue un zombie
Au moment où j’écris cet article, la version 5 d’express est en alpha depuis plus de 6 ans, le dernier incrément de version date plus de deux ans et le dernier merge sur la branche master a plus de 9 mois.
Bien qu’il existe toujours de Pull Requests récentes, la librairie semble évoluer très lentement et je ne suis pas convaincu que nous verrons la version 5 d’Express sur npm avant encore plusieurs années.
Pourquoi envisager Fastify ?
Bien que conçu pour être un framework pour n’importe quelle application web, Fastify brille par ses performances lorsqu’il est utilisé dans la conception d’API REST. Fastify permet d’avoir des performances remarquables en termes de volume de requêtes qu’il peut traiter.
Ce graphique représente les performances du nombre de requêtes simultanées que peut traiter un serveur web suivant le framework utilisé (source: doc Fastify)
Suivant ce benchmark publié sur la documentation de Fastify, ce framework est bien supérieur à ses pairs en termes de capacité de traitement. Attention toutefois à ne pas prendre pour argent comptant ce type de benchmark car nous n’avons pas le détail du test et il y a fort à parier que celui-ci ne reflète pas forcément une véritable application en production.
Fastify propose également un principe d’encapsulation qui permet de rendre disponible des modules uniquement dans certains espaces de votre application. La capacité d’encapsuler les modules de manière très simple permet d’appliquer des concepts d’architecture.
Illustration de l’encapsulation avec Fastify
Dans cette illustration tirée de la documentation de Fastify, nous pouvons voir comment l’encapsulation que propose Fastify permet d’isoler certains plugins. Par exemple, les plugins déclarés dans le carré « Child Context » supérieur seront utilisables lorsque la requête traversera le code du « Child Context » ainsi que de son « Grandchild Context ». En revanche il ne sera pas disponible dans le bloc Child Context inférieur ni son bloc GrandChild.
Cette encapsulation va nous permettre de découpler des modules de l’application. Ce découplage va éviter au développeur de créer des dépendances entre blocs de codes qui ne devraient pas être liés en termes de métier.
Voici à quoi ressemble l’encapsulation dans le code d’une app Fastify:
// server.jsconst fastify = require('fastify')({ logger: true })// La méthode décorateRequest permet d'ajouter un objet à l'objet Requestfastify.decorateRequest('user', 'rayed')/*Etant donné qu'il est déclaré à la racine de mon serveur, il est accessible partout*/fastify.get('/', async (request, reply) => { return { user: request.user, email:request.email }})// Ici, l'objet retourné sera {user:'rayed'}// car nous n'avons pas encore décoré la request avec l'objet email// Lancer le serveurconst start = async () => { try { await fastify.listen(3000) } catch (err) { fastify.log.error(err) process.exit(1) }}start()
Après avoir lancé ce code, lorsque nous faisons une requête GET sur l’API REST que nous venons de créer, nous avons la réponse {"user":"rayed"}
.
Ajoutons maintenant une autre route qui aura accès à l’objet request.email
:
// fichier routes.jsasync function routes (fastify, options) { fastify.decorateRequest('email', '[email protected]') fastify.get('/email', async (request, reply) => { return { user: request.user, email:request.email } }) } module.exports = routes
D’abord nous créons un fichier routes.js
, dans lequel nous allons ajouter le decorateRequest
et déclarer une route /email
Ensuite nous irons modifier le fichier server.js
pour déclarer cette nouvelle route à notre serveur Node:
// server.jsconst fastify = require('fastify')({ logger: true })// On importe le fichier routes.jsconst routes = require('./routes')// Ici nous déclarons le plugin dans le rootContextfastify.register(routes)fastify.decorateRequest('user', 'rayed')fastify.get('/', async (request, reply) => { return { user: request.user, email:request.email }})const start = async () => { try { await fastify.listen(3000) } catch (err) { fastify.log.error(err) process.exit(1) }}start()
En relançant notre serveur, on peut voir qu’en faisant une requête GET sur l’endpoint /
nous avons toujours la réponse {"user":"rayed"}
mais lorsqu’on fait une requête /email
nous avons accès à {"user":"rayed", "email":"[email protected]"}
.
La fonction decorateRequest
placée dans notre fichier routes.js n’est pas accessible quand le code ne la traverse pas, ce qui nous permet d’isoler le fonctionnement d’un module du reste de la codebase.
Créer un serveur Fastify
Créer un serveur web avec Fastify est aussi facile qu’avec Express. Installez le package avec npm you Yarn:
npm install fastify
Créez un fichier server.js:
const fastify = require('fastify')({ logger: true })fastify.get('/', async (request, reply) => { return { hello: 'world' }})const start = async () => { try { await fastify.listen(3000) } catch (err) { fastify.log.error(err) process.exit(1) }}start()
Lancez-le avec la commande node server
depuis votre terminal.
Validez vos requêtes et vos réponses
Une fonctionnalité vraiment intéressante de Fastify est sa capacité à valider les requêtes à grande vitesse.
La validation est une étape qui, à l’arrivée d’une requête HTTP sur le serveur, va vérifier un set de règles que vous aurez défini, avant même de faire une opération lente tel qu’un appel en base de données ou n’importe quelle action asynchrone.
Suivant vos besoins, vous pourrez vous assurer que, pour chaque route spécifiquement, la requête entrante contient bien les headers
, les params
, le body
ou les query params
de votre choix.
Lorsque vous définirez vos routes, vous pourrez passer un schema
, de la même façon que vous l’auriez fait sur Mongoose par exemple, afin de définir les règles de validation.
const querySchema = { querystring: { type: 'object', properties: { name: { type: 'string', enum:['user-a','user-b'] }, excitement: { type: 'integer' }, }, required: ['name'] }}fastify.route({ method: 'GET', url: '/', schema: querySchema, handler: async (request, reply) => { return { hello: 'world' } }})
Dans cet exemple tiré de la documentation, le développeur impose un querystring
ayant la clé « name » pour tout appel HTTP sur l’endpoint /
et n’acceptera que les valeurs imposées dans l’objet enum
.
Si une requête entrante déroge à ces règles, Fastify retournera immédiatement une erreur 400 avec suffisamment de détails pour que le client puisse reformuler sa requête.
Sous le capot, Fastify utilise la librairie Ajv et la norme JSON-Schema. Pour maximiser les performances de votre serveur Fastify, je vous encourage à utiliser au maximum les validations proposées par JSON-Schema avant de développer vos propres règles.
Réagissez aux requêtes grâce aux hooks
Les hooks permettent de greffer votre code en réaction au cycle de vie d’une requête HTTP. Fastify a créé plusieurs hooks sur lesquels vous pourrez accrocher votre code afin qu’il soit exécuté à un instant précis de la requête sur un endpoint déterminé.
Fastify propose neuf hooks différents, positionnés tout au long du cycle de vie d’une requête:
onRequest
est le premier hook, juste après l’arrivée de la requête et le premier message de log. À ce stade le body n’est pas encore accessible.preParsing
arrive juste avant que Fastify transforme la requête et transforme en objet JSON.preValidation
se déclenche juste avant la validation de la requête par rapport aux règles définies dans les schémas de validation. À ce stade le body devient accessible.preHandler
se déroule juste avant la passation du code vers la suite de votre application, très souvent au niveau du controller.preSerialization
se déroule à la fin de votre code et juste avant la préparation de l’objetresponse
onError
est un hook utile s’il faut ajouter du code spécifique en cas d’erreur.onSend
est un hook appelé juste avant l’envoi de la réponse au client. Cette étape est utile pour envelopper votre payload dans un body de réponse standardisée par exemple.onTimeout
se déclenche lorsque le serveur ne répond pas à la requête à temps. Ce hook peut être utile si on souhaite effectuer une nouvelle tentative par exemple.
Il existe également des hooks liés à votre serveur:
onReady
: lorsque le serveur a terminé son initialisation.onClose
: lorsque le serveur est sur le point de s’arrêter.onRoute
: lorsqu’une route est déclarée.onRegister
: lorsqu’un plugin est enregistré.
Les hooks n’ont pas la même utilité que les middlewares. Ces derniers sont déclarés au nouveau du serveur et vont agir sur l’ensemble des requêtes, quel que soit le verbe HTTP envoyé. Les hooks ont la même idée mais permettent une gestion plus fine de quand les déclencher.
Enrichissez vos requêtes et vos réponses grâce aux Decorators
L’API decorators permet d’enrichir les objets Fastify tels que le serveur, les requêtes et les réponses. L’utilisation des fonctions decorators permet d’enrichir ces objets sans affecter la performance de votre serveur Fastify.
Dans la documentation, Fastify explique pourquoi ce code qui souhaite enrichir l’objet request
est un frein pour le moteur JavaScript:
// Bad example! Continue reading.// Attach a user property to the incoming request before the request// handler is invoked.fastify.addHook('preHandler', function (req, reply, done) { req.user = 'Bob Dylan' done()})// Use the attached user property in the request handler.fastify.get('/', function (req, reply) { reply.send(`Hello, ${req.user}`)})
En changeant la structure de l’objet, dans ce cas en y ajoutant la clé ‘user’ et sa valeur, JavaScript va devoir modifier l’instance de l’objet request
à chaque fois que ce hook est déclenché.
La bonne approche utilisant les decorators serait:
// Decorate request with a 'user' propertyfastify.decorateRequest('user', '')// Update our propertyfastify.addHook('preHandler', (req, reply, done) => { req.user = 'Bob Dylan' done()})// And finally access itfastify.get('/', (req, reply) => { reply.send(`Hello, ${req.user}!`)})
En utilisant le decorator decorateRequest
avant le hook, chaque objet request
sera par défaut construit avec la propriété user
et la valeur d’une chaîne de caractères vide, correspondant au type qui lui sera affecté plus tard. Le fait de lui modifier sa valeur au niveau du hook sera plus optimal que de modifier la structure de l’objet en lui affectant également une nouvelle clé.
Les décorateurs les plus courants sont decorate
, qui sont utilisés pour personnaliser l’instance serveur de Fastify, decorateRequest
et decorateReply
qui portent bien leurs noms.
Comment utiliser les plugins Fastify ?
Fastify a bâti son framework autour des plugins afin de faciliter la modularité et l’isolation du code. Ils permettent d’englober des fonctions, des routes, des decorators, des hooks et potentiellement d’autres plugins afin de rendre tout cela disponibles au contexte dans lesquels ils seront importés.
Les plugins Fastify sont importés dans votre code via la fonction fastify.register
. Par défaut, la fonction register
va créer un nouveau scope, de telle sorte à ce que permettre l’encapsulation.
Les plugins officiels et communautaires
Les contributeurs Fastify ont créé 44 plugins officiels et la communauté en a créé plus de 140 autres de façon à répondre à leurs besoins. Parmi les plugins core, on retrouve des connecteurs à des bases de données tels que MongoDB, ElasticSearch, Postgresql ou Redis, des plugins fonctionnels pour mettre en cache, limiter les requêtes entrantes, charger des variables d’environnement ou utiliser des websockets.
Connecter une base de données à un serveur Fastify
Grâce aux plugins officiels, connecter une base de données à son serveur Fastify se fait très simplement. Par exemple, le plugin fastify-mongodb peut être implémenté de la façon suivante:
const fastify = require('fastify')()fastify.register(require('fastify-mongodb'), { forceClose: true, // URL de votre DB en local ou sur MongoDB Atlas url: 'mongodb://mongo/mydb'})fastify.get('/user/:id', function (req, reply) { // Or this.mongo.client.db('mydb') const db = this.mongo.db db.collection('users', onCollection) function onCollection (err, col) { if (err) return reply.send(err) col.findOne({ id: req.params.id }, (err, user) => { reply.send(user) }) }})fastify.listen(3000, err => { if (err) throw err})
La fonction register
va créer l’objet mongo
dans le contexte. La connexion à la base de données sera accessible dans le module ainsi que dans tous les modules enfants en utilisant this.mongo.db
.
Pour faire fonctionner le serveur avec une base de données SQL, le plugin fastify-postgres permet d’arriver aux mêmes fins avec très peu de changements.
Pouvoir encapsuler son code dans différents plugins est un des deux principaux atouts que Fastify a par rapport à Express. La capacité d’isoler du code du reste de la codebase permet de maintenir un code source propre et limiter les effets de bords.
Comme on l’a vu, utiliser un plugin dans un contexte est aussi simple que de faire fastify.register
. Pour créer un plugin, il suffit de créer un module javascript et de l’exporter:
// notre-premier-plugin.jsasync function firstPlugin (fastify,options) { fastify.get('/plugin', async (request, reply) => { return { hello: 'world' } })}
Dans le serveur, il suffira de réimporter le fichier en faisant un require
puis en utilisant la fonction fastify.register
.
Utiliser TypeScript avec Fastify
TypeScript est devenu un incontournable dans l’écosystème JavaScript. Bien que Fastify soit écrit en JavaScript, l’équipe cœur de Fastify a mis un point d’honneur à rendre les « type definitions » disponibles pour les développeurs souhaitant utiliser Fastify avec TypeScript.
Fastify repose déjà sur la librairie JSON Schema pour effectuer la validation des requêtes. Fastify invite les développeurs ayant défini leurs schémas à capitaliser sur ces derniers afin de définir des Types réutilisables plus tard dans votre code.
Grâce à la librairie json-schema-to-ts, vous pouvez ajouter un script à votre fichier package.json afin de compiler les schémas en types et les réutiliser dans votre code.
Écrire des tests sur son serveur Fastify
Vous savez déjà à quel point les tests automatisés sont la colonne vertébrale de toute application. Sans tests, votre code n’est qu’un château de cartes qui menace de s’effondrer à chaque fois qu’on y touche.
Comme pour tous les autres frameworks node, Fastify peut être testé avec Jest, Mocha-Chai ou n’importe quelle autre librairie de votre choix. La rédaction de tests unitaire ne va rien avoir de spécifique sur Fastify par rapport à Express.
Là où Fastify devient intéressant est lorsqu’il faut coder des tests fonctionnels. La fonction inject
permet de faire une fausse requête HTTP avec les paramètres de son choix. Plus besoin d’utiliser nock ou Supertest.
Devrais-je envisager Fastify pour mon prochain projet ?
Bien que Fastify soit très bien maintenu et que la communauté soit très active, il faut d’abord t’interroger sur le but de ce projet. S’il s’agit de monter en compétence en JavaScript ou TypeScript dans un contexte backend, c’est très intéressant d’utiliser Fastify pour découvrir une nouvelle approche.
S’il s’agit de créer une véritable application fonctionnelle avec des utilisateurs, et que tu n’as pas spécialement de préférence ou d’expérience à part avec Express, je te conseille de te pencher sur les frameworks plus haut niveau tels qu’Adonis ou NestJS.
Si tu souhaites développer tes compétences en vue de devenir développeur et décrocher ton premier CDI, je te conseille plutôt de persérvérer sur ExpressJS qui reste le framework le plus utilisé dans l’écosystème, et de loin.
Enfin, si tu es développeur backend plus aguerri, que tu souhaites rester sur un framework bas niveau parce que tu aimes contrôler les librairies que tu veux utiliser dans ton projet et que tu es lassé d’express, Fastify est une bonne option pour ton projet.