paint-brush
La simplicidad de tRPC con el poder de GraphQLpor@wunderstef
6,554 lecturas
6,554 lecturas

La simplicidad de tRPC con el poder de GraphQL

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

Demasiado Largo; Para Leer

featured image - La simplicidad de tRPC con el poder de GraphQL
Stefan Avram  HackerNoon profile picture
0-item


Soy un gran fan de tRPC. La idea de exportar tipos desde el servidor e importarlos en el cliente para tener un contrato de tipo seguro entre ambos, incluso sin un paso de tiempo de compilación, es simplemente brillante. A todas las personas que están involucradas en tRPC, están haciendo un trabajo increíble.


Dicho esto, cuando veo comparaciones entre tRPC y GraphQL, parece que estamos comparando manzanas y naranjas.


Esto se vuelve especialmente evidente cuando observa el discurso público sobre GraphQL y tRPC. Mira este diagrama de Theo , por ejemplo:

Theo explicó este diagrama en profundidad y, a primera vista, tiene mucho sentido. tRPC no requiere un paso de tiempo de compilación, la experiencia del desarrollador es increíble y es mucho más simple que GraphQL.


Pero, ¿es esa realmente la imagen completa, o se logra esta simplicidad a costa de algo más? Averigüémoslo creando una aplicación simple con tRPC y GraphQL.

Construyamos un clon de Facebook con tRPC

Imaginemos un árbol de archivos con una página para la fuente de noticias, un componente para la lista de fuentes y un componente para el elemento de fuente.


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


En la parte superior de la página de noticias, necesitamos información sobre el usuario, notificaciones, mensajes no leídos, etc.


Al representar la lista de fuentes, necesitamos saber la cantidad de elementos de la fuente, si hay otra página y cómo obtenerla.


Para el elemento de alimentación, necesitamos saber el autor, el contenido, la cantidad de Me gusta y si al usuario le ha gustado.


Si tuviéramos que usar tRPC, crearíamos un "procedimiento" para cargar todos estos datos de una sola vez. Llamaríamos a este procedimiento en la parte superior de la página y luego propagaríamos los datos a los componentes.


Nuestro componente de alimentación se vería así:

 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> ) }


A continuación, veamos el componente de la lista de feeds:

 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> ) }


Y, por último, el componente del elemento de alimentación:

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


Tenga en cuenta que todavía somos un solo equipo, todo es TypeScript, una sola base de código y todavía usamos tRPC.


Averigüemos qué datos necesitamos realmente para representar la página. Necesitamos el usuario, los mensajes no leídos, las notificaciones, los elementos del feed, la cantidad de elementos del feed, la página siguiente, el autor, el contenido, el número de Me gusta y si le ha gustado al usuario.


¿Dónde podemos encontrar información detallada sobre todo esto? Para comprender los requisitos de datos para el avatar, debemos observar el componente Avatar . Hay componentes para mensajes y notificaciones no leídos, por lo que también debemos analizarlos. El componente de lista de fuentes necesita el número de elementos, la página siguiente y los elementos de fuente. El componente de elemento de alimentación contiene los requisitos para cada elemento de la lista.


En total, si queremos comprender los requisitos de datos para esta página, debemos observar 6 componentes diferentes. Al mismo tiempo, no sabemos realmente qué datos se necesitan realmente para cada componente. No hay forma de que cada componente declare qué datos necesita, ya que tRPC no tiene ese concepto.


Tenga en cuenta que esto es solo una sola página. ¿Qué sucede si agregamos páginas similares pero ligeramente diferentes?


Digamos que estamos creando una variante de la fuente de noticias, pero en lugar de mostrar las publicaciones más recientes, mostramos las publicaciones más populares.


Podríamos usar más o menos los mismos componentes, con solo algunos cambios. Digamos que las publicaciones populares tienen insignias especiales que requieren datos adicionales.


¿Deberíamos crear un nuevo procedimiento para esto? ¿O tal vez podríamos simplemente agregar algunos campos más al procedimiento existente?


¿Este enfoque escala bien si agregamos más y más páginas? ¿No suena como el problema que hemos tenido con las API REST? Incluso tenemos nombres famosos para estos problemas, como Overfetching y Underfetching, y ni siquiera hemos llegado al punto en el que estamos hablando del problema N+1.


En algún momento podríamos decidir dividir el procedimiento en un procedimiento raíz y varios subprocedimientos. ¿Qué pasa si estamos obteniendo una matriz en el nivel raíz y luego, para cada elemento de la matriz, tenemos que llamar a otro procedimiento para obtener más datos?


Otra posibilidad podría ser introducir argumentos a la versión inicial de nuestro procedimiento, por ejemplo, trpc.newsFeed.useQuery({withPopularBadges: true}) .


Esto funcionaría, pero parece que estamos empezando a reinventar las características de GraphQL.

Construyamos un clon de Facebook con GraphQL

Ahora, comparemos esto con GraphQL. GraphQL tiene el concepto de Fragmentos, que nos permite declarar los requisitos de datos para cada componente. Los clientes como Relay le permiten declarar una sola consulta de GraphQL en la parte superior de la página e incluir fragmentos de los componentes secundarios en la consulta.


De esta manera, todavía estamos haciendo una búsqueda única en la parte superior de la página, pero el marco en realidad nos ayuda a declarar y recopilar los requisitos de datos para cada componente.

Veamos el mismo ejemplo usando GraphQL, Fragments y Relay. Por motivos de pereza, el código no es 100 % correcto porque estoy usando Copilot para escribirlo, pero debería parecerse mucho a cómo se vería en una aplicación real.


 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> ) }


A continuación, veamos el componente de la lista de fuentes. El componente de lista de noticias en tiempo real declara un fragmento para sí mismo e incluye el fragmento para el componente de elemento de noticias en tiempo real.


 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> ) }


Y, por último, el componente del elemento de alimentación:

 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> ) }


A continuación, creemos una variación de la fuente de noticias con insignias populares en los elementos de la fuente. Podemos reutilizar los mismos componentes, ya que podemos usar la directiva @include para incluir condicionalmente el popular fragmento de insignia.

 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> ) }


A continuación, veamos cómo se vería el elemento de la lista de fuentes actualizada:

 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> ) }


Como puede ver, GraphQL es bastante flexible y nos permite crear aplicaciones web complejas, incluidas variaciones de la misma página, sin tener que duplicar demasiado código.

Los fragmentos de GraphQL nos permiten declarar requisitos de datos a nivel de componente

Además, los fragmentos de GraphQL nos permiten declarar explícitamente los requisitos de datos para cada componente, que luego se elevan a la parte superior de la página y luego se recuperan en una sola solicitud.


GraphQL separa la implementación de la API de la obtención de datos

La gran experiencia de desarrollador de tRPC se logra fusionando dos preocupaciones muy diferentes en un solo concepto, implementación de API y consumo de datos.


Es importante entender que esto es una compensación. No hay almuerzo gratis. La simplicidad de tRPC tiene el costo de la flexibilidad.


Con GraphQL, tiene que invertir mucho más en el diseño de esquemas, pero esta inversión vale la pena en el momento en que tiene que escalar su aplicación a muchas páginas relacionadas.


Al separar la implementación de API de la obtención de datos, se vuelve mucho más fácil reutilizar la misma implementación de API para diferentes casos de uso.

El propósito de las API es separar la implementación interna de la interfaz externa

Hay otro aspecto importante a tener en cuenta al crear API. Es posible que esté comenzando con una API interna que usa exclusivamente su propia interfaz, y tRPC podría ser una excelente opción para este caso de uso.


Pero, ¿qué pasa con el futuro de su empresa? ¿Cuál es la probabilidad de que haga crecer su equipo? ¿Es posible que otros equipos, o incluso terceros, quieran consumir sus API?

Tanto REST como GraphQL se construyen teniendo en cuenta la colaboración. No todos los equipos usarán TypeScript, y si está cruzando los límites de la empresa, querrá exponer las API de una manera que sea fácil de entender y consumir.


Hay muchas herramientas para exponer y documentar las API REST y GraphQL, mientras que tRPC claramente no está diseñado para este caso de uso.


Entonces, si bien es genial comenzar con tRPC, es muy probable que lo superes en algún momento, lo cual creo que Theo también mencionó en uno de sus videos.


Ciertamente, es posible generar una especificación OpenAPI a partir de una API tRPC, las herramientas existen, pero si está construyendo un negocio que finalmente dependerá de la exposición de las API a terceros, sus RPC no podrán competir contra REST y REST bien diseñados. API de GraphQL.

Conclusión

Como dije al principio, soy un gran admirador de las ideas detrás de tRPC. Es un gran paso en la dirección correcta, ya que hace que la obtención de datos sea más sencilla y amigable para los desarrolladores.


GraphQL, Fragments y Relay, por otro lado, son herramientas poderosas que lo ayudan a crear aplicaciones web complejas. Al mismo tiempo, la configuración es bastante compleja y hay muchos conceptos que aprender hasta que lo domines.


Si bien tRPC lo ayuda a comenzar rápidamente, es muy probable que supere su arquitectura en algún momento. futuro. ¿Qué tan complejos serán los requisitos de obtención de datos? ¿Habrá varios equipos consumiendo sus API? ¿Expondrá sus API a terceros?


panorama

Dicho todo esto, ¿qué pasaría si pudiéramos combinar lo mejor de ambos mundos? ¿Cómo se vería un cliente API que combina la simplicidad de tRPC con el poder de GraphQL? ¿Podríamos crear un cliente API de TypeScript puro que nos brinde el poder de Fragments y Relay, combinado con la simplicidad de tRPC?


Imagine que tomamos las ideas de tRPC y las combinamos con lo que hemos aprendido de GraphQL y Relay.


Aquí hay un pequeño adelanto:


 // 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é piensas? ¿Usarías algo como esto? ¿Ve el valor de definir dependencias de datos a nivel de componente o prefiere seguir con la definición de procedimientos remotos a nivel de página? Me encantaría escuchar tus pensamientos...


Actualmente estamos en la fase de diseño para crear la mejor experiencia de obtención de datos para React, NextJS y todos los demás marcos. Si te interesa este tema, sígueme en Twitter para estar al día.


Si desea unirse a la discusión y discutir RFC con nosotros, siéntase libre de unirse a nuestro servidor Discord .

próximo