Pular para o conteúdo
17 min de leitura

REST vs GraphQL: qual estilo de API escolher e por quê

Por Lucas Andrade ·

REST e GraphQL resolvem o mesmo problema por caminhos opostos; um comparativo sem hype sobre over-fetching, cache, complexidade e quando cada um ganha.

Poucas discussões de arquitetura geram tanto ruído quanto REST contra GraphQL. Times entram na conversa já com um veredito — "GraphQL é o futuro" ou "REST resolve 95% dos casos e o resto é modismo" — e a decisão real, que depende do formato dos dados, do número de clientes e de quem paga o custo operacional, fica em segundo plano. Nenhum dos dois é sucessor do outro: são dois estilos com trade-offs diferentes, criados para resolver problemas diferentes, e continuam vivos porque nenhum eliminou o motivo de existir do outro.

O objetivo aqui é entender o que cada estilo realmente é — não a superfície de sintaxe, mas o modelo mental por trás — e comparar os custos concretos de escolher um ou outro: o que se ganha, o que se paga, e em que contexto cada ganho compensa o preço. Não é um tutorial de como escrever uma query ou desenhar uma rota; é o raciocínio que deveria vir antes de escrever qualquer uma das duas.

O que REST realmente é

REST não é "JSON sobre HTTP". Essa é a confusão mais comum e a que mais empobrece a discussão. REST — Representational State Transfer — é um estilo arquitetural definido por um conjunto de restrições: recursos identificados por URLs, manipulados através de um conjunto uniforme de verbos HTTP, onde cada resposta é uma representação do estado daquele recurso em determinado momento. Uma API pode devolver XML, HTML ou até texto puro e ainda ser REST; uma API que devolve JSON mas ignora verbos, códigos de status e a ideia de recurso não é REST só porque o corpo é JSON. O formato de serialização nunca foi o ponto.

Recursos, não ações. No modelo REST, o substantivo da URL é um recurso — /pedidos, /usuarios/42, /pedidos/42/itens — e o verbo HTTP expressa a ação sobre ele: GET lê, POST cria, PUT/PATCH atualizam, DELETE remove. Isso é uma escolha deliberada de desenhar a API em torno do o quê (o recurso) e deixar o como (a operação) para um vocabulário pequeno e já conhecido por qualquer cliente HTTP. Uma URL como /criarPedido quebra essa convenção — é uma chamada de procedimento remoto disfarçada de REST, e junto com ela se perdem os benefícios que o estilo entrega de graça.

Statelessness é a restrição menos visível e mais importante. Cada requisição precisa carregar tudo que o servidor precisa para processá-la — autenticação, contexto, parâmetros — sem depender de um estado de conversa guardado no servidor entre uma chamada e outra. Isso não significa que o sistema como um todo não tenha estado (o banco de dados tem, e muito); significa que o servidor não guarda "em que passo da conversa" um cliente específico está. A consequência prática é enorme: qualquer requisição pode ser atendida por qualquer instância do servidor, o que torna escalar horizontalmente trivial e torna cache — tanto de cliente quanto de proxies intermediários — possível, porque a mesma requisição sempre significa a mesma coisa.

Os códigos de status carregam significado, não são decoração. 200 é sucesso, 201 é criação bem-sucedida, 404 é recurso inexistente, 409 é conflito, 422 é entidade que não passou validação, 401 é falta de identidade, 403 é identidade sem permissão. Um cliente HTTP genérico — um proxy, uma CDN, um navegador — entende esse vocabulário sem saber nada sobre o domínio da aplicação, e é exatamente essa generalidade que permite que uma infraestrutura inteira de cache, retry e observabilidade funcione sem código específico da API.

O que GraphQL realmente é

GraphQL não é um banco de dados nem um protocolo de rede — é uma linguagem de consulta com um schema tipado por trás. A API expõe um único endpoint (tipicamente /graphql), e sobre esse endpoint roda uma linguagem própria em que o cliente descreve exatamente a forma dos dados que quer: quais campos, de quais entidades, seguindo quais relações. O servidor expõe um schema — um contrato explícito de todos os tipos, campos e relações disponíveis — e a query do cliente é validada contra esse schema antes de ser executada. Se o campo pedido não existe no schema, o erro aparece na validação, não depois de uma tentativa de acesso a um atributo inexistente.

A inversão de controle é o ponto central. Numa API REST tradicional, o servidor decide a forma da resposta: o endpoint /usuarios/42 devolve um objeto fixo, com os campos que o desenhista da API escolheu incluir. Numa API GraphQL, o cliente decide a forma da resposta dentro do universo permitido pelo schema — ele pode pedir só o nome e o e-mail do usuário, ou pedir o nome, os últimos cinco pedidos e o endereço de cada pedido, tudo numa query só. O servidor deixou de decidir "o que vai na resposta" e passou a decidir "o que é possível pedir".

``graphql query { usuario(id: "42") { nome email pedidos(ultimos: 5) { id total itens { produto { nome } quantidade } } } } ``

Essa query única, endereçada a um único endpoint, atravessa três níveis de relação — usuário, pedidos, itens de cada pedido — e volta com exatamente os campos pedidos, nem um a mais.

O problema que GraphQL nasceu para resolver

Over-fetching é receber campos que ninguém vai usar. Um endpoint REST /usuarios/42 desenhado para atender a tela de perfil completo devolve nome, e-mail, telefone, endereço, preferências, data de criação da conta e mais uma dezena de campos — porque algum cliente da API precisa de todos eles. Mas o app mobile que só quer mostrar o nome no cabeçalho da tela recebe o objeto inteiro mesmo assim, porque o endpoint não sabe distinguir quem está perguntando. Em conexões móveis com banda limitada, ou em listas com centenas de itens onde cada objeto carrega dez vezes mais dado do que a UI usa, esse excesso não é cosmético — é latência e consumo de dados real.

Under-fetching é o oposto: o endpoint devolve de menos, e o cliente precisa complementar com mais chamadas. O caso clássico é o problema N+1 do lado do cliente: buscar a lista de pedidos em GET /pedidos devolve os pedidos, mas não os dados do produto de cada item — então o cliente precisa disparar uma chamada adicional a GET /produtos/{id} para cada item de cada pedido, ou aceitar uma segunda viagem de rede para uma versão "expandida" do endpoint. Numa lista de vinte pedidos com três itens cada, isso é potencialmente sessenta chamadas HTTP extras, cada uma com seu próprio round-trip, só para montar uma tela.

A saída tradicional dentro de REST é multiplicar endpoints por caso de uso — um /usuarios/42?fields=nome,email com seleção de campos, um /pedidos?expand=itens.produto com embutimento de relações, um /mobile/usuarios/42 inteiro dedicado ao cliente mobile. Funciona, mas empurra a explosão de combinações para o servidor: cada novo cliente com uma necessidade de dados ligeiramente diferente pede um parâmetro novo, um endpoint novo, ou aceita pagar over-fetching. GraphQL resolve isso invertendo quem decide a forma: em vez do servidor prever todas as combinações possíveis de campos que algum cliente algum dia vai querer, o cliente pede exatamente a combinação que precisa, numa query só, e o servidor resolve os relacionamentos internamente antes de devolver.

O que GraphQL custa

Cache HTTP deixa de funcionar de graça. Esse é o custo mais subestimado por quem chega vindo de REST. Cache de borda, CDN e cache de proxy intermediário funcionam porque a URL identifica o recurso e o verbo GET é seguro e idempotente — uma CDN sabe que GET /pedidos/42 sempre significa a mesma coisa e pode guardar a resposta. GraphQL tipicamente expõe um único endpoint e usa POST para tudo, porque a query em si — potencialmente grande — vai no corpo da requisição, não na URL. Isso tira do caminho toda a infraestrutura de cache HTTP que existe desde sempre: não tem URL distinta para memorizar, não tem verbo seguro para assumir "não muda nada". O ganho tem que ser reconstruído manualmente, com uma camada de cache no cliente (normalizando entidades por ID, como o Apollo Client ou o Relay fazem) ou com queries persistidas — enviar um hash pré-registrado da query em vez do texto completo, o que devolve alguma cacheabilidade a nível de CDN, à custa de mais uma peça de infraestrutura para manter.

O N+1 não desaparece — ele migra para o servidor. Resolver uma query GraphQL que atravessa relações significa que, para cada usuário retornado numa lista, o resolver de "pedidos" é chamado individualmente; para cada pedido, o resolver de "itens" é chamado individualmente. Sem cuidado, uma query aparentemente inocente dispara uma avalanche de leituras individuais no banco — o mesmo problema N+1 que existia do lado do cliente em REST, reaparecendo do lado do servidor em GraphQL. A correção padrão é o DataLoader: uma camada que agrupa (batching) chamadas equivalentes feitas dentro do mesmo ciclo de execução e as resolve numa única consulta em lote, além de fazer cache por requisição para não repetir a mesma busca duas vezes. Implementar isso corretamente para cada relação do schema é trabalho real de engenharia, não uma opção de configuração.

Um único endpoint aceitando qualquer combinação de campos é superfície de ataque. Como o cliente controla a forma da query, nada impede — sem defesas explícitas — uma query profundamente aninhada (usuário → pedidos → itens → produto → categoria → produtos da categoria → pedidos desses produtos…) que multiplica o custo de execução exponencialmente a cada nível, ou uma query que pede o mesmo campo caro centenas de vezes com aliases diferentes. Isso exige um conjunto de defesas que não têm equivalente simples em REST: limite de profundidade (depth limiting), análise de complexidade (atribuir um custo a cada campo e recusar queries acima de um teto), limite de taxa por custo de query em vez de por número de requisições, e, em produção séria, queries persistidas — restringir o endpoint a um conjunto pré-aprovado de queries, eliminando a superfície arbitrária depois que o cliente está em produção.

O contrato tipado tem manutenção própria. O schema é uma peça de infraestrutura que precisa evoluir com disciplina — depreciar campos antes de removê-los, versionar sem quebrar clientes antigos que ainda fazem queries velhas contra um schema novo. É um ganho de explicitação sobre REST, onde o contrato muitas vezes vive só em documentação separada do código, mas ainda assim é superfície nova para manter e testar.

Onde REST brilha

Simplicidade e previsibilidade. Um desenvolvedor que nunca viu a API ainda reconhece o padrão: GET para ler, POST para criar, códigos de status com significado universal. Isso reduz a curva de entrada para quem consome a API e para quem constrói o servidor — não precisa aprender uma linguagem de consulta nova nem montar uma camada de resolvers.

Cache de borda funciona sem esforço extra. Uma API pública de conteúdo majoritariamente estável — um catálogo de produtos, um blog, uma listagem de posts — se beneficia enormemente de CDN na frente devolvendo respostas cacheadas por segundos ou minutos sem tocar o servidor de origem. Isso é ganho arquitetural embutido no estilo, não algo que precisa ser construído.

O ecossistema de ferramentas é maduro e universal. Qualquer cliente HTTP, qualquer proxy, qualquer ferramenta de observabilidade já entende REST nativamente — não exige biblioteca cliente específica, parser de schema ou ferramenta de introspecção. Para uma API pública consumida por terceiros desconhecidos, isso importa: reduz o atrito de quem integra pela primeira vez.

Recursos estáveis com formato previsível. Quando o formato dos dados de resposta não varia muito entre clientes — a maioria das APIs internas simples, a maioria das APIs públicas com poucos tipos de consumidor — o over-fetching é irrelevante na prática, porque o corpo da resposta já é do tamanho que o cliente típico precisa. Resolver um problema que não existe no seu contexto é custo sem retorno.

Onde GraphQL brilha

Clientes com necessidades de dados muito diferentes entre si. Um app mobile, um painel administrativo web e um widget embutido de terceiros, todos consumindo a mesma base de dados, mas cada um precisando de um subconjunto de campos bem diferente — esse é o cenário em que deixar cada cliente pedir exatamente o que precisa evita tanto o over-fetching do mobile quanto a multiplicação de endpoints dedicados no servidor.

Agregação de múltiplas fontes atrás de um BFF (Backend For Frontend). Quando os dados que uma tela precisa vêm de vários serviços internos — um serviço de usuários, um de pedidos, um de catálogo — GraphQL funciona bem como camada de agregação: uma query do cliente vira, do lado do servidor, várias chamadas internas orquestradas pelos resolvers, e o cliente recebe tudo já montado numa resposta só, sem precisar saber que os dados vieram de três serviços diferentes.

Frontends que evoluem rápido e iteram no que a tela mostra. Quando o time de frontend muda o design de uma tela e passa a precisar de dois campos novos e deixa de usar três antigos, com GraphQL isso é uma mudança de query no cliente, sem esperar o time de backend publicar um endpoint novo ou uma versão nova de um endpoint existente — desde que os campos já existam no schema. Isso desacopla a velocidade de iteração de UI da velocidade de deploy do backend.

Comparando eixo por eixo

Cache. REST aproveita cache HTTP nativo (CDN, proxy, navegador) porque URL e verbo carregam o significado. GraphQL precisa reconstruir cache manualmente — normalização no cliente ou queries persistidas — porque tudo passa por um único endpoint via POST.

Versionamento. REST tipicamente versiona pela URL (/v1/pedidos, /v2/pedidos) quando uma mudança quebra compatibilidade, o que significa manter múltiplas versões vivas em paralelo. GraphQL evita versionar o endpoint inteiro e evolui o schema in-place, adicionando campos novos livremente e depreciando os antigos com a diretiva @deprecated antes de remover — uma abordagem mais granular, mas que exige disciplina para não acumular dívida de campos "depreciados há dois anos e nunca removidos".

Tipagem. REST não tem tipagem nativa no protocolo — o contrato vive em documentação separada (OpenAPI/Swagger, quando existe e está atualizada) e nada impede a resposta real de divergir dela silenciosamente. GraphQL tem o schema como parte do próprio protocolo: o cliente pode introspectar o schema em tempo real, gerar tipos automaticamente, e o servidor recusa uma query que peça um campo inexistente antes mesmo de tentar executá-la.

Erros. REST expressa erro através do código de status HTTP — 404, 422, 500 — que ferramentas genéricas de rede já sabem interpretar. GraphQL devolve 200 quase sempre, mesmo quando parte da query falhou, e reporta problemas dentro de um campo errors no corpo da resposta, ao lado dos dados que tiveram sucesso — o que é poderoso para respostas parciais (alguns campos resolvidos, outros não), mas quebra a expectativa de qualquer ferramenta que trate status HTTP como fonte de verdade sobre sucesso ou falha.

Tooling e observabilidade. REST se apoia em uma geração inteira de ferramentas genéricas — API gateways, WAFs, ferramentas de APM — que já entendem o modelo de recurso e verbo sem configuração extra. GraphQL exige ferramentas cientes do protocolo para obter o mesmo nível de observabilidade: saber qual parte de uma query custou o quê dentro de uma única requisição POST exige instrumentação própria do resolver, não vem de graça do log de acesso HTTP.

O falso dilema

A escolha raramente é binária num sistema real. Muitas arquiteturas usam os dois ao mesmo tempo, cada um onde o trade-off compensa: um GraphQL na borda como camada de agregação para o frontend, conversando por trás com uma constelação de serviços internos que continuam expondo REST simples entre si, porque comunicação serviço-a-serviço não precisa da flexibilidade de forma de resposta que GraphQL oferece — precisa de contratos estáveis, cache e simplicidade operacional. Webhooks e integrações públicas de terceiros seguem majoritariamente REST, porque é o que qualquer sistema externo já sabe consumir sem biblioteca cliente especial. GraphQL entra tipicamente na camada voltada ao cliente final, onde a variação de necessidade de dados entre telas e dispositivos é real e recorrente.

Um exemplo rápido de REST puro, para contraste com a query GraphQL vista antes:

```http GET /usuarios/42 HTTP/1.1 Host: api.exemplo.com

HTTP/1.1 200 OK Content-Type: application/json

{ "id": "42", "nome": "Ana", "email": "ana@exemplo.com", "telefone": "+55...", "criadoEm": "2024-01-10T12:00:00Z" } ```

Uma única chamada, resposta fixa — inclui telefone e criadoEm mesmo que o cliente só quisesse nome. Para buscar os pedidos e os itens de cada um, seriam necessárias chamadas adicionais, uma para a lista de pedidos e, potencialmente, uma por item para resolver o produto — o mesmo cenário de under-fetching descrito antes.

Guia de decisão prático

Escolha REST quando: a API é majoritariamente consumida por poucos tipos de cliente com necessidades de dados parecidas; cache de borda/CDN importa de verdade para o volume de tráfego; a API é pública e vai ser integrada por terceiros que não vão instalar uma biblioteca cliente GraphQL; a equipe é pequena e não tem capacidade de operar DataLoader, análise de complexidade de query e uma camada de cache customizada; o domínio tem poucos relacionamentos profundos entre recursos.

Escolha GraphQL quando: há múltiplos clientes (mobile, web, parceiros) com necessidades de dados visivelmente diferentes entre si, e mantê-los com endpoints REST dedicados já virou uma proliferação difícil de sustentar; existe um BFF agregando dados de vários serviços internos e a tela final precisa de composição, não de um recurso isolado; o frontend itera rápido em cima de dados existentes e o time não quer depender do ciclo de deploy do backend para cada campo novo na tela; a equipe tem — ou está disposta a construir — a maturidade operacional para DataLoader, limite de profundidade e queries persistidas, porque sem isso o "GraphQL fácil de consumir" vira "GraphQL fácil de derrubar o banco".

Um critério de desempate, quando os dois parecem viáveis: pense em quem paga o custo de mudança. Se o custo de over-fetching e de multiplicar endpoints recai sobre o time de frontend — telas lentas, banda desperdiçada, PRs de backend toda vez que uma tela muda — GraphQL desloca esse custo para uma única vez, na construção da infraestrutura de resolvers. Se o custo de operar resolvers, DataLoader e proteção contra query cara recai sobre um time de backend pequeno que já está no limite, REST com bom design de endpoints — seleção de campos via query string, embutimento de relações via parâmetro expand — entrega 80% do ganho de flexibilidade de GraphQL com uma fração da complexidade operacional.

Fechando

REST e GraphQL não competem pelo mesmo prêmio — competem por trade-offs diferentes que fazem sentido em contextos diferentes. REST ganha em simplicidade, cache nativo e ferramentas universais, e paga esse ganho com over-fetching, under-fetching e multiplicação de endpoints quando os clientes divergem demais entre si. GraphQL ganha em flexibilidade de forma de resposta e resolve o over/under-fetching na raiz, e paga esse ganho perdendo cache HTTP de graça, herdando o N+1 do lado do servidor e abrindo uma superfície de ataque que precisa de defesas explícitas. Nenhum dos dois é "mais moderno" nem "mais correto" — são ferramentas com formas diferentes, e a decisão certa nasce de olhar para quem consome a API, quantos tipos de cliente existem e quem tem capacidade de operar a complexidade que cada estilo exige. Escolher pela moda do momento, em qualquer direção, é o único caminho garantido de pagar o pior dos dois lados.

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly