Aller au contenu
gaetancottrez.dev

Produire des Webhooks sur son API

Published:le  à 06:00 | (6 min de lecture)
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 :

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 :

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);
  }
});

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.

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.

Vous pourriez aussi aimer

Github Copilot : Mon avis après 2 mois d'utilisation

Github Copilot : Mon avis après 2 mois d'utilisation

Comment être productif en programmation ?

Comment être productif en programmation ?

Article précédent
Combien de temps pour apprendre la programmation ?
Article suivant
Comment réaliser un vrai projet web : de A à Z