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.
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.
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.
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..protoest la source de vérité pour tous les langages.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).
Comparaison : lequel, quand
.protoLa règle simple que j'applique :
Pièges et limites
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.
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.
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..protois the source of truth for all languages.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).
Comparison: which one, when
.protoThe simple rule I apply:
Pitfalls and 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.