Node.js permet nativement de créer un serveur HTTP, mais il est de notre responsabilité de devoir gérer le routage, le formatage des données ou encore la gestion des erreurs. Bref, on ne va pas se mentir, la plupart du temps nous utilisons des frameworks tels que le populaire Express.js. Mais un autre framework commence à faire de plus en plus parler de lui, il s’agit de Fastify.
Note : Puisqu’il s’agit d’un article de découverte, je ne rentre pas dans les détails et les exemples sont les plus simples possibles pour rendre l’article accessible à tous. Je ferais peut-être un article plus poussé sur Fastify dans le futur.
Fastify c’est quoi ?
Fastify est un framework web léger pour Node.js, inspiré de Hapi, Restify et Express. Comme son nom l’indique, Fastify est rapide, c’est même le framework web le plus rapide de l’écosystème Node.js. Mais outre sa vitesse, sa popularité vient de l’excellente expérience développeur qu’il offre et de son système de plugins simple, mais efficace.
Entrons tout de suite dans le vif du sujet et installons Fastify :
1 | npm install fastify |
Créons ensuite un fichier server.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | const Fastify = require('fastify') // On crée une instance de fastify en activant les logs const fastify = Fastify({ logger: true }) // On enregistre une route GET fastify.get('/', (request, reply) => { reply.send({ hello: 'world' }) }) // On démarre le serveur qui est en écoute sur le port 3000 fastify.listen(3000, (err) => { if (err) { fastify.log.error(err) process.exit(1) } }) |
Puis lançons notre serveur :
1 | node server.js |
Enfin, vérifions que tout fonctionne correctement :
1 | curl http://localhost:3000 |
Vous devriez voir s’afficher {"hello":"world"}
.
Si vous avez déjà utilisé Express ou tout autre framework web, vous ne devriez pas être perdu.
Déclaration de routes
La déclaration de route peut se faire de deux façons. La première en utilisant la méthode fastify.route(options)
:
1 2 3 4 5 6 7 | fastify.route({ method: 'GET', url: '/', handler: function (request, reply) { reply.send({ hello: 'world' }) } }) |
Ou la seconde en utilisant directement les méthodes correspondantes aux méthodes HTTP :
fastify.get(path, [options], handler)
fastify.head(path, [options], handler)
fastify.post(path, [options], handler)
fastify.put(path, [options], handler)
fastify.delete(path, [options], handler)
fastify.options(path, [options], handler)
fastify.patch(path, [options], handler)
Par exemple :
1 2 3 | fastify.get('/', (request, reply) => { reply.send({ hello: 'world' }) }) |
async/ await
La plupart des autres frameworks (notamment Express) ne proposent pas un support async/await
c’est-à-dire lorsque vous écrivez un handler asynchrone d’une route, vous êtes obligé d’utiliser un try/catch
sous peine de vous retrouver avec une erreur UnhandledPromiseRejectionWarning
.
Voici un exemple avec Express :
1 2 3 4 5 6 7 8 9 10 | app.get('/data', async (req, res, next) => { try { const data = await getData() const processed = await processData(data) res.json(processed) } catch (err) { // On transfère l'erreur au middleware d'erreur return next(err) } }) |
Et voici le même exemple avec Fastify :
1 2 3 4 5 6 | fastify.get('/data', async (request, reply) => { const data = await getData() const processed = await processData(data) // reply.send n'est pas obligatoire vous pouvez directement renvoyer la réponse via l'instruction return return processed }) |
Si une erreur se produit, Fastify va automatiquement la transmettre au handler d’erreurs.
Si vous voulez en savoir plus, je vous invite à aller lire la documentation concernant les routes.
Validation et sérialisation des données
Fastify utilise JSON schema pour valider les données reçues sur les différentes routes, mais également pour sérialiser le corps des réponses HTTP. Pour cela il fait appel à deux libraires :
- Ajv pour la validation des requêtes ;
- fast-json-stringify pour la sérialisation du corps des réponses qui permet entre autres d’améliorer les performances.
Bien que ce ne soit pas obligatoire, il est vivement recommandé d’utiliser la validation et la sérialisation des données.
Validation
La validation des données des requêtes peut s’effectuer sur les quatre propriétés suivantes :
body
: permet de valider le corps de la requêtePOST
ouPUT
;querystring
ouquery
: permet de valider les « query strings » (ce sont les paramètres qui se trouvent après le caractère?
par exemple?foo=hello&bar=world
)params
: permet de valider les paramètres de la route (par exemple dans la route/posts/:id
,id
est un paramètre) ;headers
: permet de valider les en-têtes de la requête.
Pour définir le schéma de validation de ces propriétés, il suffit d’ajouter l’option schema
à la route :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | fastify.get('/users/:userId/posts', { schema: { params: { type: 'object', required: ['userId'], properties: { userId: { type: 'integer' } } }, query: { type: 'object', properties: { start_at: { type: 'string', format: 'date' }, end_at: { type: 'string', format: 'date' } } } } }, (request, reply) => { return [/* les articles de l'utilisateur */] }) |
Testons maintenant notre route avec un paramètre id
incorrect :
1 | curl http://localhost:3000/users/invalid_id/posts |
On obtient l’erreur HTTP suivante :
1 2 3 4 5 | { "statusCode":400, "error":"Bad Request", "message":"params.userId should be integer" } |
Voyons maintenant avec le paramètre « query string » start_at incorrect :
1 | curl http://localhost:3000/users/1/posts?start_at=invalid_date |
On obtient l’erreur HTTP suivante :
1 2 3 4 5 | { "statusCode":400, "error":"Bad Request", "message":"querystring.start_at should match format \"date\"" } |
Il est également possible d’utiliser d’autres librairies de validation telle que Joi
par exemple.
Sérialisation
Le schéma de sérialisation permet d’améliorer les performances, mais également d’empêcher de renvoyer des données sensibles. Comme pour la validation, il suffit d’ajouter l’option schema
à la route :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | fastify.get('/me', { schema: { response: { // On spécifie le code de la réponse HTTP pour lequel le schéma s'applique 200: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' } } } } } }, (request, reply) => { return { id: 1, name: 'John', password: 'password' // ne sera pas envoyé } }) |
Testons maintenant notre route :
1 | curl http://127.0.0.1:3000/me |
On obtient la réponse suivante :
1 2 3 4 | { "id":1, "name":"John" } |
On remarque que la propriété password
n’est pas contenue dans la réponse puisque celle-ci ne fait pas partie du schéma.
Si vous voulez en savoir plus concernant la validation et la sérialisation des données, direction la documentation.
Hooks
Fastify propose de nombreux hooks permettant d’interagir avec le cycle de vie du serveur et/ou du cycle de vie des requêtes et des réponses. Les hooks sont simplement des méthodes que l’on associe à des événements comme vous avez sûrement l’habitude de le faire.
Les hooks liés au serveur
Il existe quatre hooks liés au serveur :
onReady
: déclenché quand le serveur a fini son initialisation juste avant que celui-ci soit en écoute ;onClose
: déclenché lorsque la méthodeclose
est appelée pour arrêter le serveur ;onRoute
: déclenché lors de l’enregistrement d’une route ;onRegister
: déclenché lors de l’enregistrement d’un plugin.
Les hooks liés aux requêtes et réponses
Il existe neuf hooks liés aux requêtes et réponses :
onRequest
: déclenché quand une requête arrive sur le serveur ;preParsing
: déclenché avant de parser la requête. Vous pouvez, dans ce hook, modifier le payload de la requête ;preValidation
: déclenché avant la validation de la requête ;preHandler
: déclenché avant l’exécution du handler de la requête ;preSerialization
: déclenché avant la sérialisation de la réponse ;onSend
: déclenché avant l’envoi de la réponse. Ce hook permet par exemple de changer le payload de la réponse ;onError
: déclenché lorsqu’une erreur se produit ;onResponse
: déclenché lorsque la réponse est envoyée ;onTimeout
: déclenché lorsque la requête a expiré. L’optionconnectionTimeout
doit être définie lors de la création du serveur pour que ce hook se déclenche.
Eh ben ça en fait des hooks ! L’ajout d’un hook s’effectue via la méthode addHook
de l’instance du serveur. Par exemple :
1 2 3 4 | fastify.addHook('onRequest', (request, reply, done) => { // traitement ... done() }) |
Decorators
Les décorators permettent d’ajouter des propriétés ou des méthodes à l’instance du serveur ou aux objets correspondant aux requêtes et aux réponses. Javascript étant un langage dynamique, il est possible d’ajouter facilement des propriétés à un objet :
1 2 3 4 | const obj = {} obj.a = 'a' obj.b = 'b' |
On serait donc tenté de faire la même chose avec l’instance de notre serveur ou avec les requêtes et réponses :
1 2 3 4 5 6 7 8 9 10 11 12 13 | const Fastify = require('fastify') const fastify = Fastify({ logger: true }) // On attache une fonction à l'instance du serveur fastify.utility = () => { /* ... */} fastify.utility() // On ajoute un hook permettant d'authentifier l'utilisateur à chaque requête fastify.addHook('onRequest', async (request, reply) => { // On peut cette fois affecter l'utilisateur à la requête request.user = await authenticate() }) |
Mais faire de cette façon empêche le moteur JavaScript V8, d’effectuer des optimisations. À la place il faut faire appel, aux méthodes decorate
, decorateRequest
et decorateReply
pour respectivement ajouter des propriétés à l’instance du serveur, à l’objet correspondant à la requête et à celui correspondant à la réponse.
Modifions l’exemple précédent en utilisant les méthodes que je viens d’évoquer :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const Fastify = require('fastify') const fastify = Fastify({ logger: true }) fastify.decorate('utility', () => { /* ... */}) fastify.utility() // On ajoute la propriété "user" à la requête et on lui assigne la valeur null fastify.decorateRequest('user', null) // On ajoute un hook permettant d'authentifier l'utilisateur à chaque requête fastify.addHook('onRequest', async (request, reply) => { // On peut cette fois affecter l'utilisateur à la requête de façon optimisée request.user = await authenticate() }) |
Système de plugins
L’une des forces de Fastify est son système de plugins très simple à prendre en main. Un plugin est simplement une fonction contenant des decorators, des hooks, des routes ou même d’autres plugins et se présentant sous la forme suivante :
1 2 3 4 5 | const myPlugin = function (instance, opts, done) { // Ajout de décorators, hooks, routes, etc... done() } |
Avec en paramètre :
instance
: l’instance du serveur ;opts
: un objet contenant les options ;done
: une fonction de callback a appelé une fois le plugin initialisé.
Il suffit ensuite d’enregistrer le plugin via la méthode register
comme ceci :
1 | fastify.register(myPlugin, {/* l'objet contenant les options du plugins */ }) |
L’encapsulation
Vous remarquez que l’on passe l’instance du serveur en premier paramètre de la fonction du plugin. Cette instance va nous permettre d’ajouter des hooks, des decorators, des routes ou même d’autres plugins. Mais tout ce que vous enregistrez à l’intérieur d’un plugin à l’exception des routes, ne sera pas accessible à l’extérieur de celui-ci. Pour faire simple, ce qui se passe à l’intérieur du plugin reste à l’intérieur du plugin !
Voyons un exemple tout simple pour comprendre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const myPlugin = function (instance, opts, done) { // On transforme le texte de la réponse en majuscule avant de l'envoyer au client instance.addHook('onSend', (request, reply, payload, done) => { const newPayload = payload.toUpperCase() done(null, newPayload) }) instance.get('/hello-plugin', (request, reply) => { reply.send('Hello, World !') }) done() } fastify.register(myPlugin) fastify.get('/hello', (request, reply) => { reply.send('Hello, World !') }) |
Nous avons deux routes, /hello-plugin
et /hello.
Testons la première :
1 | curl http://localhost:3000/hello-plugin |
Nous avons le message HELLO, WORLD !
en majuscule qui s’affiche. Testons maintenant la seconde :
1 | curl http://localhost:3000/hello |
Cette fois-ci nous avons le message Hello, World !
en minuscule qui s’affiche. Le hook présent dans le plugin ne s’applique qu’aux routes contenues dans celui-ci.
Mais il arrive parfois que l’on ait besoin de rendre accessibles certains decorators ou hooks à l’extérieur d’un plugin. Pour cela il suffit d’utiliser le package fastify-plugin
:
1 | npm install fastify-plugin |
Reprenons notre exemple précédent en utilisant fastify-plugin
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const fp = require('fastify-plugin') const myPlugin = function (instance, opts, done) { // On transforme le texte de la réponse en majuscule avant de l'envoyer au client instance.addHook('onSend', (request, reply, payload, done) => { const newPayload = payload.toUpperCase() done(null, newPayload) }) instance.get('/hello-plugin', (request, reply) => { reply.send('Hello, World !') }) done() } fastify.register(fp(myPlugin)) fastify.get('/hello', (request, reply) => { reply.send('Hello, World !') }) |
Si nous refaisons une requête sur la route http://localhost:3000/hello
nous avons bien le message HELLO, WORLD !
en majuscule qui s’affiche. Le hook s’applique bien sur les routes présentes en dehors du plugin.
L’exemple que j’ai donné est vraiment sommaire et n’est pas forcement très utile mais c’est pour que vous compreniez le fonctionnement de l’encapsulation de Fastify.
Ecosystème
Fastify dispose de nombreux plugins créés par la communauté. Vous retrouverez la plupart des fonctionnalités bien connues comme la prise en charge de l’authentification par JWT, la documentation des routes via Swagger/OpenAPI ou encore la prise en charge de CORS. N’hésitez pas à aller voir la liste des plugins sur la documentation officielle et d’aller jeter un oeil au code source.
Benchmark
Je vous ai dit en début d’article que Fastify était le framework web Node.js le plus rapide du marché. Mais à quel point ? Je vous laisse juger par vous-même avec ce benchmark issu du site officiel :
Framework | Nombre de requêtes par seconde |
Fastify | 64683 req/sec |
Koa | 41324 req/sec |
Restify | 34424 req/sec |
Hapi | 30830 req/sec |
Express | 11795 req/sec |
Pour finir…
Fastify est l’un des frameworks les mieux maintenus à l’heure actuelle et dispose d’une communauté très active. Le développement est également soutenu par la fondation OpenJS qui est également derrière Node.js. Cela fait maintenant plus d’un an que j’utilise Fastify et c’est pour moi l’un des frameworks web avec lequel je prends le plus de plaisir à travailler du fait de sa simplicité et de son système de plugins qui offre de nombreuses possibilités.
Nous avons fait dans cet article, qu’un bref tour d’horizon de Fastify, je vous laisse le soin de poursuivre la découverte en allant lire la documentation officielle qui est pour le coup vraiment bien fichue.
Si des petits articles de découvertes de librairies/frameworks comme celui-ci vous plaît, n’hésitez pas à me le dire !
Je suis lead developer dans une boîte spécialisée dans l’univers du streaming/gaming, et en parallèle, je m’éclate en tant que freelance. Passionné par l’écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J’adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs.
Si vous avez envie de papoter, n’hésitez pas à me retrouver sur Twitter, m’envoyer un petit email ou même laisser un commentaire.
Oui ça nous plaît, encore 🙂
Bonjour, C’est parfois un peu compliqué pour comprendre, dans le site même de Fastify, ce qui est un fichier server.js ou ce qui est un fichier routes.js, etc.
L’utilisation de fichiers plugins portant la mention « module.exports = » correspond généralement au nom du fichier (.js).
Le codage, ici, que vous proposez en exemple pour les plugins peut s’inclure directement à l’intérieur dans le fichier server.js .
Si l’on veut le placer à l’intérieur d’un module, pour faire un fichier myPlugin.js . Si je comprends bien le didacticiel de Fastify et votre explication, je dois placer toute la portée du plugin dans le fichier, et ajouter « module.exports = myPlugin.js » ?
Comment dois-je appeler ce plugin dans le fichier server.js afin qu’il y soit utilisé?
Que dois-je placer en en-tête de fichier de plugin, pour ce qui est des fichiers requis (const n = require(‘…’), car si je dénomme par exemple mon instance « app », dans le fichier server je vais avoir « const app = require(‘fastify’)() » , mais si j’utilise ce « app » en tant que nom d’instance, Node va me dire que c’est une variable non-définie, et il va falloir donc que je redéfinisse de la même façon app dans ce fichier plugin? Si je veux appeler mon instance « desapp », par exemple, dans le fichier plugin.js , est-ce que ça va poser un problème dans le fichier server pour l’identification de l’instance?
La question que je me pose est que si je définis une instance « app » dans le fichier server puis une instance « app » dans un fichier plugin, alors, je génère la même instance ou bien je génère deux instance que je dois spécifier comme identiques dans le fichier server?
Ca fait trois questions, en fait, de ma part.
Mon fichier server contient la totalité des routes, avec quelques hooks qui semblent fonctionnels quand je teste mon instance.
Je voudrais cependant ajouter un plugin contenant la fonction writeFile tel que le site O’Reilly en propose un exemple dans l’utilisation des FileSystem (leur fonction s’appelle on Fs(fs), en fait, si vous regardez leur site).
Merci.