Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

8.5 — Protocols API modernes

Confirmé 40 min prérequis : axe 8 (concepts-communs + un parcours backend)

🎯 Objectif : connaître les 6 grands protocoles API, savoir les comparer, et choisir le bon pour ton contexte. Pas une encyclopédie technique — une boussole de décision.

À l'issue de cet axe, tu sauras :

  • Comparer REST, GraphQL, gRPC, tRPC : forces, faiblesses, cas d'usage
  • Implémenter une API GraphQL minimale (schéma, query, mutation) avec Apollo ou Yoga
  • Comprendre gRPC + Protobuf et savoir quand l'utiliser (microservices internes)
  • Distinguer WebSocket vs Server-Sent Events selon le pattern de comm
  • Recevoir et émettre des WebHooks signés, idempotents, retryables
  • Choisir le protocole adapté en 2 minutes pour n'importe quel cas

💡 Termes glossaire : GraphQL , gRPC , SSE , WebSocket , RPC , Protobuf .

ProtocoleTransportFormatQuand l’utiliser
RESTHTTPJSONAPI publique standard, CRUD simple, cacheable HTTP
GraphQLHTTP (POST)JSONFrontend qui consomme N entités liées, équipes front autonomes
gRPCHTTP/2Protobuf binaireMicroservices internes, throughput élevé, langages multiples
tRPCHTTPJSON typéMonorepo TS full-stack, types partagés sans codegen
WebSocketTCP upgradeTexte/binaireBi-directionnel temps réel : chat, jeu, collab live
SSEHTTP keepaliveText eventsServer → client unidirectionnel : notifications, progress, LLM streaming
WebHookHTTP POSTJSONNotification asynchrone vers un système externe (Stripe, Clerk, GitHub)
  1. Communication client ↔ serveur ou serveur ↔ serveur ?

    • Client ↔ serveur web → REST, GraphQL, tRPC
    • Serveur ↔ serveur (microservices) → gRPC
  2. Le client a-t-il besoin de N relations imbriquées en 1 call ?

    • Oui (graphes, listes filtrées profondes) → GraphQL
    • Non (CRUD simple) → REST
  3. Tu fais du bi-directionnel temps réel ?

    • Oui (chat, jeu, collab) → WebSocket
    • Non, juste push serveur → client → SSE (plus simple, traverse mieux les proxys)
  4. Tu notifies un système externe asynchrone ?

    • Oui → WebHooks (avec signature + idempotence)

REST n’est pas un protocole, c’est un style : utiliser HTTP correctement.

GET /tasks ← lister
GET /tasks/42 ← détail
POST /tasks ← créer
PUT /tasks/42 ← remplacer (idempotent)
PATCH /tasks/42 ← modifier partiellement
DELETE /tasks/42 ← supprimer

Ce qui distingue un bon REST d’un mauvais :

Bon RESTMauvais REST
Codes HTTP sémantiques (200, 201, 204, 400, 404, 409, 422, 429, 500)Tout en 200 avec {success: false}
Idempotence respectée (PUT, DELETE multiples = même résultat)DELETE qui crée un truc en cas d’absence
Pagination cohérente (cursor ou page+limit)Tout retourner d’un coup
Versioning explicite (/v1/tasks ou Accept: application/vnd.api.v1+json)Pas de versioning → breaking change cataclysmique
HATEOAS si applicable (liens vers actions disponibles)Doc humaine seulement

Cas d’usage type : API publique consommée par N clients hétérogènes (web, mobile, partenaires B2B).

3. GraphQL — un seul endpoint, le client choisit ses champs

Section intitulée « 3. GraphQL — un seul endpoint, le client choisit ses champs »

Au lieu d’avoir 50 endpoints REST, tu as 1 endpoint (/graphql) et le client envoie une query qui décrit ce qu’il veut.

# Le client envoie cette query
query {
user(id: "42") {
id
name
posts(limit: 5) {
title
comments(limit: 3) {
author { name }
}
}
}
}
# Le serveur retourne exactement ces champs, rien d'autre
type User {
id: ID!
name: String!
email: String!
posts(limit: Int): [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
comments(limit: Int): [Comment!]!
}
type Query {
user(id: ID!): User
searchPosts(q: String!): [Post!]!
}
type Mutation {
createPost(title: String!, content: String!): Post!
}
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query { hello(name: String): String! }
`,
resolvers: {
Query: {
hello: (_, { name }) => `Hello, ${name ?? 'world'}!`,
},
},
}),
});
createServer(yoga).listen(3000);
1 endpoint, le client choisit ses champsCache HTTP cassé (tout passe en POST)
Schéma typé partagé front/backComplexité opérationnelle (DataLoader, query depth limit)
Évolution sans versioning (déprécation par champ)Risque de N+1 queries sans optimisation manuelle
Doc auto via introspectionAuthorization plus complexe (granularité champ)
Idéal pour BFF (Backend for Frontend)Pas adapté aux APIs publiques larges
  • Tu as une équipe frontend autonome qui itère vite et veut piloter ses requêtes.
  • Ton domaine a beaucoup de relations (réseaux sociaux, e-commerce avec catalogues profonds).
  • Tu construis un BFF qui agrège plusieurs APIs REST internes pour un client mobile.
  • API publique simple type CRUD → REST suffit, plus cacheable.
  • Microservices internes → gRPC plus rapide et typé binaire.
  • Petite app monolith où front et back sont la même équipe → tRPC plus simple.
Piège réel rencontré — Le piège N+1 en GraphQL Performance

Symptôme : tu fais une query qui retourne 50 users avec leurs 5 posts chacun. Ton resolver fait 1 query DB pour les users, puis 50 queries DB pour les posts (1 par user). Total = 51 queries.

Cause : les resolvers GraphQL sont appelés par champ, sans connaissance des autres demandes en cours.

Fix : utiliser DataLoader (lib officielle Facebook). Il batche les demandes individuelles en 1 query SQL WHERE user_id IN (...). C’est obligatoire dès qu’on dépasse le tutoriel.

RPC = Remote Procedure Call. Ton client appelle userService.GetUser({id: 42}) comme si c’était une fonction locale ; en dessous, ça transite via HTTP/2 + Protobuf binaire.

user.proto
syntax = "proto3";
package taskly;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc StreamUsers(StreamRequest) returns (stream User); // streaming !
}
message User {
string id = 1;
string email = 2;
string name = 3;
}
message GetUserRequest {
string id = 1;
}

Tu génères ensuite le code client + serveur pour Node, Python, Go, Java, etc. à partir de ce .proto. Le typage est garanti à la compilation côté client ET serveur.

Binaire = 5-10× moins de bande passante que JSONPas browser-friendly (gRPC-Web exige proxy)
Streaming bi-directionnel natifCurl impossible (binaire) → debug plus dur
Types stricts entre N langagesCodegen obligatoire (Buf, protoc)
HTTP/2 multiplexingCourbe d’apprentissage
Standard pour microservices internes (Google, Netflix, Uber)Pas adapté pour API publique web
  • Microservices internes : auth-servicepayment-serviceemail-service en gRPC, avec un gateway HTTP REST/GraphQL exposé au public.
  • Streaming : le serveur push des updates en continu (logs, métriques, events).
  • Polyglot : tes services sont en Go, Python, Node — gRPC garantit le contrat.
import { createServer } from 'node:http';
import { createConnectRouter } from '@connectrpc/connect';
import { UserService } from './gen/user_pb.js';
const router = createConnectRouter();
router.service(UserService, {
async getUser(req) {
return { id: req.id, email: 'user@example.com', name: 'Alice' };
},
});

Note 2026 : @connectrpc/connect (Buf) est plus moderne que grpc-js officiel et marche bien dans le navigateur. Recommandé.

Si tu fais du TS full-stack dans un monorepo, tRPC est imbattable.

server/router.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
user: t.router({
byId: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return await db.users.findById(input.id);
}),
}),
});
export type AppRouter = typeof appRouter;
// client.ts (importe juste le TYPE du serveur)
import type { AppRouter } from '../server/router';
import { createTRPCClient } from '@trpc/client';
const client = createTRPCClient<AppRouter>({ /* ... */ });
const user = await client.user.byId.query({ id: '42' });
// ↑ types autocomplétés depuis le serveur, sans codegen

Différenciateur : pas de schéma à écrire, pas de codegen — le type du serveur est importé directement par le client. C’est REST en pure TypeScript.

0 codegen, 0 schéma à maintenirCouplage TS-only — pas de mobile natif sans wrapper
Auto-complétion parfaite end-to-endPas un standard (= recrutement plus dur)
Validation Zod intégréeLimité aux apps full-stack TS

Monorepo TS avec Next.js + Hono ou Fastify où les mêmes équipes maintiennent front et back. Hors de ce cas, REST ou GraphQL sont mieux.

Connexion persistante entre client et serveur. Les deux peuvent envoyer des messages à tout moment.

  • Chat : client envoie chat.send, serveur push aux N autres.
  • Jeu en ligne : 60 messages/seconde dans les deux sens.
  • Collaboration live (Google Docs, Figma, Yjs) : opérations CRDT en push.
  • Trading / monitoring : prix temps réel.
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 3001 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
// Broadcast à tous les clients
wss.clients.forEach(c => c.send(JSON.stringify(msg)));
});
ws.send(JSON.stringify({ type: 'welcome' }));
});
  • Pas de cache HTTP (TCP brut après l’upgrade).
  • Authentification au handshake uniquement (pas de re-auth facile en cours).
  • Reconnexion à gérer côté client (réseau mobile coupe).
  • Scaling horizontal : besoin d’un broker (Redis Pub/Sub, NATS) pour relayer les messages entre instances.

Plus simple que WebSocket quand le client n’a rien à envoyer en continu.

GET /events HTTP/1.1
Accept: text/event-stream
# Réponse :
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"type":"notification","message":"Nouvelle commande"}
data: {"type":"progress","value":42}
event: error
data: {"reason":"timeout"}
import { streamSSE } from 'hono/streaming';
app.get('/events', (c) => {
return streamSSE(c, async (stream) => {
while (true) {
await stream.writeSSE({
data: JSON.stringify({ time: Date.now() }),
event: 'tick',
});
await stream.sleep(1000);
}
});
});
const es = new EventSource('/events');
es.onmessage = (e) => console.log(JSON.parse(e.data));
es.addEventListener('tick', (e) => console.log('tick:', e.data));
CritèreSSEWebSocket
DirectionServer → Client uniquementBi-directionnel
ProtocoleHTTP standardTCP upgrade
Reconnexion autoOui (natif)Non, à coder
Traverse les proxys d’entrepriseOuiSouvent bloqué
AuthentCookie HttpOnly natifTu dois passer le token au handshake
Cas d’usageNotifications, LLM streaming, progress, dashboard liveChat, jeu, collab

Règle 2026 : SSE par défaut pour le push serveur → client. WebSocket seulement si tu as besoin de bi-directionnel.

Tu fournis une URL à un service externe (Stripe, Clerk, GitHub). Quand un événement se produit chez eux, ils font un POST sur ton URL.

POST /webhooks/stripe HTTP/1.1
Stripe-Signature: t=1234567890,v1=abc123def456...
Content-Type: application/json
{"id":"evt_1","type":"checkout.session.completed","data":{...}}

Sans signature, n’importe qui peut POSTer sur ton endpoint et déclencher tes actions métier.

// Stripe
const event = stripe.webhooks.constructEvent(
rawBody, // BODY BRUT, pas re-parsé !
c.req.header('stripe-signature')!,
process.env.STRIPE_WEBHOOK_SECRET!
);

⚠️ Toujours utiliser le body brut (request.text()), pas await c.req.json(). Le HMAC est calculé sur les bytes exacts envoyés.

Le sender (Stripe, Clerk) peut retry si tu réponds lentement ou que ta connexion drop.

// Pattern : table `processed_events` avec contrainte unique
const exists = await db.execute({
sql: 'SELECT 1 FROM processed_events WHERE event_id = ?',
args: [event.id],
});
if (exists.rows.length > 0) return c.json({ ok: true }); // déjà traité
// Sinon, traiter + marquer
await db.transaction(async (tx) => {
await businessLogic(event);
await tx.execute({
sql: 'INSERT INTO processed_events (event_id) VALUES (?)',
args: [event.id],
});
});

Alternative : upsert sur la donnée métier (si l’effet est commutatif → mêmes données = même résultat → idempotent naturellement).

Stripe / Clerk timeout à 30s par défaut, retry si > 5s. Si ton traitement est long, mets en queue et réponds 200 immédiatement.

app.post('/webhooks/stripe', async (c) => {
const event = verifySignature(...);
await queue.publish('stripe.event', event); // queue interne
return c.json({ received: true }, 200); // réponds tout de suite
});

4. Logger tous les events reçus (incident postmortem)

Section intitulée « 4. Logger tous les events reçus (incident postmortem) »

Garde un audit log avec event_id, received_at, processed_at, status. Quand un événement n’a pas été traité comme attendu, tu peux rejouer depuis le log.

Si toi tu émets des webhooks vers tes clients :

RèglePourquoi
Signature HMAC dans un header (X-MyApp-Signature)Anti-spoofing chez ton client
event_id unique dans le bodyPermet l’idempotence côté client
Retries exponentiels (1m, 5m, 30m, 2h, 24h) si client retourne 5xxRésilience
Stop sur 4xx (sauf 429)4xx = mauvaise URL ou client cassé, retry inutile
Dashboard pour replay manuelQuand un client a eu un downtime, il veut récupérer les events ratés
CasChoix recommandé
API publique B2B (consommée par partenaires)REST + OpenAPI
App web monolithe TS, équipe full-stacktRPC
App web avec relations complexes, équipe front autonomeGraphQL + DataLoader
Microservices internes haute perfgRPC + Buf
Streaming LLM, notifications, progress barSSE
Chat, jeu, collab liveWebSocket
Notification depuis Stripe / Clerk / GitHubWebHooks (signés + idempotents)
Notification de toi vers tes clientsWebHooks émis (signés + retries)

Règle d’or : commence en REST. Migre vers GraphQL/gRPC/tRPC seulement quand tu as un cas concret qui justifie le coût. La majorité des apps web 2026 vivent très bien en REST + WebHooks.

Tu construis une API consommée par un mobile iOS, un web React, et un partenaire B2B en Java. Quel protocole choisir ?
Pourquoi vérifier la signature d'un WebHook reçu de Stripe ?
Tu veux pousser des updates de progression LLM (token par token) du serveur vers le client. Quel protocole ?