paint-brush
La simplicité de tRPC avec la puissance de GraphQLpar@wunderstef
6,554 lectures
6,554 lectures

La simplicité de tRPC avec la puissance de GraphQL

par Stefan Avram 13m2022/12/14
Read on Terminal Reader
Read this story w/o Javascript

Trop long; Pour lire

featured image - La simplicité de tRPC avec la puissance de GraphQL
Stefan Avram  HackerNoon profile picture
0-item


Je suis un grand fan de tRPC. L'idée d'exporter des types depuis le serveur et de les importer dans le client pour avoir un contrat de type sécurisé entre les deux, même sans étape de compilation, est tout simplement géniale. À toutes les personnes impliquées dans tRPC, vous faites un travail incroyable.


Cela dit, lorsque je regarde des comparaisons entre tRPC et GraphQL, il semble que nous comparions des pommes et des oranges.


Cela devient particulièrement évident lorsque vous regardez le discours public autour de GraphQL et tRPC. Regardez ce schéma de theo par exemple :

Theo a expliqué ce diagramme en profondeur et à première vue, il a beaucoup de sens. tRPC ne nécessite pas d'étape de compilation, l'expérience du développeur est incroyable et c'est beaucoup plus simple que GraphQL.


Mais est-ce vraiment le tableau complet, ou cette simplicité est-elle obtenue au prix d'autre chose ? Découvrons-le en créant une application simple avec tRPC et GraphQL.

Construisons un clone Facebook avec tRPC

Imaginons une arborescence de fichiers avec une page pour le fil d'actualités, un composant pour la liste des flux et un composant pour l'élément du fil.


 src/pages/news-feed ├── NewsFeed.tsx ├── NewsFeedList.tsx └── NewsFeedItem.tsx


Tout en haut de la page de flux, nous avons besoin d'informations sur l'utilisateur, les notifications, les messages non lus, etc.


Lors du rendu de la liste de flux, nous devons connaître le nombre d'éléments de flux, s'il existe une autre page et comment la récupérer.


Pour l'élément de flux, nous devons connaître l'auteur, le contenu, le nombre de likes et si l'utilisateur l'a aimé.


Si nous devions utiliser tRPC, nous créerions une "procédure" pour charger toutes ces données en une seule fois. Nous appellerions cette procédure en haut de la page, puis propagerions les données vers les composants.


Notre composant de flux ressemblerait à ceci :

 import { trpc } from '../utils/trpc' export function NewsFeed() { const feed = trpc.newsFeed.useQuery() return ( <div> <Avatar>{feed.user}</Avatar> <UnreadMessages> {feed.unreadMessages} unread messages </UnreadMessages> <Notifications> {feed.notifications} notifications </Notifications> <NewsFeedList feed={feed} /> </div> ) }


Examinons ensuite le composant de liste de flux :

 export function NewsFeedList({ feed }) { return ( <div> <h1>News Feed</h1> <p>There are {feed.items.length} items</p> {feed.items.map((item) => ( <NewsFeedItem item={item} /> ))} {feed.hasNextPage && ( <button onClick={feed.fetchNextPage}>Load more</button> )} </div> ) }


Et enfin, le composant de l'élément de flux :

 export function NewsFeedItem({ item }) { return ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> </div> ) }


Gardez à l'esprit que nous sommes toujours une seule équipe, tout est TypeScript, une seule base de code, et nous utilisons toujours tRPC.


Voyons de quelles données nous avons réellement besoin pour rendre la page. Nous avons besoin de l'utilisateur, des messages non lus, des notifications, des éléments de flux, du nombre d'éléments de flux, de la page suivante, de l'auteur, du contenu, du nombre de likes et si l'utilisateur l'a aimé.


Où pouvons-nous trouver des informations détaillées sur tout cela ? Pour comprendre les exigences en matière de données pour l'avatar, nous devons examiner le composant Avatar . Il existe des composants pour les messages et les notifications non lus, nous devons donc également les examiner. Le composant de liste de flux a besoin du nombre d'éléments, de la page suivante et des éléments de flux. Le composant d'élément de fil contient les exigences pour chaque élément de liste.


Au total, si nous voulons comprendre les exigences en matière de données pour cette page, nous devons examiner 6 composants différents. En même temps, nous ne savons pas vraiment quelles données sont réellement nécessaires pour chaque composant. Il n'y a aucun moyen pour chaque composant de déclarer les données dont il a besoin car tRPC n'a pas un tel concept.


Gardez à l'esprit qu'il ne s'agit que d'une seule page. Que se passe-t-il si nous ajoutons des pages similaires mais légèrement différentes ?


Disons que nous construisons une variante du fil d'actualités, mais au lieu d'afficher les derniers messages, nous affichons les messages les plus populaires.


Nous pourrions plus ou moins utiliser les mêmes composants, avec seulement quelques changements. Disons que les messages populaires ont des badges spéciaux qui nécessitent des données supplémentaires.


Doit-on créer une nouvelle procédure pour cela ? Ou peut-être pourrions-nous simplement ajouter quelques champs supplémentaires à la procédure existante ?


Cette approche s'adapte-t-elle bien si nous ajoutons de plus en plus de pages ? Cela ne ressemble-t-il pas au problème que nous avons rencontré avec les API REST ? Nous avons même des noms célèbres pour ces problèmes, comme Overfetching et Underfetching, et nous n'en sommes même pas arrivés au point où nous parlons du problème N+1.


À un moment donné, nous pourrions décider de diviser la procédure en une procédure racine et plusieurs sous-procédures. Que se passe-t-il si nous récupérons un tableau au niveau racine, puis pour chaque élément du tableau, nous devons appeler une autre procédure pour récupérer plus de données ?


Une autre ouverture pourrait être d'introduire des arguments dans la version initiale de notre procédure, par exemple trpc.newsFeed.useQuery({withPopularBadges: true}) .


Cela fonctionnerait, mais on dirait que nous commençons à réinventer les fonctionnalités de GraphQL.

Construisons un clone Facebook avec GraphQL

Maintenant, comparons cela avec GraphQL. GraphQL a le concept de Fragments, qui nous permet de déclarer les exigences de données pour chaque composant. Des clients comme Relay vous permettent de déclarer une seule requête GraphQL en haut de la page et d'inclure des fragments des composants enfants dans la requête.


De cette façon, nous effectuons toujours une seule récupération en haut de la page, mais le framework nous aide en fait à déclarer et à collecter les exigences en matière de données pour chaque composant.

Regardons le même exemple en utilisant GraphQL, Fragments et Relay. Pour des raisons de paresse, le code n'est pas correct à 100% car j'utilise Copilot pour l'écrire, mais il devrait être très proche de ce à quoi il ressemblerait dans une vraie application.


 import { graphql } from 'react-relay' export function NewsFeed() { const feed = useQuery(graphql` query NewsFeedQuery { user { ...Avatar_user } unreadMessages { ...UnreadMessages_unreadMessages } notifications { ...Notifications_notifications } ...NewsFeedList_feed } `) return ( <div> <Avatar user={feed.user} /> <UnreadMessages unreadMessages={feed.unreadMessages} /> <Notifications notifications={feed.notifications} /> <NewsFeedList feed={feed} /> </div> ) }


Ensuite, regardons le composant de liste de flux. Le composant de liste de flux déclare un fragment pour lui-même et inclut le fragment pour le composant d'élément de flux.


 import { graphql } from 'react-relay' export function NewsFeedList({ feed }) { const list = useFragment( graphql` fragment NewsFeedList_feed on NewsFeed { items { ...NewsFeedItem_item } hasNextPage } `, feed ) return ( <div> <h1>News Feed</h1> <p>There are {feed.items.length} items</p> {feed.items.map((item) => ( <NewsFeedItem item={item} /> ))} {feed.hasNextPage && ( <button onClick={feed.fetchNextPage}>Load more</button> )} </div> ) }


Et enfin, le composant de l'élément de flux :

 import { graphql } from 'react-relay' export function NewsFeedItem({ item }) { const item = useFragment( graphql` fragment NewsFeedItem_item on NewsFeedItem { author { name } content likes hasLiked } `, item ) return ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> </div> ) }


Ensuite, créons une variante du fil d'actualités avec des badges populaires sur les éléments du fil. Nous pouvons réutiliser les mêmes composants, car nous pouvons utiliser la directive @include pour inclure conditionnellement le fragment de badge populaire.

 import { graphql } from 'react-relay' export function PopularNewsFeed() { const feed = useQuery(graphql` query PopularNewsFeedQuery($withPopularBadges: Boolean!) { user { ...Avatar_user } unreadMessages { ...UnreadMessages_unreadMessages } notifications { ...Notifications_notifications } ...NewsFeedList_feed } `) return ( <div> <Avatar user={feed.user} /> <UnreadMessages unreadMessages={feed.unreadMessages} /> <Notifications notifications={feed.notifications} /> <NewsFeedList feed={feed} /> </div> ) }


Ensuite, regardons à quoi pourrait ressembler l'élément de liste de flux mis à jour :

 import { graphql } from 'react-relay' export function NewsFeedItem({ item }) { const item = useFragment( graphql` fragment NewsFeedItem_item on NewsFeedItem { author { name } content likes hasLiked ...PopularBadge_item @include(if: $withPopularBadges) } `, item ) return ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> {item.popularBadge && <PopularBadge badge={item.popularBadge} />} </div> ) }


Comme vous pouvez le constater, GraphQL est assez flexible et nous permet de créer des applications Web complexes, y compris des variantes d'une même page, sans avoir à dupliquer trop de code.

Les fragments GraphQL nous permettent de déclarer les exigences en matière de données au niveau du composant

De plus, les fragments GraphQL nous permettent de déclarer explicitement les exigences en matière de données pour chaque composant, qui sont ensuite hissées en haut de la page, puis récupérées en une seule requête.


GraphQL sépare l'implémentation de l'API de la récupération des données

La grande expérience de développeur de tRPC est obtenue en fusionnant deux préoccupations très différentes en un seul concept, la mise en œuvre d'API et la consommation de données.


Il est important de comprendre qu'il s'agit d'un compromis. Il n'y a pas de déjeuner gratuit. La simplicité du tRPC se fait au détriment de la flexibilité.


Avec GraphQL, vous devez investir beaucoup plus dans la conception de schémas, mais cet investissement porte ses fruits dès que vous devez adapter votre application à de nombreuses pages, mais liées.


En séparant l'implémentation de l'API de la récupération des données, il devient beaucoup plus facile de réutiliser la même implémentation de l'API pour différents cas d'utilisation.

Le but des API est de séparer l'implémentation interne de l'interface externe

Il y a un autre aspect important à prendre en compte lors de la création d'API. Vous commencez peut-être avec une API interne qui est exclusivement utilisée par votre propre frontend, et tRPC pourrait être un excellent choix pour ce cas d'utilisation.


Mais qu'en est-il de l'avenir de votre entreprise? Quelle est la probabilité que vous agrandissiez votre équipe ? Est-il possible que d'autres équipes, voire des tiers veuillent consommer vos API ?

REST et GraphQL sont tous deux conçus dans un esprit de collaboration. Toutes les équipes n'utiliseront pas TypeScript, et si vous franchissez les frontières de l'entreprise, vous souhaiterez exposer les API d'une manière facile à comprendre et à utiliser.


Il existe de nombreux outils pour exposer et documenter les API REST et GraphQL, alors que tRPC n'est clairement pas conçu pour ce cas d'utilisation.


Donc, même s'il est bon de commencer avec le tRPC, vous risquez fort de le dépasser à un moment donné, ce que je pense que Theo a également mentionné dans l'une de ses vidéos.


Il est certainement possible de générer une spécification OpenAPI à partir d'une API tRPC, l'outillage existe, mais si vous construisez une entreprise qui finira par s'appuyer sur l'exposition d'API à des tiers, vos RPC ne pourront pas rivaliser avec REST bien conçu et API GraphQL.

Conclusion

Comme indiqué au début, je suis un grand fan des idées derrière tRPC. C'est un grand pas dans la bonne direction, rendant la récupération de données plus simple et plus conviviale pour les développeurs.


GraphQL, Fragments et Relay, d'autre part, sont des outils puissants qui vous aident à créer des applications Web complexes. En même temps, la configuration est assez complexe et il y a de nombreux concepts à apprendre jusqu'à ce que vous maîtrisiez le tout.


Bien que tRPC vous permette de démarrer rapidement, il est très probable que vous deviendrez trop grand pour son architecture à un moment donné. Si vous décidez aujourd'hui de parier sur GraphQL ou tRPC, vous devez tenir compte de l'évolution de votre avenir. Quelle sera la complexité des exigences de récupération de données ? Y aura-t-il plusieurs équipes qui utiliseront vos API ? Allez-vous exposer vos API à des tiers ?


Perspectives

Cela dit, et si nous pouvions combiner le meilleur des deux mondes ? À quoi ressemblerait un client API combinant la simplicité de tRPC avec la puissance de GraphQL ? Pourrions-nous créer un client API TypeScript pur qui nous donne la puissance de Fragments et Relay, combinée à la simplicité de tRPC ?


Imaginez que nous prenions les idées de tRPC et que nous les combinions avec ce que nous avons appris de GraphQL et de Relay.


Voici un petit aperçu :


 // src/pages/index.tsx import { useQuery } from '../../.wundergraph/generated/client' import { Avatar_user } from '../components/Avatar' import { UnreadMessages_unreadMessages } from '../components/UnreadMessages' import { Notifications_notifications } from '../components/Notifications' import { NewsFeedList_feed } from '../components/NewsFeedList' export function NewsFeed() { const feed = useQuery({ operationName: 'NewsFeed', query: (q) => ({ user: q.user({ ...Avatar_user.fragment, }), unreadMessages: q.unreadMessages({ ...UnreadMessages_unreadMessages.fragment, }), notifications: q.notifications({ ...Notifications_notifications.fragment, }), ...NewsFeedList_feed.fragment, }), }) return ( <div> <Avatar /> <UnreadMessages /> <Notifications /> <NewsFeedList /> </div> ) } // src/components/Avatar.tsx import { useFragment, Fragment } from '../../.wundergraph/generated/client' export const Avatar_user = Fragment({ on: 'User', fragment: ({ name, avatar }) => ({ name, avatar, }), }) export function Avatar() { const data = useFragment(Avatar_user) return ( <div> <h1>{data.name}</h1> <img src={data.avatar} /> </div> ) } // src/components/NewsFeedList.tsx import { useFragment, Fragment } from '../../.wundergraph/generated/client' import { NewsFeedItem_item } from './NewsFeedItem' export const NewsFeedList_feed = Fragment({ on: 'NewsFeed', fragment: ({ items }) => ({ items: items({ ...NewsFeedItem_item.fragment, }), }), }) export function NewsFeedList() { const data = useFragment(NewsFeedList_feed) return ( <div> {data.items.map((item) => ( <NewsFeedItem item={item} /> ))} </div> ) } // src/components/NewsFeedItem.tsx import { useFragment, Fragment } from '../../.wundergraph/generated/client' export const NewsFeedItem_item = Fragment({ on: 'NewsFeedItem', fragment: ({ id, author, content }) => ({ id, author, content, }), }) export function NewsFeedItem() { const data = useFragment(NewsFeedItem_item) return ( <div> <h1>{data.title}</h1> <p>{data.content}</p> </div> ) }


Qu'est-ce que tu penses? Utiliseriez-vous quelque chose comme ça? Voyez-vous l'intérêt de définir des dépendances de données au niveau des composants ou préférez-vous vous en tenir à la définition de procédures distantes au niveau de la page ? J'aimerais entendre vos pensées...


Nous sommes actuellement en phase de conception pour créer la meilleure expérience de récupération de données pour React, NextJS et tous les autres frameworks. Si ce sujet vous intéresse, suivez-moi sur Twitter pour rester à jour.


Si vous souhaitez rejoindre la discussion et discuter des RFC avec nous, n'hésitez pas à rejoindre notre serveur Discord .

Prochain