Skip to content

tRPC vs gRPC vs GraphQL : quel contrat d'API #504

Description

@khalilbenaz

lede: tRPC, gRPC et GraphQL résolvent le même problème — le contrat d'API — avec trois philosophies opposées ; le bon choix dépend de vos consommateurs, pas de la mode.
lede_en: tRPC, gRPC and GraphQL solve the same problem — the API contract — with three opposing philosophies; the right choice depends on your consumers, not on fashion.
title_en: tRPC vs gRPC vs GraphQL: which API contract

Le vrai sujet : le contrat, pas le protocole

Comparer tRPC, gRPC et GraphQL comme des concurrents interchangeables est une erreur fréquente. Ils partagent un objectif — garantir que le client et le serveur s'accordent sur la forme des données — mais tranchent des questions radicalement différentes. Le mauvais réflexe est de choisir selon la hype ; le bon est de partir de qui consomme votre API et de quelle frontière technique vous traversez.

  • Qui appelle ? — votre propre front TypeScript, une flotte de microservices, ou des clients tiers hétérogènes ?
  • Quelle frontière ? — intra-équipe, inter-services, ou API publique ?
  • Qui contrôle les deux bouts ? — la réponse conditionne presque tout.

tRPC : le contrat sans schéma

tRPC part d'un constat : si votre client et votre serveur sont tous deux en TypeScript et dans le même monorepo, pourquoi générer un schéma intermédiaire ? Le type de votre procédure serveur est le contrat, inféré directement côté client.

// Serveur
const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => db.user.find(input.id)),
});
export type AppRouter = typeof appRouter;

// Client — autocomplétion et types de bout en bout, zéro codegen
const user = await trpc.getUser.query({ id: "42" });

Aucune étape de génération, aucune duplication : renommez un champ côté serveur, le client casse à la compilation. Le prix de cette magie : elle ne fonctionne que dans l'univers TypeScript. Pour une API publique ou un client Swift/Kotlin, tRPC n'a rien à offrir.

gRPC : le contrat binaire à contrat fort

gRPC vise l'inverse : un contrat explicite, indépendant du langage, optimisé pour la communication service-à-service à haute performance. Vous définissez un .proto, le codegen produit des clients et serveurs dans une douzaine de langages, et les messages voyagent en Protobuf binaire sur HTTP/2.

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}
message GetUserRequest { string id = 1; }
  • Performance — sérialisation binaire compacte, multiplexage HTTP/2, streaming bidirectionnel natif.
  • Polyglotte fort — le .proto est la source de vérité pour tous les langages.
  • Faiblesse navigateur — gRPC n'est pas directement consommable depuis un navigateur sans proxy (gRPC-Web), ce qui le cantonne surtout au back-to-back.

GraphQL : le contrat piloté par le client

GraphQL déplace le pouvoir vers le consommateur. Le serveur expose un graphe typé, et chaque client demande exactement les champs dont il a besoin — ni plus (over-fetching), ni moins (under-fetching).

  • Idéal pour des clients hétérogènes — mobile, web et partenaires interrogent le même endpoint avec des besoins différents.
  • Agrégation — un seul appel peut composer plusieurs sources derrière le résolveur.
  • Coût — la flexibilité côté client déporte la complexité côté serveur : requêtes coûteuses, problème N+1, mise en cache HTTP plus délicate, et une surface d'attaque à maîtriser (profondeur, complexité).

Comparaison : lequel, quand

Critère tRPC gRPC GraphQL
Consommateur idéal front TS interne services back clients hétérogènes
Contrat types TS inférés .proto schéma SDL
Codegen aucun requis selon outillage
Navigateur natif via proxy natif
Perf réseau JSON binaire (top) JSON

La règle simple que j'applique :

  • Monorepo full-TypeScript, front + back maison → tRPC, sans hésiter.
  • Communication interne entre microservices, performance critique → gRPC.
  • Une API servant mobile, web et partenaires aux besoins divergents → GraphQL.
  • API publique REST-like simple et durable → parfois, un bon vieux REST/OpenAPI reste la meilleure réponse.

Pièges et limites

  • tRPC = couplage assumé — c'est un choix d'intégration, pas d'API publique ; ne l'exposez pas à des tiers.
  • gRPC en front — l'illusion de l'utiliser directement dans le navigateur mène au piège du proxy et de la complexité.
  • GraphQL sous-estimé — beaucoup l'adoptent pour la hype puis découvrent le coût du N+1 et du caching ; il faut de la discipline (dataloaders, limites de complexité).

Verdict

Il n'y a pas de gagnant absolu — il y a un outil par frontière. tRPC supprime le contrat quand vous contrôlez les deux bouts en TypeScript. gRPC gagne le back-to-back performant et polyglotte. GraphQL sert les clients aux besoins divergents. Mon conseil de senior : ne choisissez jamais sur la mode. Nommez votre consommateur, identifiez la frontière que vous traversez, et le bon outil devient évident. Et gardez à l'esprit que REST reste souvent le choix le plus sobre pour une API publique.

The real subject: the contract, not the protocol

Comparing tRPC, gRPC and GraphQL as interchangeable competitors is a common mistake. They share a goal — guaranteeing that client and server agree on the shape of the data — but settle radically different questions. The wrong reflex is to choose by hype; the right one is to start from who consumes your API and which technical boundary you cross.

  • Who calls? — your own TypeScript front-end, a fleet of microservices, or heterogeneous third-party clients?
  • Which boundary? — intra-team, inter-service, or public API?
  • Who controls both ends? — the answer conditions almost everything.

tRPC: the schema-less contract

tRPC starts from an observation: if your client and server are both TypeScript and in the same monorepo, why generate an intermediate schema? The type of your server procedure is the contract, inferred directly on the client.

// Server
const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => db.user.find(input.id)),
});
export type AppRouter = typeof appRouter;

// Client — end-to-end autocomplete and types, zero codegen
const user = await trpc.getUser.query({ id: "42" });

No generation step, no duplication: rename a field on the server and the client breaks at compile time. The price of this magic: it only works inside the TypeScript universe. For a public API or a Swift/Kotlin client, tRPC has nothing to offer.

gRPC: the strong binary contract

gRPC aims for the opposite: an explicit, language-independent contract optimized for high-performance service-to-service communication. You define a .proto, codegen produces clients and servers in a dozen languages, and messages travel as binary Protobuf over HTTP/2.

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}
message GetUserRequest { string id = 1; }
  • Performance — compact binary serialization, HTTP/2 multiplexing, native bidirectional streaming.
  • Strongly polyglot — the .proto is the source of truth for all languages.
  • Browser weakness — gRPC isn't directly consumable from a browser without a proxy (gRPC-Web), confining it mostly to back-to-back.

GraphQL: the client-driven contract

GraphQL shifts power to the consumer. The server exposes a typed graph, and each client requests exactly the fields it needs — no more (over-fetching), no less (under-fetching).

  • Ideal for heterogeneous clients — mobile, web and partners query the same endpoint with different needs.
  • Aggregation — a single call can compose several sources behind the resolver.
  • Cost — client-side flexibility pushes complexity to the server: expensive queries, the N+1 problem, trickier HTTP caching, and an attack surface to control (depth, complexity).

Comparison: which one, when

Criterion tRPC gRPC GraphQL
Ideal consumer internal TS front back services heterogeneous clients
Contract inferred TS types .proto SDL schema
Codegen none required tooling-dependent
Browser native via proxy native
Network perf JSON binary (top) JSON

The simple rule I apply:

  • Full-TypeScript monorepo, in-house front + back → tRPC, no hesitation.
  • Internal microservice communication, performance-critical → gRPC.
  • An API serving mobile, web and partners with diverging needs → GraphQL.
  • A simple, durable public REST-like API → sometimes, good old REST/OpenAPI is still the best answer.

Pitfalls and limits

  • tRPC = deliberate coupling — it's an integration choice, not a public-API one; don't expose it to third parties.
  • gRPC on the front-end — the illusion of using it directly in the browser leads to the proxy-and-complexity trap.
  • Underestimated GraphQL — many adopt it for the hype then discover the cost of N+1 and caching; it demands discipline (dataloaders, complexity limits).

Verdict

There is no absolute winner — there is one tool per boundary. tRPC removes the contract when you control both ends in TypeScript. gRPC wins performant, polyglot back-to-back. GraphQL serves clients with diverging needs. My senior advice: never choose on fashion. Name your consumer, identify the boundary you cross, and the right tool becomes obvious. And keep in mind that REST is often still the soberest choice for a public API.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions