Node for API: Architecture et Ecosystème d'Express et Hapi
Dans mon précédent article j'exposais les raisons pouvant nous amener à opter pour la plateforme Node.js pour réaliser des API REST. Plutôt que de réimplémenter la roue au-dessus des fonctionnalités bas niveau du coeur de Node, le choix d'un framework s'impose.
Au sein de l'écosystème Node, deux frameworks tiennent le haut du pavé pour la réalisation d'API: express et hapi. Dans cet article nous allons étudier leur architecture ainsi que leur histoire, leur dynamique et communauté.
Les Concurrents
Ces deux frameworks se dégagent quant à la réalisation de services REST. Le premier, Express est plus orienté framework d'application Web. Quant au second Hapi, il est - comme son nom le suggère - axé sur la réalisation d'API.
Pourquoi ce choix?
Hapi et Express ont tout d'abord été sélectionnés en raison de leur importance dans l'écosystème, et de leur utilisation dans des projets d'envergure, à dimension industrielle. De plus ceux-ci ont une approche bien différente, qu'il est intéressant d'étudier. Ceci est en partie lié au fait qu'Hapi est API centric, alors qu'Express est plus orienté Framework Web.
Nous avons écarté dans cette étude les frameworks construits au-dessus d'express, ceux-ci sont très orienté MVC et introduisent des conventions pour libérer le développeur de nombreuses décisions. Les plus conséquents dans cette catégorie sont Kraken, Sails, Locomotive ou Loopback. Parmi les autres recalés, il y a Koa, celui-ci étant une revisite d'express à la sauce Es6 tirant partie des générateurs, et Restify qui a quelques similarités avec Express tout en étant plus API centric, sans avoir une portée comparable à Hapi.
Rentrons maintenant dans le vif du sujet, et commençons par Express.
Quelques Statistiques
Avant de rentrer dans le vif du sujet, voici quelques statistiques pour se faire une première idée de l'aura des deux frameworks.
Métriques | Express | Hapi |
---|---|---|
Github stars | 19k | 4,2k |
Github fork | 3,6k | 0,6k |
StackOverflow | 15k | 180 |
Contributor | 177 | 114 |
Github require | ~360k | 6k |
Que veulent dire ces chiffres? Quelle interprétation en tirer? Difficile de dire quoi que ce soit sans avoir un idée du contexte et de la portée des frameworks. Par exemple ceci exprime que les deux frameworks n'ont pas la même ancienneté, ce qu'illustre ce google trend. Une analyse qualitative des deux frameworks est nécessaire, tant en terme d'architecture et d'écosystème. C'est le but de cet article.
Les présentations
Histoire de satisfaire son (éventuelle) envie de voir du code - et avoir un premier contact avec les deux frameworks - voici ici deux exemples minimaux tirés des documentations officielles.
Hello Express
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.send('hello world');
});
var server = app.listen(3000, function () {
console.log('Example app listening at http://%s:%s',
server.address().address, server.address().port);
});
Hello Hapi
var Hapi = require('hapi');
var server = new Hapi.Server();
server.connection({ port: 3000 });
server.route({
method: 'GET',
path: '/',
handler: function (request, reply) {
reply('Hello, world!');
}
});
server.start(function () {
console.log('Server running at:', server.info.uri);
});
Express
Express est un cadriciel à la fois simple et flexible; la documentation officielle le décrit comme un minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
Architecture
S'il fallait résumer Express, je dirais que c'est un micro-framework bien conçu au dessus des fonctionnalités web du serveur http de node.js, une API qui se charge principalement de router les requêtes aux bons middlewares.
Le framework n'est pas opiniâtre, il n'impose pas de structure rigide. Il ne s'appuie pas sur le principe Convention over Configuration, étant flexible tant pour la structure que pour les middlewares utilisés. En effet un des motos d'express est @batteries not included. C'est l'écosystème de middlewares construit autour de connect puis express qui apportent les fonctionnalités.
Middleware
Le concept de middleware prédate express et a été introduit dans node par connect. Attention, middleware n'est pas à interpréter au sens large d'intergiciel, mais dans le sens introduit entre autres par Rack, une interface ruby pour le serveur Web.
Le principe est d'éviter d'avoir un handler monolytique qui se charge de gérer l'intégralité d'une requête. A la place on va utiliser une collection de handlers qui se chargent de faire un traitement spécifique, et qui font être chaînés, un peu comme une pipeline unix.
Concrètement, un middleware est une fonction avec trois arguments. Les deux premiers items req
et res
sont là pour accéder aux informations de la requête, et pour manipuler la réponse. Le dernier est une callback next
pour éventuellement passer la main au prochain middleware de la chaine. Voici un exemple simple de middleware, qui se chargera juste de logger l'heure courante, avant d'invoquer le prochain chaînon.
function (req, res, next) {
console.log('Time:', Date.now());
next();
}
Construire un serveur revient donc à chaîner les middlewares, prétraitements, pour accumuler des informations avant de traiter les différents cas, selon notre logique métier. Ainsi on a des middlewares pour gérer l'authentification, logger les requêtes, parser les cookies et corps des requêtes, régler certains headers de la réponse et ainsi de suite jusqu'à l'émission de la réponse.
Attention par contre, l'ordre de ces middlewares est crucial et critique, il faut donc bien les chaîner dans l'ordre qu'il convient. De plus on évitera de mélanger la logique métier avec des considérations transverses, ou liées à HTTP comme la journalisation, ou la gestion du cache. En réalité, il y a deux grand types de middlewares, ceux pour gérer le cas nominal, et ceux pour gérer les cas d'erreurs. On bascule en mode erreur dès qu'un middleware en appelant next
avec un argument, l'erreur. A cela s'ajoute les handlers de requêtes placés en bout de chaîne qui ne manipulent que la requête et la réponse.
Routage et sous applications
Le routage est le coeur de la fonctionnalité apporté par express: Il s'agit d'associer à une route particulière - c'est à dire à des pattern d'url et des verbes http particuliers - les middlewares et handlers qui se chargeront de traiter les requêtes. Ce routage n'est pas restreint à des routes statiques, on peut avoir recours à des routes dynamiques ce qui permettra de récupérer des variables de chemin (pathvariables). L'algorithme de routage est assez simple, pour chaque requête entrante on va suivre la chaîne de middlewares, et exécuter seulement ceux dont la route va s'appareiller avec la méthode et l'url demandées.
Par conséquent, il faut prendre soin de mettre les plus spécifiques avant les plus générales sachant que la première route qui matche l'emportera.
A noter qu'il est aussi possible de construire des sous-applications, des sous routeurs que l'on viendra monter sur son serveur final. Ceci permet ainsi de découper son api et de concevoir des composants réutilisables comme par exemple un module d'administration.
Ecosystème
Historique
Express est le plus ancien des deux frameworks, d'ailleurs son développement a commencé en 2009 dès les débuts grand public de node. Un premier jalon en 2010, une beta en juillet, suivie de plusieurs release candidates jusqu'en novembre avec la publication de la v1.
Construit à l'origine sur connect
et largement inspiré par Sinatra
(Sinatra inspired web development framework), il s'est vite établi comme le framework web de référence sur la plateforme node. Son principal developpeur fut tj
, TJ Holowaychuk travaillant à VisionMedia notamment à l'origine des bibliothèques nodejs superagent et supertest. Dernièrement le flambeau a été repris par @dougwilson
, Tj se concentrant sur go, et transférant le dépot à StrongLoop (compagnie centrée sur Node à l'origine LoopBack un des frameworks construit sur express).
Rythme de développement
Express est actuellement en version 4, la version 3 étant toujours maintenue. Le passage de la 3 à la 4 correspond à la suppression de connect
en tant que dépendance, et de l'extraction des middlewares dans des modules spécifiques. Chaque passage de version majeure est documenté à la fois avec la liste des nouvelles fonctionnalités (v3, v4) et un guide de migration (v2 vers v3, v3 vers v4).
Une version 5 est en préparation sur sa propre branche. La release RTM étant prévu pour ce mois de juillet, en attendant les versions alphas sont déjà disponible sur npm, npm install express@5.0.0-alpha.1
. Pour plus de détails, on se réfèrera à la Pull Request #2235 qui détaille les changements et améliorations prévues.
Communauté
De part sa philosophie nodesque, batteries not includes express apporte un nombre limité de fonctionnalités. Celles-ci sont apportées par les middlewares qui ont été développés en grand nombre. Ceux-ci vont faciliter les différents aspects que peut nécessiter la bonne réalisation d'une appli Web et/ou API REST et répondent aux différents besoins. Les principaux sont répertoriés sur le site officiel.
La communauté d'express est assez conséquente. Il s'agit du framework MVC le plus populaire ce qui se constate à la fois par le nombre d'étoiles du projet, et de questions stackoverflow. D'ailleurs le E de MEAN du nom de la stack javascript - annoncé par certains comme le successeur de LAMP - correspond à Express.
Niveau communication, en plus du site vitrine, qui contient aussi tutoriel et documentation, le projet est bien entendu hébergé sur github. Pour engager la communauté, il existe un google group dédié ainsi qu'un channel irc #express et un chat gitter. De nombreux cours sont disponibles en lignes, dont celui de nodeschool expressworks.
Hapi
Hapi est un rich framework for building applications and services. hapi enables developers to focus on writing reusable application logic instead of spending time building infrastructure.
Architecture
La philosophie d'hapi se distingue d'express avec des choix que l'on pourrait qualifier de radicalement différents. En effet, Hapi abstrait le serveur HTTP node. Il introduit un cycle de vie des requêtes et propose une autre architecture pour favoriser l'isolation des différents composants de l'application et ainsi éviter les impacts inattendus.
Configuration over code
Hapi a une approche centrée configuration. Mis à part la création des handlers de requête et leur logique métier, construire le serveur reviendra à configurer les différentes propriétés et fonctionnalités du serveur, les plugins chargés, et surtout les différentes routes de notre API.
La spécification de route en hapi est le meilleur exemple pour illustrer cette différence d'approche avec express. Dans ce dernier, les routes sont ajoutées via différentes méthodes pour chaque verbe http. A contrario, l'api d'Hapi offre une seule méthode qui va prendre en argument un object de configuration décrivant la route.
// express
app.get('/my-route', myHandler)
// hapi
server.route({path:'/my-route', method: 'GET', handler: myHandler})
On retrouvera cette approche d'objets de configuration à la fois pour le serveur et les différents plugins que l'on chargera.
Les avantages de cette approche sont nombreux. Ceci permet notamment à de nombreuses préoccupations comme les performances, la robustesse et la sécurité, d'être traitées nativement sans compromettre la flexibilité. Ceci permet aussi grâce à la réflexion de produire facilement de la documentation, de générer du code. On peut aussi facilement configurer le comportement du serveur dans les différents environnements en changeant juste les paramètres. Ainsi à titre d'exemple, on pourra aisément désactiver la mise en cache en production, ne pas activer certaines surcouches comme la journalisation des requêtes pour les test. Ceci permet aussi de déployer plusieurs version de l'application, de facilement mettre en place un système de feature flipping ou de gérer l'évolution de certaines parties du serveur.
Architecture de plugin et modularization
Hapi s'est construit en réaction à un des problèmes d'express, le fait qu'il ne passait pas à l'échelle d'un point de vue organisationnel. En effet, express permet de construire une API très rapidement, cependant ceci se fait au dépend de la maintenabilité. Notamment le montage des routes est un goulot d'étranglement où il faut coordonner les changements au risque de grosses surprises. Hapi et son système de plugin ont été conçus pour éviter cela et offrir un cadre pour écrire des applications réutilisables de façon très modulaire.
Un plugin est un composant logique, réutilisable, et indépendant apportant un certain nombre de fonctionnalités aux serveurs. Pour développer les fonctionnalités transverses, qui s'appliqueront à l'ensemble des requêtes, hapi donne un certain nombre de points d'entrée sur le traitement de la requête. Plutôt que de concevoir le traitement d'une requête par une suite de fonctions définies par l'utilisateur, Hapi définit un cycle de vie de la requête, avec ses points d'extension associés. C'est sur ces événements - comme la réception de la requête, la fin de l'authentification, le début d'émission de la réponse - que l'on peut accrocher des traitements. Pour ce faire il s'appuie sur l'api d'EventEmitter de Node.js.
Il existe un bon nombre de plugins "natifs", développés par l'équipe derrière hapi, et reconnaissables (ou pas) à leur nom assez original. Parmi les quasi indispensables il y a Joi pour la validation de schéma, Good pour le monitoring et logging (avec une extension pour le rejeu), aussi tv pour le debuggage intéractif, Bell pour l'authentification tierce.
Les plugins ne se réduisent pas aux fonctionnalités transverses, c'est un cadre pour découper le code, et c'est d'ailleurs la manière recommandée de structurer son application. En terme de structure, Hapi offre aussi un moyen d'injecter des dépendances, de mettre à disposition des objets et fonctionnalités à l'ensemble des parts du serveur.
Une bonne synergie
Les deux approches de configuration et de plugin se nourrissent mutuellement. Ainsi Hapi offre un cadre que l'on pourra facilement étendre et configurer, cadre offrant nativement sécurité et robustesse et nombre de fonctionnalités que l'on attend d'un serveur web: cache, authentification, validation, réflection….
De part l'aspect configuration, et le fait d'avoir des handlers en un seul bloc - en faisant abstraction des points d'extension bien définis - le routage est totalement déterministe. Ceci est une grande force d'Hapi, et surtout cela rend les collisions de routes détectables. Le cas présent le serveur ne démarrera pas, et affichera une erreur explicite alors que dans express ce problème potentiel est totalement silencieux et invisible, un vrai "Middleware hell".
Ecosystème
Historique
Hapi est né bien plus récemment au Lab Walmart avec une équipe dédiée sous la houlette de Eran Hammer aka hueniverse. Le projet a commencé en août 2012, né des frustrations et limitations de l'utilisation des outils existants. N'ayant pas trouvé un framework améliorable dans le sens souhaité, l'équipe est partie from scratch, reprenant quelques bonnes pratiques mises en place sur un précédent projet, sledge. Le principal grief contre express est le passage à l'échelle d'un point de vue organisationnel. Le montage des routes est un goulot d'entrainement où il était fréquent que des membres d'équipe se marchent les uns sur les autres en insérent de nouvelles routes.
Dans un premier temps basé sur express en proposant une abstraction au dessus, Hapi s'en est rapidement détaché. Après une série de version mineures, la première version majeure est sortie en avril 2013 faisant suite à la V0.13. Hapi passe avec brio son baptême de feu lors du black friday 2013 à Walmart, sur lequel Eric Hammer est revenu dans quelques talks et entretiens.
Une v2 est sortie en Jan 2014, suivant la v1.20 en raison de changements incompatibles pour raisons de sécurité. S'en suivent au cours de l'année 2014 de nombreuses versions majeures, en raison de changements non retrocompatibles, occasionnés par l'extraction de nombreuses fonctionnalités dans des modules séparés et d'une refonte d'api.
Rythme de développement
Ces 8 versions majeures - 7 versions majeures en 10 mois - en un laps de temps si court peuvent donner l'impression d'une certaine instabilité. Cependant Hapi est jeune, la SemVer pousse à vite bumper la version majeure une fois la v1 sortie. De plus ces changements sont très bien documentés, chacun étant associé à une estimation du temps de mise à jour, de la complexité, des risques, et des dépendances!
La version actuelle, v8, a été un refactoring majeur du framework et de son api. Les différentes interfaces existantes pour configurer le serveur ont été unifiées. C'est ainsi que des méthodes comme pack
ont disparu (un pack étant la composition de plusieurs serveurs en un unique objet). L'objectif était celui de simplifier l'expérience de développement en rendant l'api plus simple et prévisible. D'améliorer l'expérience Développeur en gagnant en simplicité et apprenabilité sans perdre en puissance. Ce gros refactor a été fait dans l'état d'esprit des "breaking changes worth taking" revendiqué par le mainteneur principal. La communication sur la v8 laisse à penser qu'il est peu probable d'avoir de nouvelle version majeure dans les mois qui viennent. A titre d'exemple, voici l'estimation associée à la dernière version majeure publiée:
- Upgrade time : moderate to high - a couple of days to a week for most users
- Complexity : moderate - a very long list of changes, all well understood with no side effects
- Risk : moderate - no side effects but a lot of changes to keep track of
- Dependencies : moderate to high - every plugin must be verified to be compatible or upgraded
On retrouvera ainsi les notes de sortie des versions majeures de l'année 2014: v8 en novembre, v7 en octobre, v6 en juin, v5 en mai, v4 en avril, v3 en mars.
Communauté
Bien que né a Wallmart, Hapi a su s'en détacher, et attirer une communauté conséquente autour de lui. Néanmoins il est indéniable que son créateur garde pour l'instant un leadership certain sur la vie du framework. Cependant ceci n'a pas empêché 900 pull requests d'être acceptées, et 21 personnes composent actuellement l'organisation hapijs. Le dépôt initialement sur le compte de WallmartLab a été migré sur cette organisation. Celui-ci contient bien entendu le framework hapi, mais aussi l'ensemble des plugins de l'écosystème, et le code source du site contenant la documentation et divers tutoriels.
Toute la dynamique et le développement du plugin se fait via github et ses issues ce qui permet à la fois visibilité et centralisation des discussions. Tout cela est organisé grâce à un système de tag très bien pensé des issues: bug, security, dependency, discussion, documentation, release notes, breaking changes, question, request, test.
Comme Express il dispose d'un channel IRC #hapi, d'un chat gitter, et d'un cours nodeschool makemehapi. A noter aussi l'existence d'un programme de mentoring pour assister les développeurs dans leur montée en compétence sur hapi.
Hapi et Express: la Synthèse
Si on revient à une analyse naïvement, purement quantitative des métriques, express se détachait nettement d'hapi. Cependant on vient de voir que ceci est en grande partie dû à des raisons historiques. De plus ceci concerne la communauté et la visibilité, plus que l'utilisation d'un point de vue industriel.
Si express est pas mal utilisé dans nombreuses startups ayant opté pour node, difficile à trouver des projets d'ampleur, et de grandes entreprises l'utilisant. D'ailleurs netflix l'a abandonné suite à des problèmes de performance à grande échelle . A contrario Hapi est utilisé par de nombreuses boites commerciales d'envergure listées sur leur site: notamment Disney, mozilla, Paypal, npm. Sans parler bien entendu de Wallmart.
Cela rejoint la recommandation qui émerge de cette analyse qualitative quant au choix d'un framework node pour la réalisation d'API REST. Dans le cadre d'un POC, avec une équipe réduite qui a déjà une expérience d'express, celui-ci fera aisément le job. Cependant pour un projet d'ampleur, que ce soit pour la taille de l'équipe, la durée, ou l'audience, il est avisé d'opter pour Hapi.