Node.js

Une formation Delicious Insights assurée par Christophe Porteneuve

whoami


            const christophe = {
              family: { wife: 'Élodie', sons: ['Maxence', 'Elliott'] },
              city: 'Paris, FR',
              company: 'Delicious Insights',
              trainings: [
                'Node.js', 'React PWA', 'TypeScript', 'React Avancé', 'TypeScript Avancé',
                'ES Total', 'Git Total'
              ],
              jsSince: 1995,
              nodeSince: 2009,
              claimsToFame: [
                'dotJS',
                'Prototype.js',
                'script.aculo.us',
                'Bien Développer pour le Web 2.0',
                'NodeSchool Paris',
              ],
            }
          

Bonjour Node !

Ça faisait longtemps que je voulais te voir

L’historique

en 5 minutes

nov. 2009 Annonce officielle (Ryan Dahl @ JSConf.EU)
juin 2010 Nodejitsu
nov. 2010 Express 1.0
mai 2011 npm 1.0
déc. 2011 Windows Azure supporte Node.js
2013–2015 Déferlante de gros usages (eBay, PayPal, LinkedIn, Walmart, Uber, Medium, Yahoo!, IBM…). Support officiel dans VS.
Aujourd’hui > 2,5M+ modules dans npm (+1000/j, 55B DLs/sem.), Top-5 PaaS, conférences JS/Node pratiquement toutes les semaines.

Mon 1er programme


          const path = require('path')
          const os = require('os')

          const nodeVersion = process.version
          const hostName = os.hostname().split('.')[0]
          const progName = path.basename(__filename)

          console.log('Hello world!')
          console.log('This is', progName, 'running on', hostName, 'using Node', nodeVersion)
        

Exécuter un fichier JS


          node index.js
          Hello world!
          This is index.js running on CodeForTheWin using Node v20.7.0
        

Un peu plus loin


          import { createServer } from 'http'

          createServer((request, response) => {
            response.writeHead(200, { 'Content-Type': 'text/plain' })
            response.end(new Date().toString())
          }).listen(3333, () => {
            console.log('Server running at http://127.0.0.1:3333/')
          })
        

Shebangs

et fichiers « exécutables »

  1. Bit d’exécution
    chmod +x richer-program.js
  2. Shebang en tout début de fichier :
    #! /usr/bin/env node

Partout… sauf sur Windows (ou alors WSL).

Découpage en modules

C’est fini les conneries

CommonJS

C’est tout bateau

Le format historique, synchrone, popularisé par Node

On parle en fait de CommonJS/1.0 (plein d'extensions…)

Très facile à comprendre :


exports.key = value

mod = require('pathspec')

CommonJS

C’est tout bateau

Des vrais modules

qui défoncent

Exporter

Niveau langage, donc analysables statiquement ; pas de soucis de dépendances circulaires grâce à l’export de live bindings.

Mode strict par défaut (comme les corps de classes).


                    // Exports explicites, nommés, à la volée
                    export function addTodo(text) {
                      return { type: types.ADD_TODO, text }
                    }

                    export const ADD_TODO = 'ADD_TODO'

                    // Export par défaut (un seul par module)
                    export default Footer

                    // Exports a posteriori
                    export { Component, Factory, makeHigherOrder }
                  

                    // Export renommé
                    export { each as forEach }

                    // Ré-export (délégation)
                    export * from './lib/cool-module.js'

                    // ou, plus ciblé :
                    export {
                      makeHigherOrder as wrap,
                      Component
                    } from 'toolkit'
                  

Importer


              // Import intégral dans un « namespace »
              import * as types from '../constants/ActionTypes.js'

              // Import nommé de l’export par défaut, imports homonymes d’exports nommés
              import React, { PropTypes, Component } from 'react'

              // Ou juste un des deux modes :
              import classnames from 'classnames'
              import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters.js'

              // Import renommé
              import { makeHigherOrder as wrap } from 'toolkit'
            

Modules Node

  • Basé CommonJS (enrobage, mise en cache et chemins)
  • Près de 40 modules noyaux
  • Hiérarchie de node_modules apportée par npm
  • Permet l’enregistrement de chargeurs supplémentaires

Node et les ESM

Ayé, on les a

  • Considéré stable depuis la 15, a démarré avec la 10…
  • Extension .mjs ou package.json parent avec "type": "module"
  • Les imports relatifs/absolus exigent l’extension de fichier et ne résolvent pas implicitement les dossiers seuls.
  • L’import dynamique asynchrone (import(…)) est possible
  • import.meta fournit les métadonnées (URL du chemin, notamment)
  • Bonne interopérabilité avec des sources CJS (export par défaut et, le plus souvent, exports nommés)
  • Le package.json d’un module peut contrôler ses exports ESM séparément via son champ exports.

Chemins de recherche

  1. Modules noyaux
    
                    const fs = require('node:fs') // ou require('fs'), mais soyons explicites
                    // import fs from 'node:fs' (export par défaut)
                    // import * as fs from 'node:fs' (consolidation des exports nommés)
                  
  2. Modules locaux par hiérarchie node_modules
    
                    const { invoke } = require('underscore')
                    // import { invoke } from 'underscore'
                  
  3. Modules locaux relatifs
    
                    const HumanView = require('./views/human_view') // .default si on requiert un ESM
                    // import HumanView from './views/human_view.mjs'
                  
  4. Modules locaux absolus
    
                    require('/usr/shared/node_modules/house_framework')
                    // import '/usr/shared/node_modules/house_framework/index.js'
                  

Chargeurs de modules

et syntaxes alternatives

Évitez de fournir une extension explicite dans le require


  1. Exploitation des chargeurs complémentaires par extension de fichier (ex. CoffeeScript, TypeScript…)
  2. Fichier à extension .js, .json ou .node (binaire)
  3. Dossier avec…
    1. Fichier package.json doté d’une clé main
    2. Fichier index.js
    3. Fichier index.json
    4. Fichier index.node

L’asynchrone

c’est pas si dur

Rappels sur les callbacks

Les callbacks, ou fonctions de rappel, existent depuis ES1 (fonctions de premier ordre).
ES2 a ajouté les fonctions d’ordre supérieur, qui permettent de les passer en arguments.

Traditionnellement, on passe un callback unique pour un traitement asynchrone one-shot, ou associé à un type d’événement.

La valeur de retour du callback est généralement sans importance.

En raison de leur exploitation asynchrone, les callbacks ne proposent pas le flux habituel de propagation des exceptions (ils se déclenchent généralement en racine de stack), de sorte que pour faire « remonter » une erreur, on doit souvent la repasser manuellement en argument. Ainsi, Node.js a établi une convention error-first sur tous les callbacks hors événements :


            function loadConfig(cb) {
              fs.readFile(CONFIG_PATH, { encoding: 'utf-8' }, (err, str) => {
                if (err) {
                  return cb(err)
                }
                return cb(null, JSON.parse(str))
              })
            }
          

Fastidieux et bourré de risques…

Problèmes intrinsèques des callbacks (1/7)

Le Callback Hell / Pyramid of Doom :


            db.users.findById(req.session.userId, (err, user) => {
              // -- snipped: error forwarding --
              user.posts.findRecent({ page: req.params.page, limit: PAGE_SIZE }, (err, posts) => {
                // -- snipped: error forwarding --
                posts[0].comments.findRecent((err, comments) => {
                  // -- snipped: error forwarding --
                  res.render('users/show', { user, posts, comments })
                })
              })
            })
          

Problèmes intrinsèques des callbacks (2/7)

Exceptions et long stacks :


            try {
              fs.readFile('foo.xml', { encoding: 'utf-8' }, (err, xml) => {
                if (err) {
                  throw err // Hu-ho…
                }
                JSON.parse(xml) // => Lève une `SyntaxError`
              })
            } catch (err) {
              // Jamais atteint !  L’exception est levée hors de ce contexte.
            }
          

Problèmes intrinsèques des callbacks (3/7)

La poussière sous le tapis :


            fs.readFile('config.json', { encoding: 'utf-8' }, (err, json) => {
              // Et hop, on oublie de traiter l’erreur ou de la refiler…
              processConfig(JSON.parse(json))
            })
          

Problèmes intrinsèques des callbacks (4/7)

Après l’heure, c’est plus l’heure :


            notifier.emit('ready')

            notifier.on('ready', () => {
              // Jamais appelé : l’événement a été déclenché avant…
            })
          

Problèmes intrinsèques des callbacks (5/7)

Appels uniques qui sont en fait multiples :


            function connect(uri, cb) {
              if (__connection) {
                process.nextTick(cb, null, __connection)
                // Oubli de `return`, par exemple…
              }

              db.setup(uri, (err, cnx) => {
                if (err) {
                  return cb(err)
                }
                __connection = cnx
                cb(null, __connection)
                // Et hop, 2e fois !
              }
            }
          

Problèmes intrinsèques des callbacks (6/7)

Appels conflictuels (erreur + succès) :


            function connect(uri, cb) {
              db.connect(uri, (err, cnx) => {
                if (err) {
                  cb(err)
                  // Oubli de `return` !
                }

                __connection = cnx
                cb(null, __connection)
              })
            }
          

Problèmes intrinsèques des callbacks (7/7)

Zalgo


            function connect(uri, cb) {
              if (__connection) {
                return cb(null, __connection)
                // cb synchrone !
              }
              db.connect(uri, (err, cnx) => {
                if (err) {
                  return cb(err)
                  // cb asynchrone
                }

                __connection = cnx
                cb(null, __connection)
                // cb asynchrone
              })
            }
          

Promesses (1/10)

Les promesses, abstraction déjà ancienne, blindent contre une bonne partie de ces pièges.

Une promesse est un objet qui représente l’aboutissement d’un traitement asynchrone.

Elle ne transtionne qu’une seule fois, dans un seul sens (pas d’appels multiples / en conflit).

L’appel est garanti (même « après la bataille »), et garanti asynchrone (pas de Zalgo).

Les levées d’exceptions sont capturées et converties en rejets de promesse.

Promesses (2/10) : Promises/A+

Standard de facto des promesses en JS. Domenic Denicola, 2012*. Simple comme tout. Pour commencer :

  • On construit une promesse avec new Promise(), qui utilise le revealing constructor pattern.
  • Une promesse démarre en état pending, et peut transiter une seule fois vers un état settled, à savoir fulfilled ou rejected**
  • La primitive de chaînage de base est la méthode then(), qui prend jusqu’à 2 callbacks, optionnels : succès et erreur. Ils sont garantis asynchrones et exclusifs.

Une tonne de bibliothèques proposent des promesses compatibles, notamment Bluebird. Et c’est natif en ES2015.

* Lui a pratiquement valu son statut de membre permanent du TC39…
** Terminologie FR fréquente : en attente, établie, accomplie, rejetée.

Promesses (3/10) : API de base


            function readConfig(path) {
              return new Promise((resolve, reject) => {
                fs.readFile(path, { encoding: 'utf-8' }, (err, text) => {
                  if (err) {
                    return reject(err)
                  }
                  try {
                    resolve(JSON.parse(text))
                  } catch (err) {
                    reject(err)
                  }
                })
              })
            }

            readConfig('config.json')
              .then((obj) => obj.user.email)
              .then((email) => console.log('EMAIL:', email), (err) => console.error('OOPS:', err))
              .catch((err) => console.error('DAMNED:', err))
          

Promesses (4/10) : flexibilité

Toute exception levée, une fois la chaîne de promesse en place, est convertie en promesse rejetée.

Toute valeur renvoyée qui n’est pas une promesse est convertie en promesse accomplie via Promise.resolve()

Le renvoi d’une promesse l’incruste dans la chaîne : c’est la composition de promesses.

On peut même incruster un « thenable » : un objet doté d’une méthode then() compatible…


            fetch('https://jsonplaceholder.typicode.com/users/1')
              // Incrustation de promesse intermédiaire (équiv. waterfall, sans l’imbrication)
              .then((res) => res.json())
              // La déstructuration ci-dessous va échouer, car `names` n’existe pas.
              // La `TypeError` va être convertie en promesse rejetée.  Mais si ça avait marché,
              // l’objet renvoyé aurait été converti en promesse accomplie.
              .then(({ names: { first, last }, website }) => ({ first, last, website }))
              // On ne verra jamais ce log, car il correspond à un callback de succès / accomplissement
              .then(console.log)
              // En revanche, on verra celui-ci, la chaîne étant en rejet à ce stade.
              .catch((err) => console.error('OOPS', err))
              // …et celui-ci aussi, `catch()` neutralisant le rejet, comme dans un `try`…`catch` classique.
              .then(() => console.log('DONE'))
            

Promesses (5/10) : meilleures pratiques

Blinder l’initialisation de la chaîne


            function setupCall() {
              gabuzomeu() // => ReferenceError
              return new Promise((resolve, reject) => resolve(42))
            }

            // Cette chaîne ne protègera pas de l’erreur, qui a lieu hors du périmètre de la promesse…
            setupCall()
              .then(console.log)
              .catch((err) => console.log('OOPS', err))
          

On préfèrera donc ceci :


            function setupCall() {
              return Promise.resolve()
                .then(() => {
                  gabuzomeu() // => ReferenceError, deviendra un rejet
                  return new Promise((resolve, reject) => resolve(42))
                })
            }
          

Promesses (6/10) : meilleures pratiques

Toujours finir par un catch()

D’un côté, la capture automatique des levées d’exception au sein d’une chaîne de promesse est pratique, car elle permet une certaine préservation de contexte et améliore la robustesse. De l’autre, si on oublie le catch() final, elle passe sous le tapis…

Dans Node, au même titre que process.on('uncaughtException') pour les exceptions non rattrapées et les événements 'error' non traités, on a process.on('unhandledRejection'). Depuis Node 7, ne pas traiter les exceptions rejetées est officiellement déprécié (code DEP0018), et on s’attend prochainement à ce que ça fasse comme pour les exceptions et erreurs : par défaut, ça terminera le processus avec un code d’erreur.

C’est notamment utile pour capturer des problèmes qui surviendraient dans le dernier callback (que ce soit succès ou erreur).


            Promise.resolve()
              // Premier rejet
              .then(gabuzomeu)
              // Le 2e callback ici gère l’erreur… mais il est défectueux lui-même !
              .then(console.log, (err) => shadok(err))
              // Ouf, heureusement qu’on avait l’attrape-tout final.
              .catch((err) => console.error('OOPS', err))
          

Promesses (7/10) : meilleures pratiques

Paralléliser intelligemment


            Promise.all([
              Entry.getEntries(filter),
              Entry.tags(),
              Entry.count(),
            ])
              .then(([entries, tags, count]) => res.render('entries/index', { entries, tags, count }))
          

Ça sera encore plus criant avec async / await.

Dans les rares cas où on veut du “first past the post”, on utiliserait Promise.race()

Promesses (8/10) : antipatterns

Imbrication excessive

Quand on croit que les promesses sont juste une autre manière d’écrire des callbacks, on a tendance à reproduire le style…


            fetch('https://jsonplaceholder.typicode.com/users/1')
              .then((res) => {
                return res.json()
                  .then((obj) => {
                    return db.persistUser(obj)
                      .then((id) => res.redirect(`/my-users${id}`)) // BEUARK™
                  })
              })
              .catch(console.error)
          

Ça vient souvent d’une incompréhension de l’incrustation de promesse (composition) :


            fetch('https://jsonplaceholder.typicode.com/users/1')
              .then((res) => res.json())
              .then((obj) => db.persistUser(obj))
              .then((id) => res.redirect(`/my-users${id}`))
              .catch(console.error) // SWEET!
          

Promesses (9/10) : limitations actuelles

Les promesses ES2015 ne sont pas annulables : quand c’est parti, c’est parti

Dans le même esprit, il n’y a pas de timeout.

Ce genre de chose est simulable, en mode Powerbidouille™, avec Promise.race(). Par exemple :


            return Promise.race([
              originalPromise,
              new Promise((resolve, reject) => {
                setTimeout(() => reject(new Error('Timeout')), timeout)
              })
            ])
          

Un chantier phénoménal, Cancellation API, végète au stade 1 depuis juillet 2018 en attendant de voir si AbortController et AbortSignal gagnent la partie. Node 15+ les prend notamment en charge pour les flux en lecture, timers et événements. Côté browser, c'est surtout Fetch qui s’en sert.

Promesses (10/10) : limitations intrinsèques

Une promesse représente un traitement one-shot, comme un appel réseau ou un calcul asynchrone. Ce n’est pas la meilleure primitive pour traiter un flux de données, ou un flux d’événements.

On préfèrera alors les observables, généralement au travers de l’excellent RxJS. Ces derniers sont également utilisables pour les cas one-shot, même si c’est généralement moins pertinent en isolation.


            Rx.Observable
              .timer(0, 5000)
              .take(10)
              .map(() => Math.floor(Math.random() * 5) + 1)
              .flatMap((x) => Rx.Observable.ajax({
                crossDomain: true,
                responseType: 'text',
                url: `https://baconipsum.com/api/?type=all-meat&paras=${x}&format=html`,
              }))
              .subscribe((req) => { container.innerHTML = req.response })
          

async / await (1/5)

La révolution. Intègre la gestion des promesses directement dans la syntaxe, ce qui les rend éminemment plus lisibles
(et présente même de jolis bonus en performances). ES2017 / evergreens / Node 7.6 / Babel.

Principe

Une fonction peut être déclarée async (mot-clé devant sa déclaration/expression).

Elle renvoie alors implicitement une promesse (tout son code est enrobé dans une chaîne de promesse).

Son code immédiat (sa portée racine) peut utiliser le mot-clé await pour
suspendre sa propre exécution le temps qu’une autre promesse / fonction asynchrone s’exécute.
Cette suspension est non-bloquante : le thread courant récupère la main pour autre chose.

Une fois la promesse établie, à la première opportunité, la fonction « dégèle ».
Si la promesse est accomplie, on récupère sa valeur. Sinon, l’exception de rejet est levée.

Alors, ça remplace les promesses ?!

Absolument pas. En fait, c’est entièrement basé sur les promesses, ça leur est intrinsèquement lié. Mais cette syntaxe nous permet de faire du code non-bloquant qui ressemble à du code bloquant (et donc est extrêmement simple à lire) : on récupère les if-else, les boucles, le try…catch, etc.

async / await (2/5) : exemples concrets


            async function showPost(req, res) {
              try {
                const post = await db.posts.find(req.params.id)
                const author = await post.author.find()
                const comments = await post.comments.find({ limit: 10 })
                res.render('posts/show', { post, author, comments })
              } catch (err) {
                handleError(err)
              }
            }
          

            import { delay, limit, map } from 'awaiting'

            async function superDuper(req, res) {
              await delay(500)
              const user = await limit(User.find(req.params.id), 2000)
              const payloads = await map(req.params.urls, 3, fetchText)
              await user.processPayloads(payloads)
              res.json({ result: 'success' })
            }
          

async / await (3/5) : antipatterns

Le séquençage inutile d’opérations indépendantes les unes des autres, « façon code bloquant »


            const entries = await Entry.getEntries(filter)
            const tags = await Entry.tags()
            const count = await Entry.count()
          

Alors on parallélise, et c’est 😍 :


            const [entries, tags, count] = await Promise.all([
              Entry.getEntries(filter), Entry.tags(), Entry.count()
            ])
          

async / await (4/5) : antipatterns

Le return await débile


            async function fetchJSON(url) {
              return await fetch(url).then((res) => res.json())
            }
          

Le problème ici, c’est que cette fonction va suspendre son exécution jusqu’à avoir récupéré le contenu et l’avoir parsé. Ça empêche, préemptivement, toute parallélisation utile au niveau du code appelant, ce qui est super dommage.

Il suffit de renvoyer une promesse non-attendue ; ici spécifiquement, async-await est même superflu !

En revanche, return await est souvent ce qu’on veut au sein d’un try…catch maison. ESLint est au courant. En savoir plus

async / await (5/5) : itérer comme un manche

« Génial, ça devient bloquant et tout ! » 🤦🏼‍♀️


            const animations = [animPromise1, animPromise2, animPromise3 /*, … */]
            // EPIC FAIL
            const results = animations.map(async (anim) => await anim)
          

Une fonction async enrobe son traitement dans une promesse, et renvoie celle-ci. Ici, on obtient un tableau de (nouvelles) promesses (au lieu de leurs résultats), et les animations restent parallélisées. C’est assez simple à faire en séquentiel :


            // Dans une fonction async…
            const results = []
            for (const anim of animations) {
              results.push(await anim())
            }
          

…mais si nos promesses / fonctions asynchrones sont en fait indépendantes, et qu’on souhaite juste les résultats dans l’ordre tout en parallélisant, ce séquençage forcé est contre-productif, on préfèrera Promise.all(). Et si on veut traiter au fil de l’eau, sans attendre la toute fin mais en respectant l’ordre, ES2018* nous couvre :


            for await (const anim of anims) { /* … */ }
          
* Dispo dans Babel, Fx57, Cr63, Saf TP, Node 9.2+

npm

Node Packaged Modules

Packaging de modules

npmjs.com

Le bébé d’izs (Isaac Z. Schlueter), ex-lead Node

Racheté en 2020 par GitHub (lui même faisant partie de Microsoft)

Le référentiel officiel des « paquets » node

À l’origine des fichiers package.json

Trivial à utiliser (y compris comme auteur de paquet)

Vital à l’écosystème node/JS, au même titre que GitHub

Installer en local


            npm install async
            (barre de progression…)
            added 1 package, and audited 2 packages in 583ms

            found 0 vulnerabilities

            npm install --save joi # Défaut pour npm5+ ; version courte : -S
            (barre de progression…)
            added 11 packages, and audited 12 packages in 971ms

            found 0 vulnerabilities
          

Installer en global

Lorsqu'un module fournit des « exécutables » auxquels on souhaite accéder de n’importe où (ex. npm, Docco, RequireJS, Uglify, CoffeeScript…), il est préférable de l’installer en global.

On découpe de plus en plus fréquemment le paquet …-cli pour l'outil global, et le paquet de base pour l'implémentation locale (ex. Babel, Grunt, Ember, etc.)


            npm install --global gtop
            (…)
            added 62 packages, and audited 63 packages in 4s
          

Un module peut attirer l’attention là-dessus en activant son drapeau preferGlobal.

Le fichier

qui va bien

man package.json
  • Facile à créer/remplir avec npm init
  • Champs obligatoires : name et version ( semver)
  • Manifeste : files
  • Champs utiles : homepage, repository, bugs
  • Attribution : author (nom, e-mail, URL), contributors (idem) et license
  • Exploitation : main, scripts, config, bin et man
  • Référencement : description, keywords
  • Plus tard : directories (lib, bin, man, doc, example)

Dépendances

de développement, de production…

  • Tout le temps : dependencies ( --save)
  • En bonus appréciable : optionalDependencies ( --save-optional)
  • Collaborateurs : peerDependencies
  • Pour dev/contrib : devDependencies ( --save-dev)
  • Exigences de version node/npm : engines
  • Exigences de plate-forme : os, cpu
  • Syntaxe étendue basée semver, permet aussi des cas avancés basés URL/Git/GitHub

Commandes npm

  • Examen de la registry : search, view
  • Installation et mise à jour : install, update, ls
  • Exécution : run * + raccourcis (notamment start et test), config (overrides persistants de réglages par défaut)
  • Interaction : docs, home, bugs
  • Création : init, publish

* officiellement run-script

Node event loop

Objets globaux

  • global : le scope global
  • process et console (modules homonymes)
  • Buffer, issue du module buffer
  • setTimeout, clearTimeout, setInterval et clearInterval, issus de timers
  • require, module et exports si en CJS ou CLI (import() si en ESM ou CLI)
  • __dirname et __filename si en CJS (import.meta.url si en ESM)
  • 🌍 WebAssembly (8+)
  • 🌍 URL et URLSearchParams (10+)
  • 🌍 queueMicrotask, TextDecoder et TextEncoder (11+),
  • performance (14+),
  • 🌍 AbortController, Event, EventTarget, MessageChannel, MessageEvent, MessagePort (15+)
  • 🌍 structuredClone, DOMException, fetch / Request / Response / Headers / FormData et la crypto web (17 / 17.6+)
  • 🌍 Une tonne d'API de flux web (18+)

Flux & buffers

Le cœur I/O de Node

Buffers

Parce qu’il n’y a pas que String et Number dans la vie.

String stocke forcément en ucs2 (UTF-16LE)

Number est forcément un Double avec l’ endianness du système

En passant par Buffer, on a deux avantages :

  • Une lecture précise des octets (ex. uint8, floatLE, uint32BE)
  • Une performance accrue (tampons mémoire continus hors du tas de V8)

Modes de flux

flowing vs. non-flowing

  1. < 0.10 : « old mode » (ou « flowing mode ») basé sur l’événement data.
  2. 0.10 : « streams2 » et « non-flowing mode », basé sur readable et méthodes read([size]) et pause().
    Tout flux est en pause à la base, mais on autorise le retour au flowing, il suffit d’écouter data).
  3. 0.11+ : « streams3 » : en gros, ce qu’on avait compris pour streams2, et meilleur cohabitation des modes.
  4. Événement end dans les deux modes, quand les données sont totalement épuisées.

Types de flux

  • En lecture ( Readable), écriture ( Writable) ou R/W ( Duplex), un cas spécial de R/W étant les flux de transformation ( Transform).
  • On peut écrire nos propres flux, la doc détaille bien comment faire.
  • De nombreux modules (via npm) fournissent des tas de flux super utiles, notamment dans le contexte de pipes, pour consommer, transformer ou produire des flux. Le plus célèbre est sans doute event-stream, véritable couteau suisse.
  • Node 18+ propose aussi tous les flux WHATWG standard (ReadableStream, WritableStream, TransformStream, etc.) si vous souhaitez écrire du code universel. Des convertisseurs entre flux Node et flux WHATWG sont mis à disposition.

Flux flowing


            import { createReadStream, createWriteStream } from 'fs'
            import { fileURLToPath } from 'url'

            const fileName = fileURLToPath(import.meta.url)
            const src = createReadStream(fileName)
            const dest = createWriteStream(`${fileName}.copy`)

            src.on('data', (chunk) => dest.write(chunk))

            // En fait ça c'est optionnel, mais histoire d’être explicite…
            src.on('end', () => dest.end())
          

Flux non-flowing


            import { createReadStream, createWriteStream } from 'fs'
            import { fileURLToPath } from 'url'

            const fileName = fileURLToPath(import.meta.url)
            const src = createReadStream(fileName)
            const dest = createWriteStream(`${fileName}.copy`)

            src.on('readable', () => {
              let buf
              // Attention, cette boucle gère mal la *backpressure*
              while ((buf = src.read())) {
                dest.write(buf)
              }
            })

            // En fait ça c'est optionnel, mais histoire d’être explicite…
            src.on('end', () => dest.end())
          

Encodages de textes

Un flux fournit (ou reçoit) des Buffers par défaut.

Pour travailler directement avec des Strings, il suffit de lui associer un encodage (le plus souvent utf-8), comme chaque fois qu’on travaille avec un Buffer.


            import { createReadStream } from 'fs'
            import { setTimeout as delay } from 'timers/promises'

            const src1 = createReadStream('./lorem.txt')
            const src2 = createReadStream('./lorem.txt')

            // KO:
            for await (const buf of src1) {
              process.stdout.write(buf)
              console.log('\n', '-1'.repeat(10))
            }

            await delay(5000)

            // // OK:
            src2.setEncoding('utf-8')
            for await (const str of src2) {
              // str est une String, et non un Buffer ; par ailleurs, pas de découpage à tort
              // d'un codepoint au sein du texte.
              process.stdout.write(str)
              console.log('\n', '-2'.repeat(10))
            }
          

Pipes

  • Dans la pratique, il est très fréquent de vouloir connecter deux flux ensemble, soit pour un transfert de données (ex. flux de lecture d’un fichier vers flux d’écriture d’une connexion réseau), soit pour une transformation (ex. parser JSON, compresseur GZip).
  • À faire à la main en étant nickel, c’est chaud patate ! Notamment pour gérer la backpressure.
  • Mais .pipe fait ça très bien :-)
  • Un flux source peut être piped vers plusieurs destinations en parallèle ! (par exemple une pour l'upload S3, une pour le stockage disque temporaire, une pour le log…)

Pipes


            import { createReadStream, createWriteStream } from 'fs'
            import { fileURLToPath } from 'url'

            const fileName = fileURLToPath(import.meta.url)
            const src = createReadStream(fileName)
            const dest = createWriteStream(process.argv[2] || `${fileName}.copy`)

            src.pipe(dest)
            src.pipe(process.stdout)
          

            // Sinon on peut faire un wget…

            import { createWriteStream } from 'fs'
            import { get } from 'http'

            const [
              src = 'http://delicious-insights.com/training.json',
              dest = 'sessions.json'
            ] = process.argv.slice(2)

            get(src, (res) => res.pipe(createWriteStream(dest)))
          

Stream all the things!

Stream Handbook (npm)

Stream Adventure (npm)

Stream Playground

Déboguer du node

confortablement

Bon, console.*, ça va bien 5 minutes…

Un vrai débogueur visuel, c’est possible ? Bien sûr !

node --inspect + Chrome DevTools

Visual Studio Code

Sécurité

Bonnes pratiques de base

Mises à jour de Node

Une majeure tous les 6 mois

Les versions paires, en avril, deviennent LTS en octobre

Nombreuses mises à jour mineures / patchlevel pour sécurité ;
au moins tous les deux mois, comme par exemple ici pour août 2023.
Effectué sur toutes les LTS (maintenance et active) et l’actuelle,
donc en ce moment (début octobre 2023) les branches 18 et 20.

Dépréciation occasionnelle des API noyau présentant un danger, telles que new Buffer(…), domain, fs.exists(…) ou process.binding(). On peut demander à ESLint de nous les signaler.

npm audit (fix)

npm Inc. a racheté en avril 2018 ^Lift Security et la Node Security Platform. Cette dernière a cessé de fonctionner seule le 30/09/2018 dernier. Du coup, les fonctionnalités de nsp sont intégrées directement à npm lors des installations et mises à jour :

feedback de npm audit

npm audit fix permet souvent d’ajuster nos dépendances pour corriger les problèmes ; certaines corrections peuvent nécessiter une action manuelle, qui est souvent décrite dans le rapport détaillé.

Depuis octobre 2017, GitHub permet par ailleurs de signaler les vulnérabilités (JS et Ruby, notamment) sur la branche principale de vos projets, ce qui est assez pratique. Les abonnements de niveau Enterprise (ne pas confondre avec GitHub Enterprise, la solution on-premise) ont aussi accès à GitHub Advanced Security, qui fournit notamment CodeQL pour JS.

Les alertes sécurité de GitHub

snyk

Snyk* est un service orienté CI qui supervise lui aussi vos dépendances à la recherche de vulnérabilités ; c’est un complément intéressant à npm audit, et il propose des intégrations CI, UX, GitHub, etc. qui sont sympa.

Extrait de la page d’accueil de Snyk

* qui signifie « So Now You Know… », tout en étant un jeu de mot sur sneak peek

Node Certified Modules

Si vous avez besoin de blinder absolument l’utilisation des modules tiers.

Outil desktop + registry sécurisée qui audite en permanence les publications de module sur tout un tas de critères, et bloque l’installation des modules problématiques.

Fait partie de la plate-forme NodeSource (avec N|Solid, une surcouche de la runtime Node), et est donc payant au-delà d’un utilisateur.

Scripts postinstall (et autres)

Par défaut, lorsque npm installe des modules, il exécute leurs scripts de cycle de vie associés (prepublish*, prepare, preinstall, install et postinstall).

Ces scripts peuvent contenir tout et n’importe quoi, aussi vous pouvez choisir une approche un peu bourrine qui consiste à ne pas les exécuter :


            npm install --ignore-scripts
          

Gardez toutefois à l’esprit que de nombreux paquets ont du travail valide voire nécessaire dans leurs scripts d’installation (builds d’extensions binaires, etc.), aussi le mieux reste de « faire vos devoirs » sur les modules que vous choisissez d’installer.

* Déprécié depuis npm@4

Gestion des credentials et secrets

Par secret, on entend toute donnée dont la connaissance par un tiers peut créer une vulnérabilité (mots de passes, identifiants, clés API, tokens, clés de chiffrement, URLs authentifiées de connexion…)

Ces données ne doivent jamais figurer dans le code ni être versionnées.

Elles doivent être fournies par l’environnement (au sens POSIX du terme).

En production, le PaaS / serveur hôte les fournira (configuration Heroku, AWS Secrets Manager, Azure Key Vault, GCP Cloud KMS…).

En développement, on utilisera généralement un fichier d’environnement non versionnable, souvent de type .env, lu par dotenv-safe.

Principes généraux de stockage de données

Avec le RGPD, c’est encore plus brûlant…

  1. On ne stocke jamais un mot de passe en clair
    (protection contre le vol / la compromission de base de données)
  2. On ne communique jamais le mot de passe, même en confirmation d’inscription.
    (protection contre la consultation frauduleuse des e-mails / courriers)
  3. On ne chiffre jamais un mot de passe de façon réversible
    (en cas d’oubli, on fournit un lien périssable de modification)
  4. On ne stocke jamais de donnée personnelle en clair
    (protection contre le vol / la compromission de base de données)
  5. On ne demande aucune donnée dont on n’a pas immédiatement besoin

Choisir intelligemment

ses algorithmes de chiffrement

Mots de passe

Hachage Bcrypt* avec un salt adapté et 10–16 rounds en production (2–4 en dev). Pour du contenu trop long, on peut pré-hacher en SHA512. La référence est bcryptjs, plus facile à installer/builder que bcrypt.

Démo live

Tu peux carrément la jouer au top avec Argon2id si tu y tiens… (par exemple avec ça)

PII (Données personnelles)

Classiquement : e-mails, n°s de Sécurité Sociale, de CNI, de passeport, de permis de conduire, de téléphone.

Chiffrage symétrique avec une clé secrète et un IV dynamique, en AES256** ou, au pire, AES192 ou AES128.

Démo live

* Oubliez MD* et SHA*, trop vulnérables — ** Oubliez RC4, DES, 3DES…

sqreen

sqreen est un service qui s’intègre directement à votre codebase pour repérer à la volée toute tentative d’attaque (injection ORM/ODM, XSS, DDoS) ou d’activité suspecte des utilisateurs, et les empêcher de fonctionner.

Il fonctionne en analysant les requêtes qui partent vers les bases de données, en regard notamment des paramètres de la requête web / API en cours, mais aussi en appliquant des règles métier sur tout un tas de critères, comme la fréquence d’appels par un même utilisateur à certains points d’accès, etc.

Extensible à coup de plugins, il offre de nombreuses possibilités.

Modèle SaaS, avec une couche gratuite amplement suffisante pour se faire la main.

Développer une API REST

Récupérer le socle applicatif


            git clone https://github.com/deliciousinsights/toptunez
            cd toptunez
            npm install
          

Quel framework ?

🚫 Juste Express ?

Non, il y aurait vraiment trop de choses à rajouter par-dessus.
Idem pour Hapi (dont l’avenir semble hélas compromis), et Koa (même 2) reste à la marge.

Si vous y tenez absolument, un dépôt de départ (boilerplate pour du REST basé 100% Express est disponible,
mais je ne le recommande pas.

Restify

C’est la référence pour les API REST en Node, avec d’excellentes performancess et beaucoup de fonctionnalités intégrées.

Utilisé notamment par npm, Netflix, Pinterest… La doc est un peu spartiate mais ça vaut le coup.
Notez aussi que c'est un moteur indépendant, pas une surcouche d’Express.

Par ailleurs, pour évoluer vers du GraphQL on n’aura pas besoin de remettre ce choix en question :
la v2 du serveur Apollo se pose très bien par-dessus.

En direct : ✅ Routes ✅ Versions ✅ Hypermédia ✅ Rate limiting ✅ Formats variables ✅ Aide code client

Via plugins : 📦 Optimisation cache HTTP 📦 Validation des requêtes entrantes 📦 Middlewares / Services

Fastify

Centré sur la perf, avec une architecture modulaire et scopée très intéressante.

A atteint une maturité et un écosystème suffisants, ce serait mon choix par défaut aujourd'hui.

Pas encore eu le temps de migrer cette formation dessus, désolé 😳. Mais le jour où je le fais, je vous notifierai pour accéder aux nouvelles ressources !

Rappels sur REST

Le point le plus connu : les ressources

Une ressource est définie par une URL de base, à partir de laquelle on peut déduire des points d’accès conventionnels, par méthode HTTP + suffixe normalisé.

Supposons une URL de base /posts pour des articles :

Méthode URL Action
GET /posts Listing (filtrable par query string)
POST /posts Create : création initiale de la ressource
GET /posts/:id Read : contenu de la ressource
PATCH /posts/:id Update : mise à jour partielle de la ressource
DELETE /posts/:id Destroy : suppression de la ressource
PUT /posts/:id Replace : remplacement (idempotent) de tout le contenu de la ressource. En pratique, trop de projets traitent PUT comme un PATCH…

Rappels sur les codes HTTP

Y’a pas que 200, 302 et 404 dans la vie !

Utiliser intelligemment les codes HTTP dans les réponses permet une meilleure gestion automatique des clients.

Quelques exemples importants :

201 Created est le code officiel de réponse à une création réussie.
On complète souvent d’un en-tête Location avec l’URL de lecture de la ressource.

204 No Content est un code utile pour toute modification réussie qui n’appelle pas de corps de réponse
(ex. pas de données normalisées renvoyées).

400 Bad Request indique une erreur syntaxique des données entrantes, par exemple du JSON malformé.

422 Unprocessable Entity indique une erreur sémantique des données entrantes :
par exemple, des champs manquants ou une entité identifiée incompatible avec le traitement demandé
(pour une erreur due aux droits d’accès de l’utilisateur, préférer le 403 Forbidden).

Hypermédia et HATEOAS

REST va bien au-delà des simples conventions d’URL et de codes HTTP… La navigation dynamique autour d’une réponse (pagination, entités et manipulations associées…) est aussi très pratique pour avoir des API « découvrables ». On parle alors d’HATEOAS, nom pas ragoûtant du tout, et cette évolution est pas mal décrite dans cet article.

Si le mode d’envoi des liens complémentaires n’est pas standardisé, deux approches fréquentes consistent soit à ajouter des éléments link au sein du corps structuré de réponse (JSON, YAML, XML…), soit à ajouter des en-têtes HTTP Link: avec la même information (ce qui n’est sémantiquement possible que pour une navigation de listing, ou lorsqu’on est sur une ressource bien définie).

L’API v3 de GitHub est un bon exemple d’utilisation de ces diverses approches.

Tester une API REST

Supertest reste la meilleure option,
qu'on peut combiner tranquillement avec Jest (avec ou sans snapshots des réponses) par exemple.

Certain·e·s préfèrent faire leurs tests E2E avec Cypress,
mais c'est surtout utile pour des pages web, pas vraiment pour des API pures.

Versions

Une API, ça change avec le temps. On renvoie / exige des champs différents, on ajoute et on enlève des trucs… Pour éviter de tuer tous les clients de notre API, on va donc devoir versionner.

Il y a deux approches : des préfixes d’URL ou des en-têtes de requête HTTP. Cette deuxième approche est plus versatile, ne serait-ce que parce qu’elle évite de devoir maintenir le jeu complet des URLs communes pour chaque version, par exemple. En revanche, quand une URL se comporte différemment d’une version à l’autre, le client peut demander une version spécifique par en-ête de requête.

Là aussi, GitHub est un bon élève :


          # Requête :
          Accept: application/vnd.github.v3+json
          …

          # Réponse :
          Content-Type: application/json; charset=utf-8
          X-GitHub-Media-Type: github.v3

MongoDB

Stockage moderne

Persistance mongoDB

intro


            mongo
            MongoDB shell version v4.4.6
            connecting to: mongodb://172.24.96.1:27017/test?…
            Implicit session: session { "id" : UUID("…") }
            MongoDB server version: 4.4.3
            …
            >
          

            Current Mongosh Log ID: …
            Connecting to:          mongodb://172.24.96.1:27017/test?…
            Using MongoDB:          4.4.3
            Using Mongosh Beta:     0.12.1
            …
            >
          

Persistance mongoDB

Opérations CRUD

Depuis 3.2, on préfèrera :


            insertOne(doc[, options])
            insertMany(docs…[, options])

            updateOne(query, doc[, options])
            updateMany(query, docs…[, options])
            replaceOne(query, doc[, options])

            deleteOne(query[, options]) / deleteMany(query[, options])
          

Persistance mongoDB

avec Mongoose

  • Schémas et modèles ; pattern Active Record
  • Queries et Query builder
  • Promesses

Clients graphiques pour MongoDB

Développer une API GraphQL

Petit topo sur GraphQL

En gros, c’est le futur des API. REST était les balbutiements, GraphQL est l’évolution suivante.

Piloté par Facebook mais 100% FLOSS.

Fortement typé et validé.

Le client précise exactement ce qu’il veut, et ne récupère que ce dont il a besoin, en une seule requête même si elle traverse des ressources*.

Utilisé par tous les grands : Facebook, GitHub / GitLab / Atlassian / JetBrains, PayPal, Shopify / Magento, Twitter, Yelp, LinkedIn, Netflix, Zillow, Expedia, Lyft, Airbnb, PluralSight / Coursera / Codecademy, Rakuten, Cloudinary, MongoDB…

(En France, citons notamment 20 Minutes et Dailymotion.)

Effet de bord amusant : rarement besoin de versionner du GraphQL, on évolue sans cassure en dépréciant éventuellement des champs.

Concepts de GraphQL

Concept Descriptif rapide
Type Structure de données stricte avec des champs typés
Query Description d’un requêtage possible de données sur un type
Mutation Description d’une modification possible de données sur un type
Subscription Similaire aux queries, mais notifie des mises à jour en temps réel
Schéma Ensemble de définitions de types, queries, mutations et subscriptions

Types GraphQL

Un type strictement défini et validé. GraphQL distingue les types scalaires, notamment ceux fournis de base (Int, Float, String, Boolean, ID et le plus souvent Date) ; les types énumérés, définis manuellement ; et les types objets, pour nos structures de données, par exemple ceci :


            type User {
              birthday: Date
              email: String!
              name: String!
              password: String
              role: Role!
              tags: [String!]!
              token(regen: Boolean = false): String
            }
          

Le suffixe ! indique que le champ ou élément concerné ne peut être null. L’ordre des champs n’est pas significatif.

On peut factoriser des définitions en tant qu’interfaces ou avec des types unions.

Queries GraphQL

Une query est un descripteur de toutes les requêtes (R/O) de données du domaine envisagé.


            type Query {
              allUsers(order: SortOrder = ALPHABETICAL): [User!]!
              user(email: String!): User
            }
          

Côté requêtage, on peut par exemple demander :


                query {
                  allUsers { name }
                }
              

(Pour que le client puisse requêter des objets et champs, les descripteurs query associés doivent avoir été prévus au schéma)

Ou de façon plus personnalisée :


                {
                  allUsers(order: RECENT_FIRST) {
                    createdAt
                    email
                    name
                  }
                  user(email: 'christophe@delicious-insights.com') {
                    name
                    role
                  }
                }
              

Mutations GraphQL

Là où une query requête, une mutation modifie (tout type d’altération imaginable).


            type Mutation {
              createUser(user: UserInput!): User!
            }
          

Côté utilisation, on pourrait faire par exemple :


            mutation {
              createUser(user: { name: 'John', email: 'john@example.com', role: USER }) {
                token
              }
            }
          

Une mutation GraphQL peut toujours renvoyer des données, par exemple, la version à jour, normalisée, d’informations transmises, mais aussi les ajustements opérés côté serveur sur des données connexes.
On évite ainsi des A/R supplémentaires avec le serveur.

Subscriptions GraphQL

Flux de notifications temps réels des modifications qui nous intéressent. Similaire aux change streams de CouchDB, MongoDB, etc. mais plus granulaire.


            type Subscription {
              newUser(role: UserSubscriptionRole = ALL): UserSubscriptionPayload
            }
          

Pour s’inscrire :


            subscription {
              newUser {
                email
                name
                role
              }
            }
          

Qu’est-ce qui s’exécute, en vrai ?

La magie des resolvers

GraphQL est agnostique quant au langage de développement, toutefois il définit l’algorithme d’exécution des queries, mutations et subscriptions. C’est en fait une « simple » traversée récursive de fonctions positionnées sur leurs types respectifs. Tout étant clairement typé, le lookup est plutôt facile à chaque fois.

Par exemple, la requête suivante…


                query {
                  allUsers { name }
                }
              

…est typée comme suit, explicitement :


                query: Query {
                  allUsers: [User!]! { name: String! }
                }
              

On peut dérouler la liste des appels de resolvers nécessaires, qui sont des fonctions renvoyant des objets ou promesses :


            Query.allUsers(root, context) -> users
            for each user in users
              User.name(user, null, context) -> name
          

Tellement de modules…

C’est un peu le foutoir

Le cœur du truc, inaliénable, utilisé de toutes façons, c’est graphql.

Comme ça fournit le noyau, mais que c’est pas hyper pratique à l’usage, on utilisera souvent, directement ou de façon intégrée (ex. Apollo), graphql-tools, notamment pour faire le lien entre le schéma lui-même, ses exécutants (les fameux resolvers) et l’algorithme d’exécution.

Les outils (et leurs modules) type GraphiQL sont des sortes d’EDIs web minimalistes pour explorer un schéma, qui servent de base à des solutions comme GraphQL Playground.

Le module historique express-graphql est obsolète, pour des tas de raisons, même s’il reste dans les tutos d’origine.

Apollo 4

Apollo est la plate-forme de référence pour faire du GraphQL, côté serveur comme côté client*.

Il se vend en premier lieu comme un « adaptateur » permettant de proposer un point d’accès unique GraphQL par-dessus nos ressources légataires : APIs REST, microservices / fonctions, etc. Mais en fait il peut aussi taper directement dans les données, donc être une alternative, ou un complément, à l’existant. Ça permet une mise à niveau incrémentale, en tout cas !

Apollo 1 était bien mais pas parfait, du coup on avait tendance à ajouter du sucre comme graphql-yoga, ou carrément à utiliser son principal concurrent, Prisma (anciennement Graphcool), dont le positionnement est pourtant très différent.

Apollo 2 était une énorme mise à jour qui a fait référence, et Apollo 3 poursuivait le mouvement.

Apollo 4 vise à découpler les intégrations avec l'écosystème du travail noyau.

En somme, on a besoin de graphql, @apollo/server et éventuellement notre propre couche côté serveur, et @apollo/client pour le client. Tout l’outillage avancé est fourni avec !

GraphQL et VSCode

On vous recommande chaudement l’extension GraphQL ; installez-la si ce n’est pas déjà fait !

Coloration syntaxique (fichier et TTS gql)

Autocomplétion

Validation à la volée vis-à-vis du schéma

…et j’en passe !

Conçevoir un schéma pertinent

Un schéma GraphQL, au début, c’est comme quand on a commencé à faire des modèles de données SGBD/R, puis quand on a commencé à faire du NoSQL (ex. MongoDB), etc. : on fait des boulettes classiques.

Une modélisation qui semble parfaitement raisonnable à première vue peut vite poser des problèmes de duplication, d’utilisabilité, de performance, de responsabilités…

À lire absolument, en prenant le temps : le guide de Shopify sur le sujet.

Plus générique et concis, mais très sympa aussi : le guide d’Apollo.

La règle d’or : le client d’abord. Ce sont les besoins opérationnels du côté client qui doivent piloter le schéma ! C’est un renversement d’approche par rapport à REST, notez bien… (et à mon sens, c’est une des failles de l’approche de Prisma). Il ne faut toutefois pas sacrifier le domaine métier sur l’autel de l’UI, qui peut changer d’un client à l’autre ; c’est un arbitrage. Mais ce qui est sûr, c’est que les détails d’implémentation et l’historique d’API ou de dépendances n’ont aucun rôle à jouer.

Principe n°2 : n’y mettre que ce dont on a besoin maintenant.
Ajouter des données et opérations sera trivial, en retirer le sera beaucoup moins.

Concevoir un schéma évolutif

Auditer la performance

Apollo Studio est un énorme outil en ligne qui permet notamment de monitorer finement vos performances, de faire des alertes, recos, etc.

Par ailleurs, de nombreuses voies d'optimisation sont possibles côté serveur et côté client.

Tester une API GraphQL

Notre stack de test habituelle marche toujours !

On peut simuler les données sur base d’un schéma avec le mocking Apollo (depuis le serveur ou directement avec graphql-tools), très pratique pour mettre au point le client indépendamment du développement serveur, isoler de la base ou permettre un Storybook…

Explorer GraphQL plus avant

Le site officiel (.org)

Un excellent agrégat de ressources, tutos, études de cas… (.com)

GraphQL is the new REST : bouquin, codes sources, vidéos et exos approfondis
par John Resig et Loren Sands-Ramshaw (payant) (.guide)

How To GraphQL : excellente intro vidéo/texte et tutos interactifs par techno

Le site officiel Apollo est bourré d’infos, docs et démos utiles

Robin Wieruch a aussi deux tutos détaillés autour d’Apollo :
un côté client avec React, et un côté serveur au sein d’Express.

Sécuriser une API

Valider les requêtes REST entrantes

Il est impératif de valider la structure, et idéalement les valeurs, des éléments de la requête entrante (params, query, body…).

Validation générique de JS / JSON

Les deux principales approches sont :

joi, issu de l’écosystème Hapi

Le standard JSON Schema, exploité via ajv

Pour Restify spécifiquement

On trouve aussi deux approches principales :

Le plus complet est node-restify-validation, mais il a gelé et cherche de nouveaux mainteneurs ;
on a aussi, plus léger mais moins puissant, restify-request-validator. Les deux ont besoin d’un ajustement vis-à-vis du dernier Restify, dont le routeur a changé. On le fait dans notre code.

DI est en train de boucler un travail de reprise avec mise à jour intégrale et correctifs pour tous les tickets et pull requests proposés au fil du temps dans restify-fresh-validation.

Valider les requêtes GraphQL entrantes

Champs et valeurs

La phase de validation inhérente à GraphQL, couplée à son typage fort, offre un premier niveau de protection non négligeable. Apollo Server en rajoute une couche, mais l’injection reste possible si on n’est pas soigneux dans le design de nos mutations.

On recommande de recourir intelligemment aux scalaires personnalisés* et à leurs validateurs sur-mesure, en complément potentiel d’une validation structurelle plus générique.

DoS

Il existe ausi un risque de saturation au moyen de relations cycliques et de la profondeur arbitraire des graphes résultats.

Si toutes les requêtes potentielles viennent de nos propres codes sources (et non librement d’utilisateurs tiers), on peut créer des listes blanches de requêtes acceptables grâce à persistgraphql. Sympa, mais pas sans inconvénients…

Dans tous les cas, imposer des limites de complexité et de profondeur est une bonne idée.

Rate limiting

REST ou GraphQL, imposer un Rate Limiting est une bonne idée.
Dans l’idéal, les en-têtes de réponse gardent le client informé de sa progression, du genre :


            HTTP/1.1 429 Too Many Requests
            …
            X-RateLimit-Remaining: 0
            X-RateLimit-Limit: 5
            X-RateLimit-Rate: 1
          

Restify a un plugin interne parfaitement adapté, qui peut travailler par IP distante, IP via proxy (X-Forwarded-For), ou utilisateur logique, gère la notion de burst et peut utiliser une couche de stockage externe pour gérer le multi-process/instance.

Pour une API GraphQL, on considère souvent que les limitations de complexité sont suffisantes ;
GitHub, par exemple, implémente deux limites basées sur le nombre de nœuds résultats et le « coût » (exprimé en points, dérivé de la complexité) des requêtes.

En complément, Restify propose un plugin interne de limitation basé sur l’historique de consommation CPU, qui est plutôt un dernier recours.

Authentification

Qui es-tu ?

Distinction Sign Up / Sign In ou pas ?

Besoin d’une entité User en base, sauf à déporter toute la gestion ailleurs (OAuth 2.0 typiquement).

Dans une API, on n’utilise pas tellement les cookies, et donc les sessions… On authentifie chaque requête, généralement via un en-tête Authorization, le plus souvent dans sa forme Bearer (pas tellement parce qu’on fait de l’OAuth 2.0, mais parce qu’on utilise un token). On peut tout à fait utiliser un en-tête étendu à nous, genre X-API-Token, si on préfère…

Quel type de token ? Ce qu’on veut, en vrai. JWT a le vent en poupe, mais c’est à manipuler avec des pincettes, surtout en termes de forçage d’expiration. Le module clé est jsonwebtoken (on a aussi fast-jwt).

Avec Restify, on utilisera restify-jwt-community ; pour GraphQL avec Apollo, il faudra fournir un habilleur de contexte, qui examine la requête HTTP pour équiper le contexte d’une clé (classiquement user), afin de filtrer dessus au global, ou plus granulairement l’examiner dans les resolvers.

Autorisation

Qu’as-tu le droit de faire ?

REST

Rien de top existant pour faire du RBAC avec Restify, mais c’est assez facile à la main, avec un champ role dans User et un middleware générique incrustable dans les gestionnaires de routes…

GraphQL

Le truc top moumoute, c’est d’y aller par directives personnalisées, granulairement, au niveau du schéma. Ça pète grave. Y’a une bonne page de doc chez GraphQL Tools sur le sujet.

CORS

Rappels

Permet à un serveur d’indiquer quelles origines (« qui ») peut venir consulter ses données, utiliser ses APIs, etc.

Basé sur des en-têtes HTTP (Origin, Access-Control-*)

REST

Pour Restify, le plus simple est d’utiliser restify-cors-middleware2

GraphQL

Apollo 3 gérait ça directement avec l’option cors du constructeur ApolloServer (en standalone), mais depuis Apollo 4 ça nécessite de passer par un framework serveur tiers (la doc et notre propre code illustrent ça pour Express, mais ça peut être géré par Fastify, etc.).

Stockage sécurisé

Eh, pas de mots de passe en clair, tu te rappelles ?

Eh, les PII sont chiffrées, pas vrai ?

Aucun problème

Le module mongoose-pii gère ça de façon simple et transparente


            const { markFieldsAsPII } = require('mongoose-pii')
          

            userSchema.plugin(markFieldsAsPII, {
              fields: ['email', 'firstName', 'lastName', 'mfaSecret'],
              key: process.env.MONGOOSE_PII_KEY,
              passwordFields: 'password'
            })
          

            await User.authenticate('christophe@delicious-insights.com', 'secret')
            // => Ben `null`, hein, j’ai pas un mot de passe aussi pourri !
          

2FA / MFA

On utilisera généralement :

otplib pour générer les secrets, générer et vérifier les tokens, générer les URIs d’enregistrement pour les apps d’authentification (type Authy, ou en moins bien Google Authenticator ou MS Authenticator)

qrcode pour générer des QRCodes (en image, en Data URI…) afin de faciliter l’enregistrement dans les apps

Un middleware (REST) ou habilleur de contexte (GraphQL) pour confirmer le second facteur, généralement depuis un en-tête HTTP étendu (genre X-TOTP-Token)

Déployer un serveur API

Déployer « à la main »

On réfléchit au niveau serveur (les nôtres ou de l’IaaS)

On déploie de façon automatisée, répétable et réversible, par exemple avec Capistrano. Oubliez ce !@# de FTP !

En revanche, il faut faire en sorte que notre application…

démarre toute seule au lancement du serveur

redémarre toute seule en cas de problème ou suite à une mise à jour

évite dans ce dernier cas les interruptions de service

stocke convenablement ses logs

tire parti de tous les processeurs de la machine

etc.

En avant PM2 !

Déployer sur des PaaS

On réfléchit au niveau codebase et services tiers (notamment la base MongoDB).

On déploie par git push, sur l’hébergeur ou sur nos branches de recette / production, qui déclenchent une pipeline.

Toute la partie DevOp est gérée pour nous : déploiement, supervision, et surtout… élasticité de charge !

Y’a pratiquement pas un seul PaaS qui ne prenne en charge Node.js (parfois même que lui) :

AWS, Azure, Google Cloud Platform (GCP), Heroku, Vercel,
🇫🇷 Clever Cloud, Linode, platform.sh, AppFog, OpenShift, EngineYard, Joyent, Bluemix…

bit.ly/node-secrets

Déployer sur Heroku

J’adore Heroku, mais d’une force !

Pour l’admin, je préfère leur ligne de commande


            heroku --version
            heroku/7.16.5 darwin-x64 node-v10.10.0

            heroku login
            …

            heroku apps:create --addons=mongolab:sandbox --region=eu toptunez
            Creating ⬢ toptunez... done, region is eu
            Adding mongolab:sandbox... done
            https://toptunez.herokuapp.com/ | https://git.heroku.com/toptunez.git

            heroku config:set APOLLO_ENGINE_API_KEY=ser… JWT_SECRET=111… MONGOOSE_PII_SECRET=4a5…
            Setting APOLLO_ENGINE_API_KEY, JWT_SECRET, MONGOOSE_PII_SECRET and restarting ⬢ toptunez... done, v5
            …
          

Déployer sur Heroku (suite)

Allez, maintenant que c’est provisionné (ça a pris genre 20 secondes), on déploie :


            git push heroku master
            …
            remote: -----> Node.js app detected
            …
            remote: -----> Installing binaries
            remote:        engines.node (package.json):  >= 8
            remote:        engines.npm (package.json):   >= 6
            remote:
            remote:        Resolving node version >= 8...
            remote:        Downloading and installing node 10.11.0...
            remote:        Bootstrapping npm >= 6 (replacing 6.4.1)...
            …
            remote: -----> Launching...
            remote:        Released v6
            remote:        https://toptunez.herokuapp.com/ deployed to Heroku
            remote:
            remote: Verifying deploy... done.
          

Déployer sur Azure

Ici, spécifiquement via App Service. À la main (dans le portail), ou avec la CLI, c’est un peu pénible, mais…

l’extension Azure App Service pour VS Code est super cool !

Extrait visuel de l’extension utilisée pour notre application

On crée l’app, on définit l’environnement (clés, URLs, etc.) grâce aux Application Settings, et on déploie !
L’installation npm prend pas mal de temps, ainsi que le boot, mais ça va…

Merci !

delicious-insights.com

@DelicioInsights