Skip to main content

Exemples essentiels de backends Node.js pour les développeurs en 2024

Écrit par

12 juin 2024

0 minutes de lecture

En 2024, le développement backend avec Node.js continue de se démarquer comme un environnement d’exécution puissant et flexible pour créer des applications évolutives et efficaces, d’autant plus avec l’essor d’autres environnements comme Bun.

Dans cet article, je souhaitais proposer une introduction légère à des exemples essentiels de backend Node.js, qui illustrent l’utilisation efficace des fonctionnalités avancées de JavaScript et de Node.js. De l’exploitation de la norme WHATWG Streams et de l’API Web Streams pour traiter les données efficacement à l’utilisation du module crypto intégré à Node.js pour la sécurité, en passant par la manipulation de données binaires avec les Buffers, l’encapsulation et la gestion des espaces de noms avec les Symbols, ainsi que la génération de HTML et de requêtes SQL dynamiques avec les littéraux de gabarit et les gabarits tagués, chaque section présente des extraits de code pratiques, issus de situations réelles, et des conseils.

Ces exemples mettent en évidence la polyvalence et la puissance de Node.js pour relever les défis du backend. Ils constituent également une ressource précieuse pour les développeurs qui souhaitent améliorer leurs solutions backend en 2024.

Chapitres de cet article :

  1. La norme WHATWG Streams, l’API Web Streams et les itérables asynchrones

  2. Utiliser le module Crypto pour valider les signatures de webhooks

  3. Utiliser les buffers et les données binaires brutes dans Node.js

  4. Utiliser les Symbols JavaScript pour l’encapsulation

  5. Littéraux de gabarit et gabarits tagués pour générer du HTML et des requêtes SQL

Vous pouvez toujours consulter l’ensemble des exemples de code source de backend Node.js dans ce dépôt GitHub.

1. La norme WHATWG Streams, l’API Web Streams et les itérables asynchrones

En 2024, l’utilisation des flux s’est considérablement répandue, en particulier pour traiter des modèles de langage de grande taille dédiés à l’intelligence artificielle générative (GenAI). Le SDK OpenAI pour la complétion de chat en est un parfait exemple. Voici à quoi pourrait ressembler un exemple type de flux de données provenant de l’API du SDK OpenAI :

const completion = await openai.chat.completions.create(config);

for await (const chunk of completion) {
  console.log(chunk);
}

Nous utilisons ici le concept d’itérables asynchrones, qui a simplifié les flux de travail asynchrones dans Node.js en les alignant sur le style de programmation basé sur les promesses. L’instruction `for await…` crée une boucle qui parcourt les objets itérables asynchrones ainsi que les itérables synchrones, notamment les types intégrés String, Array, les objets semblables à des tableaux (par exemple, arguments ou NodeList), TypedArray, Map, Set et les itérables asynchrones ou synchrones définis par l’utilisateur.

Présentation des flux Web standard WHATWG dans Node.js

Les flux Web, qui font partie de la norme WHATWG Streams, ont été intégrés à Node.js et offrent une méthode robuste de gestion des flux de données. Cette norme permet aux développeurs de lire, d’écrire et de transformer efficacement des flux de données avec JavaScript.

Voyons un exemple complet d’utilisation des flux Web dans Node.js.

Dans l’exemple suivant, nous créons une fonction `createReadableStreamFromFile()` qui utilise la classe `ReadableStream` de l’API Web Streams pour créer un flux de données à partir d’un fichier. Une deuxième fonction, `consumeStreamWithAsyncIterator()`, consomme ensuite ce flux à l’aide d’un itérateur asynchrone.

import { ReadableStream } from "node:stream/web";
import fs from "fs";

function createReadableStreamFromFile(filePath) {
  const stream = new ReadableStream({
    start(controller) {
      const reader = fs.createReadStream(filePath);

      reader.on("data", (chunk) => {
        controller.enqueue(chunk);
        if (reader.readableFlowing === false) {
          reader.resume();
        }
      });

      reader.on("end", () => {
        controller.close();
      });

      reader.on("error", (err) => {
        controller.error(err);
      });
    },
  });

  return stream;
}

async function consumeStreamWithAsyncIterator(stream) {
  try {
    for await (const chunk of stream) {
      process.stdout.write(chunk);
    }
  } catch (err) {
    console.error("Error occurred while reading the stream:", err);
  }
}

const filePath = process.argv[2];
const stream = createReadableStreamFromFile(filePath);
consumeStreamWithAsyncIterator(stream);

Dans ce code, nous utilisons l’API Web Streams pour lire les données d’un fichier sous forme de flux. La fonction `createReadableStreamFromFile()` renvoie un objet `ReadableStream` à partir d’un chemin de fichier donné. Le flux est ensuite consommé par `consumeStreamWithAsyncIterator()`, qui le lit morceau par morceau et écrit chaque morceau sur la sortie standard. Si une erreur survient pendant la lecture, elle est interceptée et consignée dans la console.

Au fait, avez-vous remarqué la faille de sécurité dans le code ci-dessus ?

Si vous aviez installé l’extension Snyk pour VS Code, une erreur de lint ondulée vous aurait signalé une vulnérabilité de traversée de chemin dans cet exemple de code.

L’extension Snyk pour VS Code vous montrerait comment des données non sécurisées peuvent aboutir dans cet exemple de code backend Node.js, en quoi consiste cette faille de sécurité et comment la corriger grâce à des suggestions sélectionnées par l’IA à partir de projets open source actifs.

L’extension Snyk pour VS Code détecte une faille de sécurité dans mon exemple de code backend Node.js.

2. Utiliser le module Crypto pour valider les signatures de webhooks

Les webhooks sont devenus indispensables au développement backend moderne : ils permettent à différents services de communiquer efficacement en temps réel. Toutefois, leur utilisation croissante implique de renforcer les mesures de sécurité, notamment en validant les signatures des webhooks.

Dans le contexte des webhooks, une signature est un hash envoyé avec la charge utile du webhook. Elle est calculée à l’aide d’une clé secrète que seuls l’expéditeur et le destinataire connaissent. Le destinataire peut alors calculer le hash de son côté et le comparer à la signature afin de vérifier l’authenticité du webhook.

Le module Crypto de Node.js offre de nombreuses fonctionnalités cryptographiques, dont un ensemble de wrappers pour les fonctions de hachage, HMAC, chiffrement, déchiffrement, signature et vérification d’OpenSSL. Le HMAC (code d’authentification de message fondé sur un hash) est particulièrement utile pour valider les signatures de webhooks.

Décomposons l’extrait de code fourni pour comprendre son fonctionnement :

const crypto = require("node:crypto");

const secret = process.env.WEBHOOK_SECERT;
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(
  hmac.update(request.rawBody).digest("hex"),
  "utf8"
);
const signature = Buffer.from(request.headers["x-signature"] || "", "utf8");

if (!crypto.timingSafeEqual(digest, signature)) {
  throw new Error("Invalid signature.");
}

Dans cet extrait, la méthode `crypto.createHmac` sert à créer un objet HMAC. Elle prend deux paramètres : l’algorithme à utiliser (ici, `sha256`) et la clé secrète.

L’objet HMAC est ensuite mis à jour avec le corps brut de la requête webhook à l’aide de la méthode `hmac.update`. Cette méthode peut être appelée plusieurs fois à mesure que de nouvelles données arrivent sous forme de flux.

La méthode `digest` génère ensuite le hash. Elle ne peut être appelée qu’une seule fois sur l’objet HMAC et renvoie le hash calculé. Le paramètre `hex` indique à la méthode de renvoyer le hash au format hexadécimal.

Présentons maintenant le concept de comparaison sécurisée de chaînes. Le hash calculé (digest) et la signature reçue dans l’en-tête de la requête webhook sont ensuite comparés à l’aide de la méthode `crypto.timingSafeEqual`. Cette méthode compare deux buffers de manière sécurisée contre les attaques temporelles, ce qui la rend idéale pour comparer des résultats cryptographiques.

Les attaques temporelles sont un type d’attaque par canal auxiliaire dans lequel un attaquant tente de compromettre un système en analysant le temps nécessaire à l’exécution d’algorithmes cryptographiques. En utilisant une méthode sécurisée contre les attaques temporelles, comme `crypto.timingSafeEqual`, nous nous protégeons contre ce type d’attaque. Si le digest et la signature ne correspondent pas, une erreur est générée, indiquant que la requête webhook n’est peut-être pas authentique.

3. Utiliser les buffers et les données binaires brutes dans Node.js

L’API Buffer de Node.js est un outil puissant qui permet aux développeurs de manipuler directement des données binaires. Que vous ayez besoin de lire un fichier, d’analyser une image ou de traiter des données brutes, l’API Buffer fournit des méthodes pour effectuer ces tâches efficacement.

Découvrons quelques méthodes courantes de l’API Buffer dans Node.js, comme `.from`, `.alloc` et `.write`, qui permettent de créer et de manipuler des objets buffer. La méthode `.from` crée un nouveau buffer à partir des données transmises en argument, `.alloc` crée un buffer d’une taille spécifiée et `.write` permet d’écrire des données dans un buffer. La méthode `.concat`, quant à elle, sert à concaténer une liste d’instances Buffer.

let buffer1 = Buffer.from('Hello, ');
let buffer2 = Buffer.from('World!');
let buffer3 = Buffer.concat([buffer1, buffer2]);

console.log(buffer3.toString());
// prints: 'Hello, World!'

Analyser une image avec l’API OpenAI et l’API Buffer de Node.js

Dans le prochain exemple de code backend Node.js, nous utilisons l’API Buffer pour lire un fichier image et l’analyser avec l’API OpenAI. Nous commençons par importer les modules nécessaires et créer une instance du client de l’API OpenAI :

import { readFile } from "node:fs/promises";
import OpenAI from "openai";

const openai = new OpenAI();

Nous lisons ensuite le fichier image dans un buffer :

const imageBuffer = await readFileToBuffer(process.argv[2]);

Nous validons ensuite le type de l’image en comparant sa signature à celle d’un fichier PNG connu :

function isImageTypeValid(imageBuffer) {
  const pngSignature = Buffer.from([
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
  ]);
  const fileSignature = imageBuffer.slice(0, 8);

  if (pngSignature.equals(fileSignature)) {
    return true;
  }
}

Enfin, nous générons un texte alternatif descriptif pour l’image à l’aide de l’API OpenAI :

async function generateAltTextForImage(imageBuffer) {
  const imageInBase64 = imageBuffer.toString("base64");

  const response = await openai.chat.completions.create({
    model: "gpt-4-vision-preview",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "What's in this image? generate a simple alt text for an image source in an HTML page",
          },
          {
            type: "image_url",
            image_url: {
              url: `data:image/png;base64,${imageInBase64}`,
            },
          },
        ],
      },
    ],
  });

  return response.choices[0];
}

Considérations de sécurité lors de l’utilisation de l’API Buffer

L’utilisation de l’API Buffer implique plusieurs considérations de sécurité. Une mauvaise gestion des données binaires peut entraîner des vulnérabilités potentielles, comme des erreurs de dépassement ou de sous-dépassement de buffer. Veillez toujours à valider les entrées et à gérer correctement les erreurs.

Soyez également attentif aux vulnérabilités potentielles, comme le constructeur `Buffer`, désormais obsolète, qu’il convient d’éviter au profit d’alternatives plus sûres telles que `Buffer.from` ou `Buffer.alloc`.

4. Utiliser les Symbols JavaScript pour l’encapsulation

Les Symbols sont un type de données primitif introduit dans ES6 (ECMAScript 2015) qui représente des identifiants uniques et immuables. Ils sont créés avec la fonction `Symbol()`, qui accepte éventuellement une description (une chaîne) pouvant servir au débogage, sans toutefois modifier l’unicité du symbole. Les Symbols servent principalement à créer des clés de propriétés uniques pour les objets, qui n’entrent en conflit avec aucune autre propriété, y compris les propriétés héritées. Ils sont donc particulièrement utiles pour définir des propriétés privées ou spéciales d’objets sans risquer de conflit de noms.

Voici un exemple de code backend Node.js montrant comment utiliser les Symbols pour créer une propriété privée dans une classe et encapsuler des données :

const _privateProperty = Symbol('privateProperty');

class MyClass {
  constructor(value) {
    this[_privateProperty] = value;
  }

  getPrivateProperty() {
    return this[_privateProperty];
  }
}

const instance = new MyClass('secret');
console.log(instance.getPrivateProperty());
// Will output 'secret'
// The _privateProperty cannot be directly accessed from outside the class

De plus, les Symbols ne sont pas accessibles lors de l’énumération des propriétés d’un objet (avec des boucles `for...in` ou `Object.keys()`, par exemple). Ils peuvent donc, dans une certaine mesure, servir à simuler des propriétés et des méthodes privées pour les objets. Il faut toutefois noter qu’ils ne sont pas véritablement privés et restent accessibles à l’aide de méthodes de réflexion comme `Object.getOwnPropertySymbols()`.

JavaScript définit un ensemble de Symbols bien connus représentant des comportements internes du langage que les développeurs peuvent personnaliser. Par exemple, voici comment implémenter un itérateur pour un objet personnalisé à l’aide de `Symbol.iterator` :

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
  }
};

for (const value of iterable) {
  // Logs 1 and 2
  console.log(value);
}

Utilisation des Symbols JavaScript par Fastify

Examinons un exemple plus concret avec le framework d’applications web Fastify et la façon dont le projet utilise les Symbols. Nous allons notamment nous pencher sur l’architecture de plugins de Fastify. L’un des aspects clés de la conception de Fastify est sa fonctionnalité d’encapsulation, qui permet aux développeurs de créer des contextes d’application isolés à l’aide de plugins. C’est essentiel pour créer des applications à grande échelle, où les conflits d’espaces de noms peuvent devenir problématiques.

Fastify utilise des Symbols pour identifier de manière unique les propriétés et méthodes internes, afin d’éviter qu’elles interfèrent avec les propriétés définies par l’utilisateur ou celles d’autres plugins. Voici un exemple simplifié, inspiré de l’utilisation réelle des Symbols dans le code source de Fastify pour encapsuler les métadonnées d’un plugin :

const pluginMeta = Symbol('fastify.pluginMeta');

function registerPlugin(instance, plugin, options) {
  if (!plugin[pluginMeta]) {
    plugin[pluginMeta] = { options, name: plugin.name };
  }

  instance.register(plugin, options);
}

function myPlugin(instance, opts, done) {
  done();
}

registerPlugin(fastifyInstance, myPlugin, { prefix: '/api' });

Dans l’exemple ci-dessus, `pluginMeta` est un Symbol utilisé par Fastify pour associer des métadonnées aux fonctions de plugin. Ces métadonnées comprennent les options et le nom du plugin, ainsi que d’autres informations dont le framework pourrait avoir besoin en interne. La fonction `registerPlugin` simplifie l’association des métadonnées à un plugin avant son enregistrement dans une instance Fastify. Ces métadonnées sont ensuite accessibles au sein du framework Fastify, tout en restant isolées de l’interface publique du plugin et de l’espace de noms global de l’application.

L’utilisation des Symbols JavaScript pour des métadonnées internes comme celles-ci présente plusieurs avantages dans un framework tel que Fastify :

  • Encapsulation : elle empêche les détails internes de se retrouver dans l’espace utilisateur, préservant ainsi la clarté et la simplicité de l’API publique.

  • Sécurité : elle réduit le risque d’interférences accidentelles entre les plugins, ou entre un plugin et le cœur du framework, car les Symbols ne sont pas accessibles par l’énumération ordinaire des propriétés d’un objet.

  • Clarté : elle distingue clairement les mécanismes internes du framework des API exposées aux développeurs, ce qui facilite la maintenance et l’évolution du code source de Fastify.

5. Littéraux de gabarit et gabarits tagués pour générer du HTML et des requêtes SQL

Introduits dans ES6, les littéraux de gabarit offrent une syntaxe plus lisible et concise pour créer des chaînes en JavaScript. Vous les utilisez peut-être déjà pour écrire des chaînes et y intégrer des expressions dynamiques, comme `hello ${name}`. Fini la concaténation de chaînes.

On observe aujourd’hui une tendance croissante à utiliser les littéraux de gabarit sous forme de gabarits tagués pour générer du HTML et des requêtes SQL dans les backends Node.js. Voici deux exemples concrets de code backend Node.js et SSR :

Modèles balisés pour générer des requêtes SQL avec la bibliothèque PostgreSQL de Vercel

La gestion des requêtes SQL dans Node.js peut souvent produire du code verbeux et source d’erreurs, surtout lorsqu’il faut insérer dynamiquement des valeurs dans les requêtes. Le package PostgreSQL de Vercel (`@vercel/postgres`) propose une méthode plus concise et plus sûre pour formater les requêtes SQL à l’aide de littéraux de gabarit :

import { sql } from '@vercel/postgres';

const jediName = 'Luke Skywalker';
const { rows } = await sql`SELECT * FROM jedis WHERE name = ${jediName};`;

La fonction de littéral de gabarit balisé SQL insère de manière sécurisée la variable `jediName` dans la requête SQL, empêchant ainsi les attaques par injection SQL.

Modèles balisés pour générer du HTML sur des serveurs Fastify

Comme pour les requêtes SQL, `fastify-html` est un plug-in Fastify qui permet aux développeurs d’utiliser des modèles balisés pour générer du contenu HTML directement dans les gestionnaires de routes. Cette méthode peut s’avérer particulièrement utile pour le rendu côté serveur (SSR) ou la génération de contenu HTML dynamique (quelqu’un a dit htmx ?) :

import fastify from 'fastify'
import fastifyHtml from 'fastify-html'

const app = fastify()
await app.register(fastifyHtml)

app.get('/', async (req, reply) => {
  const name = req.query.name || 'World';
  return reply.html`<h1>Hello ${name}</h1>`;
});

Que sont les littéraux de gabarit étiquetés ?

Dans le prolongement du concept des littéraux de gabarit, les littéraux de gabarit étiquetés sont une fonctionnalité de JavaScript qui vous permet de définir une fonction personnalisée (le « tag ») pour traiter les littéraux de gabarit. Cette fonction reçoit le littéral de gabarit sous la forme d’un tableau de chaînes et les expressions interpolées sous forme d’arguments distincts. Vous pouvez ainsi manipuler le gabarit et ses valeurs avant la création de la chaîne finale.

Pour conclure

Si cet article vous a plu, découvrez aussi les bonnes pratiques pour créer un package npm moderne en tenant compte de la sécurité et 10 bonnes pratiques pour conteneuriser des applications web Node.js avec Docker.

Et si les récits autour de la sécurité vous intéressent, pensez à bien vous préparer à combattre les attaques de la chaîne logistique sur npm en appliquant les pratiques de sécurité pour les développeurs mentionnées dans cet article.

Enfin, n’oubliez pas de découvrir Snyk avec un compte gratuit pour commencer à sécuriser votre code Node.js, vos dépendances et vos images de conteneurs Docker.

Lire la suite

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

Blog

Pourquoi les agents de codage IA créent-ils sans cesse des failles de contrôle d’accès ?

Les agents de codage IA peuvent générer une logique d’autorisation qui compile et passe la revue, tout en exposant les données d’un tenant à un autre. Découvrez pourquoi les failles de contrôle d’accès sont difficiles à détecter et comment les prévenir.