Produire des Webhooks sur son API 1
Développement web

Produire des Webhooks sur son API

Qu’est-ce qu’un Webhook ?

Les Webhooks sont un moyen innovant en programmation web pour notifier à des applications externes qu’un changement ou qu’une évolution de vos données a été réalisée au sein de votre propre application.

Les Webhooks ne remplace pas une API. C’est un moyen complémentaire à une API pour récupérer et être au courant des nouvelles données.

En clair, un Webhook vous évitera de devoir interroger une API à intervalle régulier pour savoir si quelque chose à changer puisque c’est lui qui vous le notifiera : ceux qui rend votre processus de récupération de données plus performants et plus efficaces.

Concrètement, les Webhooks vont se déclencher après qu’une requête POST, PUT, PATCH ou DELETE ait réussi au niveau de votre API.

Le résultat de ces requêtes est généralement envoyé directement aux applications ayant souscrit à votre système de Webhooks. Il est tout à fait possible de reformater la réponse avant de la renvoyer.

Le Webhook doit envoyer ses données aux applications externes en méthode POST.

Schéma pour se représenter le fonctionnement d’un Webhook :

Schéma expliquant le principe des Webhooks

Gérer techniquement un système de production de Webhook au sein de son API

Il est existe beaucoup de contenu sur la façon de souscrire à des webhooks pour recevoir des données et les traiter comme ceux de Stripe, GitHub, etc…

Mais il existe peu de contenu sur la façon d’en produire pour d’autres.

Pour produire des Webhooks sur son API, il faut pouvoir créer un système qui :

  • s’exécutera après que les routes de votre API aient terminé et réussi leur traitement
  • s’exécutera uniquement sur des routes POST, PUT, PATCH ou DELETE que vous souhaitez ouvrir
  • permettra d’identifier le type d’événement par rapport au contenu des données de votre Webhook (en ajoutant une en-tête HTTP dans le Webhook)
  • permettra de retenter « X » fois votre webhook si l’application externe vous a renvoyé un statut code différent de 200
  • ne devra pas bloquer les réponses ou les autres traitements de votre API (il devra fonctionner de manière asynchrone, autonome et sans observer les retours d’états.
  • permettra de manager la liste des applications souscrivant au webhook
  • assurera un moyen de sécuriser la transaction pour l’application souscrivant à votre Webhook

Comment ça se passe techniquement sur un environnement Node.js ?

Un package NPM node-webhooks permet de créer un système de production de Webhooks sur Node.js (donc sur Express ou NestJS).

Le principe est relativement simple :

  • on instancie le package avec une liste d’URL d’application qui recevront vos données en méthode POST puis on lancer la méthode .trigger() du package pour déclencher la production d’un Webhook dans votre système de Webhooks.
  • Il sera ensuite possible possible de créer un système de « retries » grâce à la méthode .getEmitter() ce qui vous donnera la possibilité de gérer les événements success ou failure de votre système de Webhooks. L’événement failure sera intéressant pour gérer un système de « retries ». Exemple de « retries » possible :
const emitter = webHooks.getEmitter();
let retries = 3;

emitter.on('*.failure', (shortname, statusCode, body) => {
  console.error(
    `Error on trigger webHook ${shortname} with status code ${statusCode} and body ${body} - Remaining number of retries the webhooks : ${retries}`,
  );

  const retryWebhooks = setTimeout(() => {
    retries--;
    // re-lauch your webhooks system
  }, 30000);

  if (retries === 0) {
    clearTimeout(retryWebhooks);
  }
});
  • Il faudra ensuite produire un système de sécurité de votre transaction. Vous pouvez spécifier que les applications externes qui souscrivent à un Webhook requièrent de demander une authentification basique (BasicAuth). Mais il existe une autre alternative que je vous conseille : créer une signature pour votre Webhook. Le meilleur moyen est de créer un hash du contenu du body avec un secret et de placer la signature dans une en-tête HTTP. Ce qui donnera cette formule : hash_hmac('sha256', body, secret). Il suffit ensuite aux applications externes de contrôler la signature en la recréant de leur côté (avec le même secret). Exemple avec crypto : crypto.createHmac('sha256', 'my_secret').update(JSON.stringify(req.body)).digest('base64')

Le moyen le plus simple et le plus efficace est de créer un middleware sur votre API qui s’occupera d’analyser la réponse et suivant le statut de la réponse appellera votre système de Webhooks.

Les personnes qui ont lu cet article ont aussi lu :  Framework JavaScript : Angular, React ou Vue.js ?

Un bon vieux app.use() fera l’affaire sur Express tandis qu’un interceptor custom sur NestJS sera plus efficace.

Exemple de code pour intégrer le package sous NestJS et produire des Webhooks

Dans mon exemple, je me récupère un decorator custom appelé Abilities (qui contient le type de droit pour utiliser la route) sur la route utilisée pour construire mon MyAPI-Hookqui contiendra le type d’événement pour les applications souscrivant au Webhook savent le type de données et ce que cela concerne.

SECRETS_WEBHOOKS=d6b88fed70be13e62b8e31147acda29ef91f651f
LIST_URL_FOR_SEND_WEBHOOKS={"shortname1":["http://localhost:3000/webhooks"]}
MS_RETRY_DELAY_WEBHOOKS=5000
NUMBER_OF_RETRY_WEBHOOKS=5
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Reflector } from '@nestjs/core';
import * as WebHooks from 'node-webhooks';
import * as crypto from 'crypto';

@Injectable()
export class WebhooksProducerInterceptor implements NestInterceptor {
  private produceWebhook(webHooks, data, apiHook) {
    webHooks.trigger('myapi-teams', data, {
      'MyAPI-Signature': crypto
        .createHmac('sha256', process.env.SECRETS_WEBHOOKS)
        .update(JSON.stringify(data))
        .digest('base64'),
      'MyAPI-Hook': `${apiHook}`,
    });
  }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const webHooks = new WebHooks({
      db: JSON.parse(process.env.LIST_URL_FOR_SEND_WEBHOOKS),
    });
    const getReflector = new Reflector().get<string[]>(
      'abilities',
      context.getHandler(),
    );
    const ability = getReflector ? getReflector[0].toLowerCase() : null;
    const apiHook = this.buildApiHook(ability)
    return next.handle().pipe(
      map((data) => {
        this.produceWebhook(webHooks, data, apiHook);

        const emitter = webHooks.getEmitter();
        let retries = Number(process.env.NUMBER_OF_RETRY_WEBHOOKS);

        emitter.on('*.failure', (shortname, statusCode, body) => {
          console.error(
            `Error on trigger webHook ${shortname} with status code ${statusCode} and body ${body} - Remaining number of retries the webhooks : ${retries}`,
          );

          const retryWebhooks = setTimeout(() => {
            retries--;
            this.produceWebhook(webHooks, data, apiHook);
          }, Number(process.env.MS_RETRY_DELAY_WEBHOOKS));

          if (retries === 0) {
            clearTimeout(retryWebhooks);
          }
        });
        return data;
      }),
    );
  }

  private buildApiHook(ability: string) {
    return ability !== null
      ? `hook.${ability.split(':')[1]}.${ability.split(':')[0]}`
      : 'hook';
  }
}

Et il suffit ensuite d’utiliser ce morceau code sur les routes qui doivent déclencher la production d’un Webhooks

@UseInterceptors(new WebhooksProducerInterceptor())

Exemple de code pour consumer un Webhook (application externe)

var express = require('express');
var router = express.Router();



const checkValidWebhook = function (body, headers) {
  return require("crypto")
    .createHmac('sha256', 'my_secret')
    .update(JSON.stringify(body))
    .digest('base64') === headers['myapi-signature']
}

/* POST webhooks for consume */
router.post('/', async function (req, res, next) {
  if (!checkValidWebhook(req.body, req.headers)) {
    throw new Error('bad webhooks')
  }
  try {
    // Your differents treatments based on req.headers['myapi-hook'] for consume the webhook
  } catch (e) {
    console.log(e)
  }
  res.send('webhook consume');
});

module.exports = router;

Epilogue

Comme vous l’avez vu, le principe même des Webhooks n’est pas si complexe que cela à comprendre mais également à mettre en place. Si vous mettez ce genre de système en place sur votre API, vous bénéficierez d’un système innovant et performant vous ouvrant des portes pour faire discuter votre architecture logicielle avec d’autres applications externe.

Partager ce contenu

J’aide tous les débutants en programmation en partageant mon expérience à tous ceux désirant apprendre sur mon blog « Apprendre la programmation.net »

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Voudriez-vous recevoir mon guide pour débutant des 7 erreurs à éviter pour bien débuter en programmation ?