Langues disponibles:

La mort du JSON dans le backend : pourquoi j’ai migré toute ma stack vers gRPC et Protobuf

Ce post a été initialement écrit en anglais. La traduction peut ne pas refléter 100% des idées originales de l'auteur.

Je commence ce post par une confession qui pourrait me valoir quelques ennemis : Je déteste le JSON.

Ce n’est pas une haine irrationnelle, du genre qui apparaît de nulle part. C’est une haine construite, brique par brique, au fil des années de débogage de payloads malformés, de champs qui auraient dû être des nombres mais qui arrivent en tant que chaînes de caractères, et ce classique null là où vous vous attendiez à un tableau vide. Le JSON est l’équivalent numérique d’une conversation téléphonique avec votre grand-mère : vous croyez avoir compris ce qu’elle a dit, mais quand vous arrivez, le gâteau aux carottes n’avait pas ce glaçage au chocolat tant attendu (les Brésiliens comprendront).

Douleur !

Pendant des années, j’ai accepté cette torture comme faisant partie du contrat social d’être un développeur backend. “C’est le standard”, disaient-ils. “Tout le monde l’utilise”, répétaient-ils. Et moi, comme un bon petit mouton, j’ai continué à sérialiser et désérialiser du texte comme si nous étions en 2005 et que la bande passante était infinie.

Jusqu’à ce que j’en ai eu assez.

Le problème : le texte est coûteux

Faisons un exercice mental. Imaginez que vous ayez besoin d’envoyer le nombre 42 de votre backend Go vers votre application Flutter.

En JSON, cela devient :

{"answer": 42}

Cela fait 14 octets de texte. Cela semble très peu, non ? Maintenant, imaginez que vous envoyez une liste de 10 000 transactions financières, chacune avec 15 champs. Soudain, ce “texte lisible par un humain” se transforme en mégaoctets de gaspillage.

L’analyse syntaxique du JSON est un rituel de sacrifice du CPU. Votre serveur doit :

  1. Convertir la structure Go en texte.
  2. Sérialiser ce texte en UTF-8.
  3. L’envoyer sur le réseau.
  4. Le client reçoit le texte.
  5. Le client analyse le texte pour le retransformer en structure.
  6. Le client prie pour que les types soient corrects.

Chacune de ces étapes consomme des cycles de traitement. Et comme je l’ai dit dans mon post À propos de moi : les bits consomment de l’énergie. Les gaspiller est moralement offensant.

La solution : Protobuf et la beauté du binaire

Protocol Buffers (Protobuf) est le format de sérialisation créé par Google pour résoudre exactement ce problème. Au lieu du texte, vous définissez un contrat (un fichier .proto), et le compilateur génère du code natif pour n’importe quel langage.

Ce même {"answer": 42} en Protobuf ? 2 octets. Ce n’est pas de la magie, c’est des maths. Le binaire est simplement plus efficace que le texte.

Mais les économies de bande passante ne sont que la cerise sur le gâteau. Le vrai cadeau, c’est le typage fort.

syntax = "proto3";

message Transaction {
  string id = 1;
  int64 amount_cents = 2;
  string currency = 3;
  int64 timestamp = 4;
  TransactionType type = 5;
}

enum TransactionType {
  UNKNOWN = 0;
  CREDIT = 1;
  DEBIT = 2;
}

Avec ce fichier, je génère du code pour Go, Dart (Flutter) et TypeScript (SolidJS) avec une seule commande. Si je change le type de amount_cents de int64 à string, le compilateur me crie dessus avant que je ne déploie. Finis les cauchemars du “ça marchait dans Postman mais ça a cassé dans l’app”.

gRPC : Le mariage parfait avec HTTP/2

Protobuf est le format. gRPC est le protocole de communication qui utilise ce format. Et c’est là que les choses deviennent intéressantes pour ceux qui se soucient des performances mobiles.

gRPC fonctionne par défaut sur HTTP/2 (et a déjà un support expérimental pour HTTP/3). Cela signifie :

  • Multiplexage : Plusieurs appels sur la même connexion TCP. Pas d’ouverture et de fermeture de connexions pour chaque requête.

  • Compression des en-têtes : HTTP/2 utilise HPACK pour compresser les en-têtes répétitifs. En REST, vous envoyez Content-Type: application/json dans chaque requête. En gRPC, cela est négocié une seule fois.

  • Streaming bidirectionnel : Vous voulez un chat en temps réel ? WebSockets ? Oubliez ça. gRPC le fait nativement.

Pour une application mobile Flutter consommant des données depuis un backend Go, la différence est brutale. Moins d’octets transférés signifie moins de batterie consommée, des temps de chargement plus courts et moins de risques que l’utilisateur abandonne pour aller regarder TikTok.

La stack : Go + Flutter + SolidJS

Mon architecture actuelle fonctionne comme ceci :

  1. Backend en Go : J’utilise protoc-gen-go et protoc-gen-go-grpc pour générer les stubs. Le serveur est ridiculement rapide parce que Go est déjà rapide, et maintenant il n’a plus besoin de perdre du temps à analyser du JSON.
  2. Mobile en Flutter : J’utilise le package Dart grpc. Le code généré par Protobuf est type-safe. Si le backend change un champ, l’app ne se compile pas tant que je ne l’ai pas mis à jour. C’est ça, la sécurité.
  3. Web en SolidJS : Ici arrive le seul hic. Les navigateurs ne parlent pas gRPC nativement (pour l’instant). La solution est d’utiliser gRPC-Web, un proxy qui traduit les appels HTTP/1.1 en gRPC. Ce n’est pas parfait, mais c’est toujours mieux que du REST pur.

Le fichier .proto est la source unique de vérité. Je le modifie une fois, je lance le compilateur, et toutes les plateformes sont synchronisées. Finis les jeux de maintenance de trois versions différentes de DTOs dans trois langages différents.

L’éléphant dans la pièce : le débogage

Je vais être honnête : déboguer du gRPC est plus difficile que déboguer du REST. Vous ne pouvez pas simplement ouvrir le navigateur et regarder le payload dans l’onglet Réseau. Le binaire n’est pas lisible par un humain.

Pour cela, j’utilise grpcurl (le curl du monde gRPC) et Kreya (une alternative à Postman pour gRPC). Je configure également la journalisation structurée sur le serveur pour capturer les requêtes désérialisées.

C’est un coût de configuration initial. Mais une fois que vous vous y êtes habitué, vous ne revenez pas en arrière.

Quand NE PAS utiliser gRPC

Comme tout en technologie, ce ne sont pas que des roses. gRPC est excessif pour :

  • Les APIs publiques : Si vous créez une API pour que des tiers la consomment, JSON/REST est toujours le standard. Personne ne veut apprendre votre schéma Protobuf juste pour s’intégrer. Ne soyez pas ennuyeux.

  • Les petits projets : Si votre backend est un simple CRUD avec 5 endpoints, la complexité supplémentaire n’en vaut pas la peine.

  • Le SEO et les crawlers : Les bots de Google ne comprennent pas gRPC. Si vous avez besoin que votre contenu soit indexé, REST + JSON est la voie à suivre.

Dans mon cas, je me concentre sur des systèmes internes où je contrôle toutes les extrémités. Je n’ai pas à me soucier de la compatibilité externe.

Conclusion : Le JSON n’est pas mort, mais il devrait être en congé

Écoutez, le JSON ne va pas disparaître. C’est l’anglais du monde des APIs : ce n’est pas la meilleure langue (je dois écrire un post sur à quel point je trouve l’anglais moche), mais tout le monde le parle. Pour la consommation humaine, le débogage rapide et le prototypage, il a encore sa place.

TODO: Écrire un post sur à quel point je trouve l'anglais moche

Mais si vous construisez des systèmes hautes performances, si vous vous souciez de la batterie du téléphone de votre utilisateur, si vous en avez marre de découvrir des bugs de typage en production… peut-être qu’il est temps de considérer l’alternative binaire.

J’ai migré. Mon serveur me remercie. Mon app me remercie. Ma santé mentale ? Eh bien, elle était déjà compromise depuis le 7-1 que l’équipe brésilienne a pris lors de la Coupe du Monde 2014.

À l’avenir, j’écrirai un post comparatif. Rien n’est plus technique que de se taire et de le faire, non ?

Avant que j’oublie :

TODO: Écrire un post comparant REST x gRPC en pratique

Voilà, je sors, bye !

Bye !