paint-brush
A simplicidade do tRPC com o poder do GraphQLpor@wunderstef
6,553 leituras
6,553 leituras

A simplicidade do tRPC com o poder do GraphQL

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

Muito longo; Para ler

featured image - A simplicidade do tRPC com o poder do GraphQL
Stefan Avram  HackerNoon profile picture
0-item


Sou um grande fã do tRPC. A ideia de exportar tipos do servidor e importá-los no cliente para ter um contrato type-safe entre ambos, mesmo sem uma etapa de tempo de compilação, é simplesmente genial. Para todas as pessoas envolvidas no tRPC, vocês estão fazendo um trabalho incrível.


Dito isso, quando vejo comparações entre tRPC e GraphQL, parece que estamos comparando maçãs e laranjas.


Isso se torna especialmente aparente quando você olha para o discurso público sobre GraphQL e tRPC. Veja este diagrama de theo , por exemplo:

Theo explicou esse diagrama em profundidade e, à primeira vista, faz muito sentido. O tRPC não requer uma etapa de tempo de compilação, a experiência do desenvolvedor é incrível e é muito mais simples que o GraphQL.


Mas esse é realmente o quadro completo ou essa simplicidade é alcançada à custa de outra coisa? Vamos descobrir criando um aplicativo simples com tRPC e GraphQL.

Vamos construir um clone do Facebook com tRPC

Vamos imaginar uma árvore de arquivos com uma página para o feed de notícias, um componente para a lista de feeds e um componente para o item do feed.


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


No topo da página do feed, precisamos de algumas informações sobre o usuário, notificações, mensagens não lidas, etc.


Ao renderizar a lista de feeds, precisamos saber o número de itens de feed, se há outra página e como buscá-la.


Para o item do feed, precisamos saber o autor, o conteúdo, o número de curtidas e se o usuário gostou.


Se fôssemos usar o tRPC, criaríamos um "procedimento" para carregar todos esses dados de uma só vez. Chamaríamos esse procedimento na parte superior da página e depois propagaríamos os dados para os componentes.


Nosso componente de feed ficaria mais ou menos assim:

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


Em seguida, vamos ver o componente da 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> ) }


E, finalmente, o componente do item de feed:

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


Lembre-se de que ainda somos uma única equipe, é tudo TypeScript, uma única base de código e ainda estamos usando tRPC.


Vamos descobrir quais dados realmente precisamos para renderizar a página. Precisamos do usuário, das mensagens não lidas, das notificações, dos itens do feed, do número de itens do feed, da próxima página, do autor, do conteúdo, do número de curtidas e se o usuário gostou.


Onde podemos encontrar informações detalhadas sobre tudo isso? Para entender os requisitos de dados para o avatar, precisamos examinar o componente Avatar . Existem componentes para mensagens e notificações não lidas, portanto, precisamos examiná-los também. O componente da lista de feeds precisa do número de itens, da próxima página e dos itens do feed. O componente de item de feed contém os requisitos para cada item de lista.


No total, se quisermos entender os requisitos de dados para esta página, precisamos examinar 6 componentes diferentes. Ao mesmo tempo, não sabemos realmente quais dados são realmente necessários para cada componente. Não há como cada componente declarar quais dados ele precisa, pois o tRPC não possui esse conceito.


Tenha em mente que esta é apenas uma única página. O que acontece se adicionarmos páginas semelhantes, mas ligeiramente diferentes?


Digamos que estamos criando uma variante do feed de notícias, mas em vez de mostrar as postagens mais recentes, mostramos as postagens mais populares.


Poderíamos usar mais ou menos os mesmos componentes, com apenas algumas mudanças. Digamos que postagens populares tenham selos especiais que requerem dados extras.


Devemos criar um novo procedimento para isso? Ou talvez possamos apenas adicionar mais alguns campos ao procedimento existente?


Essa abordagem é bem dimensionada se adicionarmos mais e mais páginas? Isso não soa como o problema que tivemos com as APIs REST? Temos até nomes famosos para esses problemas, como Overfetching e Underfetching, e ainda nem chegamos ao ponto de falar sobre o problema N+1.


Em algum momento, podemos decidir dividir o procedimento em um procedimento raiz e vários subprocedimentos. E se estivermos buscando um array no nível raiz e, para cada item do array, tivermos que chamar outro procedimento para buscar mais dados?


Outra possibilidade seria introduzir argumentos na versão inicial de nosso procedimento, por exemplo, trpc.newsFeed.useQuery({withPopularBadges: true}) .


Isso funcionaria, mas parece que estamos começando a reinventar os recursos do GraphQL.

Vamos construir um clone do Facebook com GraphQL

Agora, vamos comparar isso com o GraphQL. O GraphQL possui o conceito de Fragments, que nos permite declarar os requisitos de dados para cada componente. Clientes como Relay permitem que você declare uma única consulta GraphQL na parte superior da página e inclua fragmentos dos componentes filhos na consulta.


Dessa forma, ainda estamos fazendo uma única busca na parte superior da página, mas a estrutura realmente nos ajuda a declarar e reunir os requisitos de dados para cada componente.

Vejamos o mesmo exemplo usando GraphQL, Fragments e Relay. Por motivos de preguiça, o código não está 100% correto porque estou usando o Copilot para escrevê-lo, mas deve ficar bem próximo de como ficaria em um aplicativo 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 seguir, veremos o componente da lista de feeds. O componente de lista de feeds declara um fragmento para si mesmo e inclui o fragmento para o componente de item de feed.


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


E, finalmente, o componente do item de feed:

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


Em seguida, vamos criar uma variação do feed de notícias com selos populares nos itens do feed. Podemos reutilizar os mesmos componentes, pois podemos usar a diretiva @include para incluir condicionalmente o popular fragmento de emblema.

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


Em seguida, vamos ver como o item da lista de feeds atualizado pode ficar:

 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 você pode ver, o GraphQL é bastante flexível e permite construir aplicações web complexas, incluindo variações da mesma página, sem ter que duplicar muito código.

Fragmentos GraphQL nos permitem declarar requisitos de dados no nível do componente

Além disso, os fragmentos do GraphQL nos permitem declarar explicitamente os requisitos de dados para cada componente, que são elevados ao topo da página e, em seguida, buscados em uma única solicitação.


GraphQL separa a implementação da API da busca de dados

A grande experiência do desenvolvedor do tRPC é alcançada pela fusão de duas preocupações muito diferentes em um conceito, implementação de API e consumo de dados.


É importante entender que isso é uma troca. Não existe almoço grátis. A simplicidade do tRPC vem com o custo da flexibilidade.


Com o GraphQL, você precisa investir muito mais no design do esquema, mas esse investimento compensa no momento em que você precisa dimensionar seu aplicativo para muitas páginas relacionadas.


Ao separar a implementação da API da busca de dados, torna-se muito mais fácil reutilizar a mesma implementação da API para diferentes casos de uso.

O objetivo das APIs é separar a implementação interna da interface externa

Há outro aspecto importante a ser considerado ao criar APIs. Você pode estar começando com uma API interna que é usada exclusivamente por seu próprio front-end, e o tRPC pode ser uma ótima opção para esse caso de uso.


Mas e o futuro do seu empreendimento? Qual é a probabilidade de você aumentar sua equipe? É possível que outras equipes, ou mesmo terceiros, queiram consumir suas APIs?

Ambos REST e GraphQL são construídos com a colaboração em mente. Nem todas as equipes usarão TypeScript e, se você estiver ultrapassando os limites da empresa, convém expor as APIs de uma maneira fácil de entender e consumir.


Há muitas ferramentas para expor e documentar as APIs REST e GraphQL, enquanto o tRPC claramente não foi projetado para esse caso de uso.


Portanto, embora seja ótimo começar com tRPC, é muito provável que você o supere em algum momento, o que acho que Theo também mencionou em um de seus vídeos.


Certamente é possível gerar uma especificação OpenAPI a partir de uma API tRPC, as ferramentas existem, mas se você estiver construindo um negócio que acabará contando com a exposição de APIs a terceiros, seus RPCs não poderão competir com REST bem projetado e APIs do GraphQL.

Conclusão

Como dito no início, sou um grande fã das ideias por trás do tRPC. É um grande passo na direção certa, tornando a busca de dados mais simples e mais amigável ao desenvolvedor.


GraphQL, Fragments e Relay, por outro lado, são ferramentas poderosas que ajudam a criar aplicativos da Web complexos. Ao mesmo tempo, a configuração é bastante complexa e há muitos conceitos para aprender até você pegar o jeito.


Embora o tRPC o ajude a começar rapidamente, é muito provável que você supere sua arquitetura em algum momento. futuro. Quão complexos serão os requisitos de busca de dados? Haverá várias equipes consumindo suas APIs? Você exporá suas APIs a terceiros?


Panorama

Com tudo isso dito, e se pudéssemos combinar o melhor dos dois mundos? Como seria um cliente de API que combinasse a simplicidade do tRPC com o poder do GraphQL? Poderíamos construir um cliente TypeScript API puro que nos desse o poder de Fragments e Relay, combinado com a simplicidade do tRPC?


Imagine que pegamos as ideias do tRPC e as combinamos com o que aprendemos com GraphQL e Relay.


Aqui está uma pequena prévia:


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


O que você acha? Você usaria algo assim? Você vê o valor na definição de dependências de dados no nível do componente ou prefere definir procedimentos remotos no nível da página? Eu adoraria ouvir seus pensamentos...


Atualmente, estamos na fase de design para criar a melhor experiência de busca de dados para React, NextJS e todos os outros frameworks. Se você estiver interessado neste tópico, siga-me no Twitter para se manter atualizado.


Se você gostaria de participar da discussão e discutir RFCs conosco, sinta-se à vontade para entrar em nosso servidor Discord .

Próximo