Tecnologia / Artigos / GraphQL /
GraphQL versus REST

Cléber

![graphql-logo.png](/files/119) Confesso que quando me disseram que o projeto em questão usava GraphQL eu torci um pouco o nariz. Pensando agora sobre isso, depois de ter estudado melhor tanto o protocolo em si quanto os argumentos contra e a favor, percebo que há uma explicação um pouco mais razoável do que meramente algum tipo de preconceito. Quem acompanhou a série ["Desenvolvedor fullstack: não seja esse cara"](https://medium.com/a-vida-de-cl%C3%A9ber/desenvolvedor-fullstack-n%C3%A3o-seja-esse-cara-aa2572aa9dc) deve lembrar do que penso a respeito da necessidade de um certo conflito de interesses entre backend a frontend e, de certa forma, GraphQL me parece uma resposta por parte das equipes de frontend às dificuldades que APIs REST ocasionam -- mesmo que o problema agora vá para o backend. Mas, como eu disse antes, essa "queda de braço" é normal e até desejável. É dessa discussão que soluções melhores continuarão surgindo. **APIs REST facilitam enormemente a vida do backend**. Faz-se uso correto do protocolo HTTP, é fácil fazer cache, *load balancing*, roteamento em geral, implementar *gateways*, et cetera. Mas isso tudo vem às custas de certas idiossincrasias que podem tornar a vida do frontend mais difícil -- se for necessário listar estruturas que se assemelhem de qualquer forma a **árvores**, tudo fica mais complicado, já que passa a ser necessário fazer diversas requisições para que se consiga ter em mãos dados com algum significado. Por outro lado, **GraphQL facilita enormemente a vida do frontend**: agora, ao invés de fazer N requisições em M endpoints diferentes, busca-se tudo o que é necessário, inclusive com controle sobre o formato dos dados retornados, em apenas 1 requisição. Entretanto, isso vem às custas de dificultar o trabalho do backend, que agora já tem muito mais dificuldades com cache, roteamento, *logging* e implementação de *gateways*, por exemplo. É possível que você agora esteja se perguntando: "*okay, e quem vai ganhar essa disputa?*", e eu responderei com a resposta *default* da indústria de T.I.: *depende*. Vamos analisar algumas vantagens e desvantagens de cada abordagem. # GraphQL O caso de uso mais interessante, a meu ver, para GraphQL é o das *mobile apps*, em que você tem um transporte com *overhead* alto (redes de internet trafegando em antenas de celular) e precisa exibir informações muito rapidamente para seus usuários (se demorar mais que **um segundo** você já perde a atenção deles), o que torna o envio de N ciclos *request/response* simplesmente proibitivo. Assim, se tua *app* pode enviar apenas uma requisição e já receber nela dados suficientes para popular uma tela inteira de uma vez só, isso certamente é uma grande vantagem. No fim do dia, o uso de uma API REST, nesse caso, torna-se realmente muito complicado, já que o maior problema é o *overhead* próprio da conexão do celular, o que faz com que não importe tanto o quão rápidas sejam as respostas da API em si. E não queremos iniciar uma guerra contra as leis da Física, certo? Todavia, essa flexibilidade toda do protocolo torna realmente complicado fugir da necessidade de presença de **algum** componente "onisciente" na arquitetura do backend. Se há um **monolito** cuidando do acesso aos dados, tudo está muito bem (ele já é onisciente por natureza). Mas se a arquitetura é de microsserviços, inevitavelmente criar-se-á algum tipo de "gateway" que seja capaz de sempre reunir todas as informações que as *queries* GraphQL necessitem. "*Mas as APIs REST também costumam fazer uso de gateways, não?*". Sim, o fazem, mas **esses** gateways são muito mais simples, já que trabalham somente com o protocolo base, que é o HTTP, e as implementações estão em evolução há muitos anos. Temos diversas ferramentas extremamente eficientes para lidar com HTTP. Além disso, os gateways REST podem se dar ao luxo de serem **burros**, sem necessidade de saber praticamente nada a respeito do que está sendo requisitado ou respondido. Repare: muito dificilmente a implementação do gateway REST é feita "na unha". Ele é, na esmagadora maioria dos casos, um componente que já vem pronto, seja um "API Gateway" (da AWS, por exemplo), um [Kong](https://konghq.com/kong/) ou mesmo o simples uso de um `nginx` como *proxy reverso*. Já no caso do "gateway" GraphQL, você provavelmente acabará implementando-o por conta própria, coisa que, mesmo não sendo uma garantia de inferioridade, certamente coloca sua equipe em óbvia **desvantagem numérica**. Tenho visto arquiteturas **bizarras** surgindo devido a essa necessidade de um componente *onisciente*, como microsserviços que usam um mesmo banco de dados, "embaixo", e um gateway GraphQL "em cima" -- o que leva à pergunta óbvia: *por que diabos implementamos "microsserviços"?* Na prática, trata-se de um "monolito desconstruído", e quem já viu alguma temporada de "MasterChef" sabe que "desconstruído", muitas vezes, é sinônimo de "deu tudo errado mas não quero admitir" (esqueceu de enfiar o recheio dentro da massa, assou tudo e agora entrega um "*empadão desconstruído*"). E a "comunidade GraphQL" **parece incentivar** esse tipo de coisa, ignorando os problemas óbvios! Pelo que entendo, **o *gateway* é uma infelicidade** e só deve existir se sua presença for absolutamente imprescindível. A lista a seguir deve cobrir uma porcentagem significativa dos casos em que tal componente se faz necessário: 1. Os microsserviços tem cada um seu próprio banco de dados; 2. *(Não tem número 2)*. ## QL = query language Não é à toa que trago a ideia de um bom e velho monolito novamente à tona. Parece-me que GraphQL tende a tentar os desenvolvedores a degradar o protocolo em um sistema de **RPC**, quando a ideia absolutamente não é essa. QL, no fim do dia, ainda é uma "*query language*", e ainda prevalece a noção de que tudo é sobre **dados, não "operações"**. (Se sua implementação de GraphQL está infestada de "*get-isso*", "*set-aquilo*", é sinal que você **já degradou** o protoclo.) Nas arquiteturas em que se trabalha tanto com dados quanto com operações, geralmente o monolito é responsável por prover o acesso aos dados, numa API coerente, e as operações se dão por alguma forma de sistema RPC. Eu realmente faço questão de bater nessa tecla, porque mesmo nas APIs REST essa discussão tão importante acaba não sendo feita e acaba-se por criar APIs que **também** contém algum tipo de "operação" -- e geralmente é na parte da qual não se tem muito pra onde fugir: a autenticação. A abordagem certa para autenticação não é imaginar, do lado do cliente, que "eu quero fazer o login", pois isso é uma abordagem de *operação*. "Fazer o login" é uma operação, e o desenvolvedor, se pensar assim, estará olhando pelo viés errado, já que APIs REST são "tudo sobre dados". O jeito certo de pensar é "eu quero receber um token de autenticação". Isso faz com que o endpoint, que antes seria algo como `/login` (flagrantemente problemático), torne-se `/auth-token` (no caso de APIs que não implementem OAuth2, evidentemente). Em detalhes: ```plaintext POST /login {"username": ᐸusernameᐳ, "password": ᐸpasswordᐳ} ``` A requisição acima (problemática), que já começa errando por ser um POST, torna-se o que vem a seguir, um GET: ```plaintext GET /token x-username: ᐸusernameᐳ x-password: ᐸpasswordᐳ ``` ## Fácil para um, complicado para outro Repare em como facilitar a vida do frontend (para poder atender requisitos de usabilidade, claro -- não estou dizendo que trate-se de mero capricho) acaba gerando uma série de discussões sobre arquitetura e implementação no backend. E isso absolutamente **não é uma reclamação** da minha parte, mas um chamado para que as equipes de desenvolvimento atentem-se a esse fato inevitável e entendam que, havendo mudança de protocolo de tratamento de dados, necessariamente deve haver também uma re-análise do planejamento da arquitetura, seja qual for o veredito final (pode ser que não seja necessário mudar nada, mas é bom que chegue-se a essa conclusão **fazendo-se** a análise e não como um chute). # REST Uma API REST bem feita deixa o desenvolvedor backend feliz: tudo é muito simples, o gateway é um componente burro, cada endpoint tem um escopo absolutamente bem definido, é simples definir as regras de acesso e, na maioria dos casos, o código dos endpoints é tão simples que uma implementação *default* possivelmente cobre pelo menos metade de todos os casos. Já para o desenvolvedor frontend isso pode tornar-se um pesadelo, e se usando GraphQL o backend precisava passar por uma re-análise da arquitetura, usando-se REST é o frontend que precisa passar por processo similar. Uma das dificudades comuns é a busca de dados em estruturas similares a árvores, coisa que aplica-se a praticamente qualquer coisa não-trivial. Ao exibir-se uma *timeline* com *posts* de diversos autores em diversos grupos o frontend precisará decidir se busca primeiro os posts e depois, para cada um, busca dados sobre o autor e o grupo (tendo que cuidar, no meio do caminho, de salvar em *cache* os valores, para não fazer requisições desnecessárias) ou se busca de antemão todos os autores e todos os grupos, operação que pode ser rápida ou extremamente demorada -- e impraticável. Percebe-se, assim, que agora é a equipe de frontend que precisa tomar decisões complicadas e que, no fim das contas, acabam resumindo-se a "*escolhamos o que for menos ruim*". E não é algo desejável termos equipes precisando decidir pelo que é "menos ruim", certo? Todavia, é bom lembrar que nem sempre o consumo de dados se dá com objetivo de apresentar algo imediatamente a um ser humano. Há uma porção de casos em que é mais importante para a API prover dados de maneira simples e acessível a qualquer ferramenta (na linha de comando você pode consumir APIs REST com `wget`, `curl`, `httpie` e várias outras) ou qualquer plataforma (um firmware rodando num microcontrolador pode fazer proveito de bibliotecas HTTP para acessar APIs REST com extrema facilidade) do que prover flexibilidade de formato ou praticidade nas consultas. Nesses casos, uma API REST torna-se a escolha óbvia, já que entrega tudo o que o consumidor final precisa enquanto não exige que o backend abra mão de nenhuma benesse. # Como escolher Se REST é extremamente simples de ser implementado pelo backend e GraphQL torna a vida do frontend extremamente simples, a decisão entre uma coisa e outra acaba não sendo tão difícil: * Se o consumidor da API usará os dados como **apresentação** para um ser humano, use GraphQL; * (Se a API será consumida por **outros meios que não HTTP**, REST provavelmente não é uma boa ideia, mesmo!) * Doutra maneira, use REST. # GraphQL e microsserviços Fico simplesmente boquiaberto quando vejo artigos tentando convencer as pessoas que GraphQL casa muito bem com o uso de microsserviços quando é evidente que isso é **um absurdo**. Implementar um gateway que não seja um componente burro simplesmente jogará no lixo qualquer benefício de uma arquitetura de microsserviços, deixando sua equipe apenas com as partes ruins -- **que não são poucas**. (É válido, também, fazer um apelo: é bem provável que sua solução **não precise** de microsserviços. Não ceda à pressão do "glamour": microsserviços tem o potencial de gerar mais **problemas** do que soluções reais. Por favor, faça essa análise com muita parcimônia, de preferência contando com o auxílio de um Arquiteto de Software experiente.) É bem verdade que um gateway pode permitir que os diversos componentes falem cada um usando o protocolo que preferirem, mas também é verdade que ter um "componente poliglota" meramente copia toda a feiura da arquitetura em si (que pode até ser inevitável) para dentro de seu *design* -- imagine um componente que fale GraphQL, REST e SOAP ao mesmo tempo... ## Gateway-monolito Se sua solução tem microsserviços "puros" em que cada um tem seu próprio banco de dados e você deseja prover uma API GraphQL, não tem muito para onde correr, e você precisará, sim, de um gateway. ![graphql-microsservices.png](/files/129) Mas nem todos sequem esse caminho, e é razoavelmente comum encontrar arquiteturas em que os microsseviços compartilham de um mesmo banco de dados relacional, de forma a aproveitar os benefícios de ambos os mundos: os dados mantém-se íntegros, mas a implementação dos componentes pode ser feita da maneira que se julgue mais adequada e com uma margem de escalabilidade individual ainda razoável. Nesses casos, a dificuldade é harmonizar a presença de diversos componentes com a presença de um "componente unificador". ![graphql-gateway.png](/files/125) A sugestão mais simples é que busque-se sempre implementar acesso a dados em um componente gateway/monolito, que ao invés de simplesmente fazer uso de outros serviços para ter acesso a dados, implementa ele próprio a grande maioria desses acessos (monolito), mas mantém a capacidade de complementar certas *queries* usando serviços externos (gateway). Dessa forma, ganha-se todos os **benefícios** de um monolito (base de código comum, compartilhamento de componentes de forma muito simples, testes automatizados envolvendo diversos recursos sem precisar de comunicação externa, eliminação de "contratos") enquanto praticamente se **empata** na comparação de "malefícios" -- escalar "*todo mundo junto*" é ruim no monolito, mas seria praticamente a mesma coisa escalar "*todo mundo junto*" no gateway. ![graphql-monolito.png](/files/126) A maioria das aplicações (eu diria) tem muito mais recursos que são mero CRUD do que recursos realmente especializados, e nesses casos a abordagem gateway/monolito apresenta vantagens claras. Mas esse não é o caso de todos e, às vezes, uma abordagem um pouco mais complexa pode trazer mais benefícios, como veremos a seguir. ## WAMP Esse projeto que comentei no começo desse artigo tem uma característica interessante, que é que, além do acesso aos dados via HTTP/GraphQL, a aplicação também faz uso de um **WebSocket**. Repare que na abordagem gateway-monolito a ideia principal acaba sendo "aplanar" a posição conceitual dos componentes e provedores de dados de recursos, de maneira que o papel de "gateway inteligente", visto como negativo, seja minimizado em favor do papel de monolito, que traz mais benefícios sem trazer nenhum malefício considerável. Mas se a aplicação já mantém-se conectada a um WebSocket, pode ser interessante simplesmente **eliminar o uso de HTTP** e partir-se para um transporte alternativo, como [WAMP](https://wamp-proto.org/). A primeira vantagem óbvia é que isso reduz o número de canais em que cada componente precisa conversar. A *mobile app*, por exemplo, que antes falava HTTP **e** WebSocket, agora comunica-se somente pelo WebSocket. E a segunda é que implementar um serviço de WebSocket confiável, seguro, estável e escalável não é tarefa trivial, e agora pode-se passar essa responsabilidade para uma solução *open source* já com muitos anos e muita contribuição externa (e uma linguagem de comunicação "no fio" padronizada ao invés dos idiomas tupiniquins que tendemos a inventar). As requisições HTTP, que eram *request/response*, passam a ser feitas via RPC, basicamente partindo-se para um conceito de *call/return*. Ou seja: do ponto de vista dos clientes, pouca coisa muda além do transporte em si. E com a vantagem que as bibliotecas que implementam clientes WAMP erguem exceções/erros significativos e de maneira padronizada, o que também facilita a vida dos desenvolvedores que antes precisavam lidar com *status codes HTTP* mais análise dos corpos de respostas de erro, **quando existiam** (erros 5xx geralmente não tem muito o que se aproveitar). ![graphql-wamp.png](/files/131) O fato de agora trabalharmos com **sessões autenticadas** elimina a necessidade de averiguar a cada requisição se o token de acesso do usuário ou a assinatura do JWT são válidos. A autenticação passa a acontecer durante o processo de conexão e, a partir daí, pode-se simplesmente confiar na autenticidade da sessão. Não apenas fazemos menos operações como os componentes tendem a tornar-se mais simples. A possibilidade de múltiplos registros de métodos RPC e chamada via *fan out* permite implementar estratégias de *alta disponibilidade* e *load balancing* com facilidade. O protocolo WAMP provê não somente um sistema de RPC, mas também um sistema de **pub/sub**. Dessa forma, o frontend pode ser notificado facilmente sobre determinados eventos, desde "um novo usuário registrou-se" (coisa que pode ser interessante mostrar em tela) quanto "o grupo X foi excluído" (coisa que pode ser interessante fazer refletir imediatamente em tela, eliminado tal grupo de eventuais listagens ou obrigando o usuário a retornar para outra tela caso estivesse vendo justamente detalhes sobre ele). Em resumo, o fato de GraphQL ser um protocolo sem acoplamento forte ao HTTP (ao contrário de REST) torna-o versátil o bastante ao ponto de ser uma adição muito interessante ao WAMP, sendo a transição simples para os clientes (que já tem as *queries* prontas, bastando alterar o transporte) e para os componentes (que também já tem os *schemas* prontos) -- inclusive podendo ser feita de maneira gradual, sem precisar de uma "virada de chave" repentina. # Resumo * REST favorece bastante o **backend**; * GraphQL favorece bastante o **frontend** (e a experiência do usuário, geralmente); * GraphQL sobre **WAMP** pode ser uma combinação interessante.

Curti

47 visitantes curtiram esse Item.

Anterior: Artigos / XFCE: quase como o Lada | Próximo: Um sistema de erros para o GraphQL