Aller au contenu principal

REST, GraphQL ou gRPC : choisir le contrat de son API

Trois façons d'exposer les mêmes données, trois contrats avec des conséquences différentes sur le cache, le typage et ce que le navigateur sait appeler. Cet article écrit le même catalogue produits dans les trois, avec du code exécuté, ce que chacun coûte en pratique, et les pièges qui apparaissent au bout de six mois de production.

Architecture logicielle ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

Le choix du contrat d'API se fait souvent sans discussion. On prend REST parce que c'est ce que le tutoriel utilisait, GraphQL parce qu'une conférence en a parlé, gRPC parce qu'une offre d'emploi le mentionnait. Ces trois options font le même travail de base, transporter des données entre un appelant et un serveur, et divergent sur des points qui apparaissent tard.

Le sujet mérite une attention particulière quand on débute, parce que ce contrat est la seule partie de ton architecture que les autres voient. Le reste, découpage du code, base de données, hébergement, peut changer sans prévenir personne. Une URL publiée, un schéma exposé, un fichier de définition partagé, ça se renégocie avec tous ceux qui l'utilisent.

Ce choix constitue le premier des quatre niveaux d'une architecture d'API, celui qui engage le plus vis-à-vis de l'extérieur. Trois questions départagent les trois options : qui appelle, quelle latitude il a sur la forme des données qu'il reçoit, et ce que le réseau entre vous deux sait mettre en cache. Voici le même catalogue produits écrit trois fois, avec ce que chaque écriture implique.

Le cas de départ

Une boutique en ligne. Un catalogue de produits, chacun avec un nom, un prix et un état de stock. Trois consommateurs : le site web, l'application mobile, et un service interne de facturation qui a besoin des prix mille fois par minute.

Ce cas est assez petit pour tenir dans un article et assez complet pour faire apparaître les différences. Il montre aussi qu'un projet réel finit souvent avec deux contrats plutôt qu'un seul.

REST : des ressources et les mécanismes natifs de HTTP

En REST, chaque chose accessible a sa propre adresse, et les verbes HTTP disent ce qu'on en fait. Une lecture est un GET, une création un POST, une suppression un DELETE.

GET /v1/produits/42 HTTP/1.1
Host: api.boutique.fr
Accept: application/json
 
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
ETag: "a3f9c1"
 
{"id": 42, "nom": "Clavier 60%", "prix_cents": 8900}

Les deux en-têtes de la réponse méritent qu'on s'y arrête. Cache-Control autorise n'importe quel intermédiaire, navigateur ou CDN, à garder cette réponse une minute sans redemander au serveur. ETag permet au client de redemander la ressource en disant « donne-la-moi seulement si elle a changé », et de recevoir un 304 sans corps le cas échéant. Cette mécanique est native, elle ne demande aucune bibliothèque, et elle absorbe une part de trafic considérable sur les données peu changeantes. Le détail de ce fonctionnement est traité dans l'article sur ce qui se passe entre le navigateur et le serveur.

Côté serveur, en Python avec FastAPI, la syntaxe recommandée depuis la version 0.95 passe par Annotated pour déclarer les paramètres, plutôt que par une valeur par défaut de type Query(). Le code ci-dessous a été exécuté tel quel.

from typing import Annotated
 
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
 
app = FastAPI()
 
 
class Produit(BaseModel):
    id: int
    nom: str
    prix_cents: int
 
 
CATALOGUE = [
    Produit(id=1, nom="Clavier 60%", prix_cents=8900),
    Produit(id=2, nom="Souris verticale", prix_cents=4500),
]
 
 
@app.get("/v1/produits", response_model=list[Produit])
def lister_produits(limite: Annotated[int, Query(ge=1, le=100)] = 20) -> list[Produit]:
    return CATALOGUE[:limite]
 
 
@app.get("/v1/produits/{produit_id}", response_model=Produit)
def lire_produit(produit_id: int) -> Produit:
    for produit in CATALOGUE:
        if produit.id == produit_id:
            return produit
    raise HTTPException(status_code=404, detail="Produit inconnu")

Une requête avec limite=999 renvoie un 422 sans que tu aies écrit la moindre validation : les bornes déclarées dans Query(ge=1, le=100) suffisent. C'est le genre de détail qui explique pourquoi ce style s'est imposé dans la documentation officielle.

Le point faible de REST apparaît côté client. Pour afficher une fiche produit avec son stock et les trois derniers avis, il faut trois appels, ou un endpoint spécial qui renvoie tout ensemble. Multiplie ce besoin par le nombre d'écrans et tu obtiens une API qui grossit à chaque nouvelle maquette. Les autres décisions de conception, nommage, idempotence, format d'erreur normalisé, sont traitées dans l'article dédié à la conception d'une API REST.

GraphQL : le client décrit ce qu'il veut

GraphQL inverse la responsabilité. Le serveur publie un schéma typé qui décrit tout ce qui est accessible, et le client compose sa requête en demandant exactement les champs dont il a besoin.

# Schéma publié par le serveur
type Produit {
  id: ID!
  nom: String!
  prixCents: Int!
  stock: Stock!
}
 
type Stock {
  disponible: Boolean!
  quantite: Int!
}
 
type Query {
  produit(id: ID!): Produit
  produits(limite: Int = 20): [Produit!]!
}
# Requête envoyée par l'application mobile
query FicheProduit($id: ID!) {
  produit(id: $id) {
    nom
    prixCents
    stock { disponible }
  }
}
 
# Réponse
{
  "data": {
    "produit": {
      "nom": "Clavier 60%",
      "prixCents": 8900,
      "stock": { "disponible": true }
    }
  }
}

L'application mobile n'a pas reçu la quantité en stock, parce qu'elle ne l'a pas demandée. Le site web, sur le même schéma, peut demander la quantité exacte plus les avis, en un seul aller-retour. Aucun endpoint supplémentaire à écrire côté serveur.

La contrepartie touche au cache, et c'est le point que les comparatifs enthousiastes passent sous silence. La documentation officielle de GraphQL est explicite : les implémentations conformes à la spécification GraphQL over HTTP supportent POST par défaut, et peuvent supporter GET pour les opérations de lecture. Or seul GET est considéré comme cacheable par défaut par les intermédiaires. Passer par GET permet donc de retrouver le cache HTTP et les CDN, à condition que la requête tienne dans les limites de taille d'URL imposées par les navigateurs et les CDN, ce qui devient vite contraignant sur des requêtes complexes.

La parade documentée s'appelle requête persistée : le client envoie une empreinte courte au lieu du texte complet, le serveur retrouve la requête correspondante dans son magasin avant de la valider et de l'exécuter. À noter aussi, les mutations doivent obligatoirement passer par POST, GET reste réservé aux lectures.

REST, GET Navigateur CDN, cache réponse servie sans atteindre le serveur GraphQL, POST Navigateur CDN traversé Serveur chaque appel gRPC Navigateur Proxy gRPC-Web Service gRPC binaire, HTTP/2
Le trajet réseau diffère selon le contrat. C'est ce trajet, plus que la syntaxe, qui décide de la charge encaissée par ton serveur.

gRPC : un contrat compilé, du binaire, et pas de navigateur

gRPC part d'un fichier de définition. Ce fichier est compilé pour générer le code client et le code serveur dans le langage de ton choix, ce qui supprime la rédaction manuelle des appels et des structures.

syntax = "proto3";
 
package catalogue.v1;
 
service Catalogue {
  rpc LireProduit(LireProduitRequete) returns (Produit);
  rpc ListerProduits(ListerProduitsRequete) returns (stream Produit);
}
 
message LireProduitRequete {
  string id = 1;
}
 
message ListerProduitsRequete {
  int32 limite = 1;
}
 
message Produit {
  string id = 1;
  string nom = 2;
  int32 prix_cents = 3;
}

Les numéros à droite des champs ne sont pas décoratifs, ils identifient le champ dans l'encodage binaire. Renuméroter un champ existant casse tous les clients déjà déployés, alors que renommer le champ en gardant son numéro ne casse rien. Cette propriété rend la compatibilité ascendante gérable, à condition de la connaître.

Le service de facturation qui interroge les prix mille fois par minute est le cas d'usage idéal : contrat strict, encodage compact, connexion HTTP/2 réutilisée, latence basse. Le mot stream dans la définition ouvre en plus un flux continu de résultats, plutôt qu'une grosse réponse d'un bloc.

Le tableau de décision

Critère REST GraphQL gRPC
Appelable depuis un navigateur Directement Directement Via gRPC-Web et un proxy
Cache HTTP et CDN Natif sur les GET Demande GET et requêtes persistées Hors sujet
Écrans aux besoins variés Endpoints qui se multiplient Son terrain de prédilection Peu adapté
Appels internes à haut débit Convenable Rarement le bon choix Le meilleur des trois
Outillage de débogage courant curl, onglet réseau Explorateur de schéma Outils spécifiques, payload illisible
Coût d'apprentissage Faible Moyen côté serveur Élevé, chaîne de compilation

Pour la boutique de l'exemple, le montage raisonnable combine deux contrats : REST pour l'API publique et les partenaires, gRPC entre le service de facturation et le catalogue. GraphQL devient intéressant le jour où trois équipes frontend différentes réclament chacune leur variante de la même fiche produit.

Les pièges

Ouvrir un schéma GraphQL sans limite de profondeur

Une requête qui descend d'un produit vers ses avis, puis vers l'auteur de chaque avis, puis vers ses autres avis, peut boucler très profond et mobiliser ton serveur pendant plusieurs secondes. Sur une API publique, c'est un vecteur de déni de service qui ne demande aucune compétence particulière. Limite de profondeur, budget de complexité et délai maximum d'exécution font partie du minimum à poser avant l'ouverture.

Le N+1 déguisé en champ innocent

Demander cent produits avec leur stock déclenche naïvement une requête pour la liste, puis cent requêtes pour les stocks. Le mécanisme de regroupement, souvent appelé chargeur de données, existe dans toutes les bibliothèques sérieuses, et beaucoup d'équipes le découvrent après la première montée en charge. Le sujet rejoint la séparation de l'accès aux données traitée dans l'article sur le repository pattern.

Choisir gRPC pour un front web

Le proxy de traduction s'ajoute à l'infrastructure, le flux montant disparaît, et l'onglet réseau du navigateur affiche du binaire au lieu de JSON lisible. Sur un projet à trois personnes, cette addition se paie tous les jours pour un gain qui ne se mesure pas à ce volume de trafic.

Renvoyer 200 avec une erreur dans le corps

Ce réflexe traîne dans beaucoup d'API REST maison. Il casse toutes les mécaniques automatiques : le client ne peut pas distinguer un succès d'un échec sans lire le corps, les intermédiaires mettent en cache une erreur, la supervision ne voit rien. En GraphQL, la question se pose différemment puisque le transport répond 200 et que les erreurs vivent dans un champ dédié de la réponse, ce qui demande une convention explicite et documentée pour ton équipe.

Changer le contrat sans plan de sortie

Retirer un champ, renommer une clé, modifier un type : chacune de ces actions casse des clients que tu ne contrôles pas. Les trois contrats ont leur mécanique de transition, préfixe de version et en-tête de dépréciation en REST, directive de dépréciation dans le schéma GraphQL, numéros de champ réservés en protobuf. Aucune ne fonctionne si personne ne prévient les consommateurs.

Ce que je conseille quand on débute

Commence par REST, et va au bout du sujet : codes de retour justes, pagination, authentification, format d'erreur cohérent, documentation générée. Cette base sert dans les deux autres mondes, puisque GraphQL et gRPC circulent tous les deux sur HTTP et reprennent une partie de ses conventions.

Ajoute GraphQL quand tu as un besoin qui le justifie, pas avant. Le signal fiable ressemble à ceci : plusieurs clients aux besoins divergents, et une liste d'endpoints qui s'allonge à chaque nouvelle maquette. Garde gRPC pour les échanges de service à service, là où il montre l'écart le plus net. Les décisions qui viennent après, exposition et infrastructure, sont situées dans la grille des quatre niveaux.

Pour la pratique, la formation Créer une API REST avec Python et FastAPI couvre ce parcours côté Python, et Spring Boot : développer des API REST en Java le fait côté JVM. Pour la partie authentification, l'article sur JWT ou session traite la décision qui vient juste après le choix du contrat.

Un mot pour finir sur les comparatifs que tu croiseras. Beaucoup opposent les trois contrats sur des chiffres de performance mesurés en laboratoire, à mille appels par seconde, sur des machines vides. Ces chiffres existent, ils sont exacts, et ils ne décrivent presque jamais ta situation.

Questions fréquentes

GraphQL remplace-t-il REST ?

Non, les deux cohabitent dans beaucoup d'architectures. GraphQL sert les interfaces aux besoins variables, REST sert les intégrations partenaires et les webhooks, où un contrat simple et cacheable vaut mieux qu'un langage de requête. Publier les deux sur les mêmes données est un montage courant.

Peut-on mettre en cache du GraphQL ?

Oui, mais pas gratuitement. La documentation officielle recommande d'utiliser GET pour les lectures, ce qui rend la réponse cacheable par les intermédiaires, et de passer par des requêtes persistées quand le texte de la requête dépasse les limites de longueur d'URL. Il reste aussi le cache applicatif côté serveur et le cache normalisé côté client, qui traitent d'autres problèmes.

Faut-il connaître gRPC pour un premier poste de développeur ?

Savoir ce que c'est et à quoi ça sert suffit largement. Les postes juniors travaillent presque toujours sur du REST, parfois sur du GraphQL. gRPC apparaît dans les architectures internes de sociétés qui ont plusieurs services et un volume d'appels important, et il s'apprend en place quand le besoin arrive.

Un projet de portfolio doit-il utiliser GraphQL pour se démarquer ?

Une API REST bien construite, avec une gestion d'erreurs propre, des tests et une documentation, impressionne davantage qu'un schéma GraphQL posé sur trois entités. Ce que regarde un recruteur technique, c'est la cohérence des décisions et la capacité à les expliquer, pas la présence du mot GraphQL sur le dépôt.

Et WebSocket dans tout ça ?

WebSocket répond à un besoin différent : maintenir une connexion ouverte pour que le serveur pousse des messages sans être sollicité. Il complète l'un des trois contrats plutôt qu'il ne les remplace, sur les usages de messagerie instantanée, de notifications ou de tableau de bord temps réel.

Peut-on migrer d'un contrat à l'autre plus tard ?

Oui, à condition que la logique métier ne vive pas dans la couche d'exposition. Si tes règles sont dans des services testables et que les contrôleurs se contentent de traduire, ajouter un schéma GraphQL au-dessus d'une base REST existante représente un travail raisonnable. Si le métier est mélangé aux contrôleurs, la migration devient une réécriture.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous