Chapitre 1220 minIntermédiaire
Webhooks, GraphQL, gRPC et WebSocket : les alternatives à REST
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer la différence entre interroger une API et recevoir un webhook ;
- sécuriser un récepteur de webhook ;
- comparer REST, GraphQL et gRPC ;
- choisir entre WebSocket et SSE pour du temps réel.
4 exercices corrigés1 schéma
REST n'est pas la seule façon de faire communiquer des applications. Un webhook inverse le sens de l'appel : c'est le serveur qui prévient ton application quand un événement se produit. GraphQL laisse le client choisir exactement les données qu'il veut, en une requête. gRPC appelle des procédures distantes en binaire, très vite, entre services. WebSocket et SSE gardent une connexion ouverte pour le temps réel. Ce chapitre explique le principe de chacun, ses forces, ses pièges, et comment choisir.
Interroger en boucle ou être prévenu
Le service comptable de GSB passe par une banque pour les virements. Quand AppliFrais met une fiche en paiement, elle demande un virement à l'API de la banque, qui l'exécute quelques heures plus tard. Comment AppliFrais sait-elle que le virement est fait ?
Première solution, l'interrogation régulière (polling) : AppliFrais appelle GET /virements/VIR-2026-1104 toutes les dix minutes jusqu'à obtenir "statut": "execute". C'est simple, mais presque toutes les requêtes sont inutiles, et l'information arrive avec jusqu'à dix minutes de retard. À grande échelle, le coût explose : 500 clients qui interrogent une API toutes les 10 secondes envoient 500 × 6 × 60 × 24 = 4 320 000 requêtes par jour, presque toutes pour s'entendre répondre « rien de neuf ».
Deuxième solution, le webhook : AppliFrais donne une fois pour toutes à la banque une adresse à appeler (https://api.gsb.example/v1/webhooks/banque). Quand le virement est exécuté, c'est la banque qui envoie une requête POST à cette adresse, avec la description de l'événement. Une seule requête, au bon moment.
Les webhooks sont partout : paiement réussi ou remboursé (prestataires de paiement), push sur un dépôt (GitHub, GitLab), formulaire rempli, colis livré, message reçu par une messagerie d'entreprise. On les appelle parfois des « API inversées ».
Recevoir un webhook
Voici la requête que reçoit AppliFrais :
POST /v1/webhooks/banque HTTP/1.1
Host: api.gsb.example
Content-Type: application/json
Webhook-Horodatage: 1793616120
Webhook-Signature: 54523001739956e854335ec9f0f8bcd4c245abe328a7dd9ddb43c04998402dfc
{"id":"evt_7Hq2","type":"paiement.reussi","cree":"2026-11-02T10:42:00Z","donnees":{"ficheId":11,"montant":389.2,"reference":"VIR-2026-1104"}}
L'adresse du webhook est publique : n'importe qui peut y envoyer une requête qui prétend qu'un paiement a réussi. Un récepteur de webhook doit donc respecter quatre règles.
- Vérifier la signature. L'émetteur et AppliFrais partagent un secret, échangé une fois à l'inscription du webhook. L'émetteur calcule un HMAC-SHA256 de l'horodatage et du corps avec ce secret, et l'envoie dans un en-tête. AppliFrais refait le calcul sur le corps brut reçu (avant toute conversion, un seul espace de différence change la signature) et compare. Sans le secret, impossible de produire une signature valide.
- Refuser les messages trop anciens. L'horodatage, inclus dans la signature, empêche un attaquant qui aurait capturé un message de le rejouer plus tard : au-delà de cinq minutes d'écart, on refuse.
- Répondre vite, traiter ensuite. L'émetteur attend une réponse
2xxen quelques secondes ; sinon il considère l'envoi comme raté et réessaie. On enregistre l'événement, on répond204, et on fait le traitement long dans une tâche de fond. - Traiter chaque événement une seule fois. Parce que l'émetteur réessaie, le même événement peut arriver deux fois : on garde les
iddéjà traités (evt_7Hq2) et on ignore les doublons. Le traitement doit être idempotent.
const crypto = require('node:crypto');
function signatureValide(corpsBrut, horodatage, signatureRecue, secret) {
// 1. Refuser un message trop ancien (rejeu) : plus de 5 minutes d'écart
const ecart = Math.abs(Date.now() / 1000 - Number(horodatage));
if (!(ecart <= 300)) return false;
// 2. Recalculer la signature sur l'horodatage et le corps BRUT reçu
const attendue = crypto.createHmac('sha256', secret).update(`${horodatage}.${corpsBrut}`).digest('hex');
// 3. Comparer en temps constant
const a = Buffer.from(attendue, 'hex');
const b = Buffer.from(signatureRecue, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/v1/webhooks/banque', express.raw({ type: 'application/json' }), async (req, res) => {
const corpsBrut = req.body.toString('utf8');
if (!signatureValide(corpsBrut, req.get('Webhook-Horodatage'), req.get('Webhook-Signature') ?? '', process.env.SECRET_WEBHOOK_BANQUE)) {
return res.status(401).end();
}
const evenement = JSON.parse(corpsBrut);
if (await dejaTraite(evenement.id)) return res.status(204).end(); // doublon : on ignore
await enregistrerEvenement(evenement); // traité ensuite par une tâche de fond
res.status(204).end();
});
crypto.timingSafeEqual compare les deux signatures en un temps qui ne dépend pas du contenu : une comparaison ordinaire s'arrête au premier caractère différent, et un attaquant patient pourrait mesurer ces écarts de temps pour deviner la signature octet par octet. Le !(ecart <= 300) refuse aussi un horodatage absent ou illisible (NaN). La signature HMAC est expliquée dans le chapitre HMAC et signature numérique du cours de cryptographie.
GraphQL : le client choisit ses données
GraphQL est un langage de requête pour les API, créé par Facebook en 2012 et publié en 2015. Au lieu de multiples URI, l'API expose une seule adresse (souvent POST /graphql) et un schéma typé qui décrit toutes les données disponibles. Le client envoie une requête qui dit exactement quels champs il veut, y compris dans les objets liés.
Le schéma, côté serveur :
type Visiteur {
id: ID!
nom: String!
prenom: String!
region: String
fiches(etat: Etat): [Fiche!]!
}
type Fiche {
id: ID!
mois: String!
etat: Etat!
montantValide: Float
}
enum Etat { CR CL VA MP }
type Query {
visiteur(id: ID!): Visiteur
}
La requête du client, qui veut le nom d'un visiteur et le mois de ses fiches clôturées, rien de plus :
query {
visiteur(id: "7") {
nom
fiches(etat: CL) {
mois
}
}
}
La réponse a exactement la forme de la requête :
{
"data": {
"visiteur": {
"nom": "Martin",
"fiches": [ { "mois": "2026-10" } ]
}
}
}
Avec une API REST, il aurait fallu deux requêtes (GET /v1/visiteurs/7, puis GET /v1/fiches?visiteur=7&etat=CL), qui renvoient chacune tous les champs. GraphQL évite ces deux problèmes classiques : le sur-chargement (over-fetching, recevoir des champs inutiles) et le sous-chargement (under-fetching, devoir enchaîner plusieurs requêtes). Les modifications passent par des mutations, et les notifications en temps réel par des abonnements (subscriptions).
Les contreparties sont réelles :
- le cache HTTP ne fonctionne pas : tout passe par
POSTsur la même adresse ; il faut un cache côté client (Apollo, Relay) ; - les erreurs arrivent souvent avec un code
200, dans un tableauerrorsdu corps : le code de statut HTTP ne suffit plus pour savoir si la requête a réussi ; - une requête peut coûter très cher : un client peut demander des imbrications profondes (les fiches des visiteurs de la région de chaque visiteur…) et saturer le serveur. Il faut limiter la profondeur et la complexité des requêtes ;
- le problème N+1 : un résolveur naïf fait une requête SQL par fiche ; on regroupe les accès (le motif DataLoader) ;
- l'introspection, qui permet de demander le schéma complet à l'API, est utile en développement mais expose toute la surface d'attaque en production : on la désactive ou on la protège.
GraphQL brille quand de nombreux clients différents (web, mobile, partenaires) ont besoin de données très reliées, sous des formes variées : GitHub, Shopify ou Meta l'utilisent. Pour une API de gestion avec des ressources bien délimitées, comme AppliFrais, REST reste plus simple.
gRPC : des procédures distantes, en binaire
gRPC est un cadre d'appel de procédures distantes (RPC) créé par Google et publié en 2015. On décrit le service et ses messages dans un fichier .proto, et un outil génère le code du client et du serveur dans le langage voulu.
syntax = "proto3";
service ServiceFiches {
rpc LireFiche (DemandeFiche) returns (Fiche);
rpc SuivreValidations (DemandeSuivi) returns (stream Fiche); // flux de réponses
}
message DemandeFiche { int32 id = 1; }
message DemandeSuivi { string mois = 1; }
message Fiche {
int32 id = 1;
string mois = 2;
string etat = 3;
double montant_valide = 4;
}
Les messages voyagent au format Protocol Buffers, un format binaire compact (les nombres = 1, = 2 identifient les champs à la place de leurs noms), sur HTTP/2. Résultat : des messages plusieurs fois plus petits que leur équivalent JSON, rapides à lire, un contrat strictement typé, et des flux (streaming) dans les deux sens.
En contrepartie, les messages ne se lisent pas à l'œil nu, et un navigateur ne peut pas appeler directement un service gRPC (il faut une passerelle, comme gRPC-Web ou Connect). gRPC est donc surtout utilisé entre services d'une même organisation : une architecture en microservices, des services internes qui s'appellent des milliers de fois par seconde.
WebSocket et SSE : le temps réel
HTTP fonctionne en requête-réponse : le serveur ne peut pas envoyer d'information au client sans qu'il l'ait demandée. Pour le temps réel (une messagerie, un tableau de bord qui se met à jour seul, un jeu en ligne), deux techniques gardent une connexion ouverte.
WebSocket (RFC 6455) ouvre une connexion permanente et bidirectionnelle. Elle commence par une requête HTTP qui demande à changer de protocole ; le serveur accepte avec le code 101 Switching Protocols, puis client et serveur s'échangent des messages dans les deux sens, à tout moment, sur la même connexion TCP :
GET /v1/notifications HTTP/1.1
Host: api.gsb.example
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
const ws = new WebSocket('wss://api.gsb.example/v1/notifications');
ws.addEventListener('message', (e) => afficherNotification(JSON.parse(e.data)));
ws.send(JSON.stringify({ abonnement: 'fiches' }));
Une connexion WebSocket ne bénéficie pas des protections habituelles de HTTP : on l'ouvre en wss:// (chiffré), on vérifie l'en-tête Origin lors de la poignée de main (le navigateur n'applique pas CORS aux WebSockets), et on authentifie la connexion à l'ouverture.
SSE (Server-Sent Events) est plus simple : le client fait une requête HTTP ordinaire, et le serveur garde la réponse ouverte pour y écrire des événements au fil de l'eau, au format text/event-stream. La communication ne va que du serveur vers le client, mais cela suffit pour des notifications, un fil d'activité ou l'affichage progressif d'une réponse générée par une IA. Le navigateur se reconnecte tout seul si la connexion coupe.
const source = new EventSource('https://api.gsb.example/v1/flux');
source.addEventListener('fiche-validee', (e) => afficherNotification(JSON.parse(e.data)));
Et SOAP ?
SOAP (Simple Object Access Protocol) est le style de services web des années 2000 : des messages XML dans une enveloppe, décrits par un fichier WSDL qui liste les opérations et les types, avec des normes complémentaires pour la sécurité (WS-Security) et les transactions. On le croise encore dans les banques, les assurances, les administrations et les progiciels de gestion. Tu n'en créeras probablement pas, mais tu pourrais devoir en consommer un : la plupart des langages génèrent un client à partir du WSDL.
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<LireFiche xmlns="https://api.gsb.example/soap">
<id>12</id>
</LireFiche>
</soap:Body>
</soap:Envelope>
Comment choisir
| Besoin | Technologie adaptée | Pourquoi |
|---|---|---|
| API de gestion ouverte à plusieurs clients (web, mobile, partenaires) | REST | Simple, universel, cache HTTP, outillage abondant |
| Être prévenu d'un événement chez un tiers | Webhook | Pas d'interrogation inutile, information immédiate |
| Clients variés qui veulent des données très reliées, sous des formes différentes | GraphQL | Une requête, exactement les champs voulus |
| Appels très fréquents entre services internes | gRPC | Binaire, rapide, contrat typé, flux |
| Échanges en temps réel dans les deux sens (messagerie, jeu, édition collaborative) | WebSocket | Connexion permanente bidirectionnelle |
| Notifications du serveur vers le client seulement | SSE | Plus simple que WebSocket, sur du HTTP ordinaire |
| Intégration avec un système existant qui l'impose | SOAP | On ne le choisit pas, on s'y adapte |
Ces technologies se combinent : AppliFrais expose une API REST, reçoit les webhooks de la banque, et pourrait pousser une notification en SSE au visiteur quand sa fiche est validée.
Exercices
Exercice 1 sur 4 Choisir la technologie Réussi À revoir
Pour chaque besoin, propose la technologie la plus adaptée et justifie.
- Le service informatique veut être averti dans sa messagerie d'équipe chaque fois qu'un ticket urgent est créé dans l'outil d'assistance hébergé par un éditeur.
- Un tableau de bord affiche en direct le nombre de fiches clôturées dans la journée.
- Deux services internes d'un grand groupe s'échangent des milliers de messages par seconde.
- L'application mobile des visiteurs affiche les fiches, leurs lignes et le nom du comptable validateur, sur un réseau parfois lent.
- Une messagerie interne où chacun écrit et reçoit des messages instantanément.
Voir la réponseMasquer la réponse de l'exercice 1
- Webhook : l'outil d'assistance appelle l'adresse fournie par la messagerie d'équipe à chaque événement, sans interrogation en boucle.
- SSE : des mises à jour du serveur vers le client seulement ; plus simple que WebSocket. (Un polling toutes les 30 secondes est acceptable pour un usage interne modeste.)
- gRPC : messages binaires compacts, HTTP/2, contrat typé, entre services d'une même organisation.
- REST bien conçu avec une représentation de détail qui inclut les lignes et le validateur, ou GraphQL pour ne demander que les champs affichés en une requête. Les deux se défendent ; GraphQL apporte plus si les écrans sont nombreux et variés.
- WebSocket : échanges en temps réel dans les deux sens.
Tu avais trouvé ?
Exercice 2 sur 4 Sécuriser un récepteur de webhook Réussi À revoir
Un stagiaire a écrit le récepteur des notifications de paiement :
app.post('/webhooks/paiement', express.json(), async (req, res) => {
const evt = req.body;
if (evt.type === 'paiement.reussi') {
await marquerFichePayee(evt.donnees.ficheId);
await envoyerCourrielConfirmation(evt.donnees.ficheId); // prend 4 secondes
}
res.status(200).send('OK');
});
Liste les problèmes et dis comment corriger chacun.
Voir la réponseMasquer la réponse de l'exercice 2
- Aucune vérification de signature : n'importe qui peut envoyer un faux « paiement réussi » et faire marquer une fiche comme payée. Il faut vérifier le HMAC avec le secret partagé.
express.json()avant la vérification : le corps est converti en objet et le corps brut est perdu, alors que la signature se calcule sur les octets exacts reçus. Il fautexpress.raw()et vérifier avant de convertir.- Pas d'horodatage contrôlé : un message capturé peut être rejoué.
- Pas de gestion des doublons : l'émetteur réessaie en cas d'échec ou de lenteur ; sans mémoire des
iddéjà traités, le courriel part deux fois et le traitement est refait. - Traitement long avant la réponse : le courriel de 4 secondes retarde la réponse, l'émetteur peut considérer l'envoi comme raté et réessayer. On enregistre l'événement, on répond
204tout de suite, et on envoie le courriel dans une tâche de fond.
Tu avais trouvé ?
Exercice 3 sur 4 Écrire une requête GraphQL Réussi À revoir
Avec le schéma GraphQL du chapitre, écris la requête qui obtient, pour le visiteur 7, son prénom, sa région, et pour chacune de ses fiches validées l'identifiant et le montant validé.
Voir la réponseMasquer la réponse de l'exercice 3
query {
visiteur(id: "7") {
prenom
region
fiches(etat: VA) {
id
montantValide
}
}
}
La réponse aura exactement cette forme, sous data.visiteur. En REST, il aurait fallu deux requêtes, chacune renvoyant tous les champs.
Tu avais trouvé ?
Exercice 4 sur 4 Le coût du polling Réussi À revoir
Une application mobile installée sur 2 000 téléphones vérifie toutes les 30 secondes, de 8 h à 20 h, si une fiche a changé d'état. En moyenne, une fiche change d'état 3 fois par jour et par utilisateur.
- Combien de requêtes l'API reçoit-elle par jour ?
- Quelle proportion apporte une information nouvelle ?
- Quelle alternative proposes-tu ?
Voir la réponseMasquer la réponse de l'exercice 4
- 12 heures × 120 requêtes par heure = 1 440 requêtes par téléphone, soit 1 440 × 2 000 = 2 880 000 requêtes par jour.
- 3 × 2 000 = 6 000 changements utiles, soit 6 000 ÷ 2 880 000 ≈ 0,2 % des requêtes. Plus de 99,7 % sont inutiles, et elles vident la batterie des téléphones.
- Pousser l'information : une notification push sur mobile, ou une connexion SSE ou WebSocket quand l'application est ouverte, déclenchée par l'API au moment où l'état change. À défaut, espacer l'interrogation et utiliser des requêtes conditionnelles (
If-None-Match) pour que les réponses304soient légères.
Tu avais trouvé ?