GraphQL vs REST em 2026: quando usar cada estilo de API
Equipe Arvucore
September 21, 2025 · Atualizado em August 26, 2026
14 min read
Escolha REST quando sua API expõe dados estáveis, no formato de recursos, para muitos consumidores que você não controla, e quando cache HTTP importa. Escolha GraphQL quando um punhado de clientes próprios (web, iOS, Android) precisa de fatias diferentes do mesmo grafo de dados e você quer parar de entregar um endpoint novo para cada tela. A maioria das empresas acaba com os dois: REST na borda para parceiros, GraphQL como camada de agregação para os próprios apps.
Como GraphQL e REST modelam dados e requisições
O REST modela o mundo como recursos com URLs. O verbo HTTP carrega a intenção (GET, POST, PUT/PATCH, DELETE), e o servidor decide o formato de cada resposta. Os clientes compõem telas chamando vários endpoints e costurando os resultados.
O GraphQL modela o mundo como um grafo tipado, descrito por um schema. Há um único endpoint (por convenção, POST /graphql), e o cliente envia uma query que nomeia exatamente os campos que quer, incluindo relações aninhadas. O servidor resolve cada campo e devolve um JSON que espelha a query.
A consequência prática: no REST, o servidor é dono do formato da resposta. No GraphQL, o cliente é, dentro dos limites do schema, então os times de frontend compõem queries sem abrir ticket para o backend. Essa mudança de propriedade é o motivo real de os times escolherem GraphQL, e também o motivo de ele precisar de mais guardrails.
Over-fetching e under-fetching
Over-fetching é receber campos que você não precisa: um GET /users/42 que devolve quarenta campos quando a linha de uma lista mostra dois. Under-fetching é o oposto: o endpoint não devolve o suficiente, e uma tela de perfil que precisa do usuário, dos últimos cinco pedidos e do status de cada envio vira uma chamada mais N mais M.
O REST mitiga os dois com sparse fieldsets (?fields=id,name), recursos embutidos (?include=orders) e endpoints sob medida (/users/42/profile-summary). Funcionam, mas cada combinação nova é uma mudança no backend.
O GraphQL resolve os dois estruturalmente: uma query, um round trip, só os campos pedidos. Em uma rede móvel com centenas de milissegundos por round trip, transformar três chamadas sequenciais em uma é um ganho visível. Em uma rede interna rápida, importa muito menos, e é por isso que o GraphQL raramente compensa para tráfego entre serviços.
Cache: semântica HTTP vs caches de cliente
Aqui o REST tem uma vantagem estrutural. Um GET com URL estável é cacheável por toda camada que já existe: cache do browser, CDN, reverse proxy, API gateway. Cache-Control e ETag são entendidos em todo lugar. Se o seu tráfego é majoritariamente de leitura e público (catálogos, conteúdo, preços), REST mais CDN é difícil de bater em custo. Os trade-offs de cada camada estão em estratégias de cache com Redis, Memcached e CDNs.
O GraphQL, por padrão, envia requisições POST com a query no corpo, que os intermediários não vão cachear. Três técnicas recuperam a maior parte do que se perde:
- Persisted queries. O cliente registra cada query em tempo de build e envia só um hash (
GET /graphql?extensions={"persistedQuery":...}). A requisição vira um GET estável e cacheável, e o servidor rejeita queries desconhecidas, o que também funciona como controle de segurança. - Caches normalizados no cliente. Apollo Client, Relay e urql guardam cada objeto retornado por tipo e id. Uma mutation que devolve o
Product:42atualizado renova toda tela que o exibe. Clientes REST só conseguem isso com trabalho extra no TanStack Query ou SWR. - Caches de resposta e de resolver. Um cache de resposta no servidor, indexado por hash da operação mais variáveis, e hints
@cacheControlpor campo que calculam o max-age da resposta inteira a partir do campo menos cacheável.
O resumo honesto: o cache do REST é de graça e grosseiro; o cache do GraphQL é preciso e precisa de configuração. Se o seu time não vai fazer essa configuração, o REST vence.
Versionamento e evolução
APIs REST costumam versionar na URL (/v2/orders) ou em um header. Uma nova versão major significa rodar duas implementações em paralelo até os clientes migrarem. É explícito, e é também o motivo de muitas APIs REST públicas ficarem na v1 por anos, só com mudanças aditivas.
O GraphQL desencoraja o versionamento. O schema evolui no lugar: adiciona um campo, marca o antigo como @deprecated(reason: "Use totalCents"), acompanha o uso por campo em um schema registry e remove quando o uso chega a zero. Como os clientes declaram exatamente quais campos usam, você sabe quem uma remoção afeta antes de fazê-la. Isso funciona para clientes próprios que você consegue atualizar, e funciona pior para terceiros que entregam uma query uma vez e nunca mais olham, o que é mais um motivo para manter REST na borda pública.
De qualquer forma, contract-first compensa: um documento OpenAPI ou um arquivo SDL do GraphQL versionado no repositório, com um diff check no CI que bloqueia breaking changes.
Erros e códigos de status
O REST se apoia no status HTTP: 404, 403, 422, 429, 503. Proxies, SDKs e monitoramento entendem esses códigos sem configuração, e a RFC 9457 (Problem Details) padroniza o corpo do erro.
O GraphQL devolve 200 para quase tudo e coloca os problemas em um array errors ao lado de um objeto data parcial. Uma requisição pode ter sucesso em user e falhar em user.orders. Sucesso parcial serve bem para dashboards, mas quebra ferramentas que dependem de códigos de status. Na prática:
- Reserve códigos de transporte (
400,401,429,5xx) para requisições malformadas, autenticação e infraestrutura. - Coloque erros de domínio no schema como resultados tipados (
union CheckoutResult = Order | InsufficientStock | PaymentDeclined), para que os clientes os tratem com tipos em vez de interpretar strings de mensagem. - Use
extensions.codepara classes de erro legíveis por máquina e registre o contexto completo no servidor, como descrito em estratégias de tratamento e registro de erros.
O problema N+1 e o DataLoader
Um servidor GraphQL ingênuo resolve os campos um por vez. Uma query por 50 pedidos com seus clientes chama customer 50 vezes, produzindo 51 consultas ao banco. Esse problema N+1 é o motivo mais comum de um backend GraphQL ter benchmark pior que o REST que substituiu.
A correção é fazer batching por requisição. O DataLoader (a implementação de referência do projeto GraphQL, com ports em todas as linguagens principais) coleta todas as chamadas customer.load(id) feitas durante um tick da execução, emite um único WHERE id IN (...) e guarda os resultados em cache pela duração da requisição:
const customerLoader = new DataLoader(async (ids: readonly string[]) => {
const rows = await db.customers.findMany({ where: { id: { in: [...ids] } } });
const byId = new Map(rows.map((r) => [r.id, r]));
return ids.map((id) => byId.get(id) ?? null);
});
// resolver
Order: { customer: (order, _, ctx) => ctx.loaders.customer.load(order.customerId) }
Os loaders devem ser criados por requisição, nunca compartilhados, ou a linha cacheada de um usuário vaza na resposta de outro. O REST tem o mesmo problema dentro de um handler que itera sobre linhas, mas é mais fácil de enxergar em uma função do que espalhado por resolvers.
Segurança: complexidade, profundidade, introspecção
A segurança do REST é por rota: escopos, rate limits e regras de WAF se prendem a POST /orders, e um API gateway aplica a maior parte disso. O GraphQL tem uma rota só, então os controles migram para a camada de execução:
- Limites de profundidade. Rejeite queries aninhadas além de, digamos, 8 níveis.
- Pontuação de complexidade. Atribua um custo por campo, multiplique pelo tamanho das listas (
first: 100) e rejeite ou limite acima de um orçamento. Cobre o rate limit por custo, não por número de requisições. - Só persisted queries em produção. Strings de query desconhecidas são rejeitadas, então um atacante nem consegue montar uma query patológica.
- Introspecção desligada para usuários anônimos. Ela alimenta os explorers e também é um mapa completo do seu schema. Mantenha ligada internamente e restrita na borda pública.
- Autorização por campo em resolvers ou diretivas (
@auth(requires: ADMIN)), porque uma query pode alcançar qualquer tipo a partir de qualquer ponto de entrada. - Limites de batching. Limite arrays de operações, ou uma única requisição HTTP pode carregar mil logins.
A autenticação em si é a mesma para os dois: OAuth 2.0 ou OIDC na borda, tokens de curta duração, como descrito em autenticação moderna com OAuth 2.0, JWT e zero trust.
Ferramentas e ecossistema
As ferramentas do REST são universais: OpenAPI para contratos, clientes gerados em qualquer linguagem, Postman ou Bruno para exploração, curl para debug, e todo produto de observabilidade fala rotas e códigos de status. FastAPI, NestJS e Spring geram o documento OpenAPI a partir do código.
As ferramentas do GraphQL são mais estreitas, porém mais profundas. O schema é documentação executável: GraphiQL e Apollo Sandbox oferecem autocomplete e docs inline a partir da introspecção, e o GraphQL Code Generator produz hooks e clientes tipados, então um campo renomeado quebra o build do frontend em vez de quebrar produção. Existem servidores maduros em todas as stacks: Apollo Server, GraphQL Yoga e Mercurius (Node), graphql-java e Spring for GraphQL (JVM), Hot Chocolate (.NET), Strawberry (Python), gqlgen (Go). Opções gerenciadas como Hasura, AWS AppSync e Apollo GraphOS trocam controle por velocidade. Para observabilidade, o GraphQL precisa de métricas por operação nomeada, não por rota; existe instrumentação OpenTelemetry para todos os servidores principais, mas você precisa ativá-la.
Federation vs BFF
Quando vários times são donos de vários serviços, duas arquiteturas colocam GraphQL na frente deles. Federation (Apollo Federation, ou a especificação aberta GraphQL Composite Schemas) permite que cada time publique um subgraph dono dos seus tipos; um router compõe tudo em um supergraph e planeja joins entre subgraphs via campos @key. Escala organizacionalmente, mas adiciona um router, um schema registry e regras de composição, e alguém precisa ser dono da plataforma.
Backend for Frontend é mais simples: um serviço GraphQL (ou REST) por tipo de cliente, de propriedade do time de frontend, que chama serviços REST e gRPC internos e dá forma ao resultado. Sem composição, sem registry; a duplicação entre BFFs é o preço da independência.
Escolha federation quando muitos times contribuem para um grafo consumido por muitos apps. Escolha um BFF quando um ou dois times de frontend precisam de agregação e os times de backend não querem aprender GraphQL. Os dois se encaixam naturalmente sobre uma arquitetura de microservices; nenhum vale a pena em um monolito com um único cliente.
Alternativas que merecem menção: tRPC e gRPC
tRPC oferece type safety de ponta a ponta sem schema e sem geração de código: o servidor exporta um router de procedures tipadas e o cliente importa o tipo. Só funciona quando cliente e servidor são TypeScript no mesmo repositório, e não é um formato de API pública. Para um app Next.js ou React Native com backend TypeScript, ele elimina boa parte dos motivos para adotar GraphQL.
gRPC usa Protocol Buffers sobre HTTP/2 com clientes gerados na maioria das linguagens. É rápido, fortemente tipado e faz streaming nativamente, o que o torna o padrão para chamadas internas entre serviços. Suporte a browser exige gRPC-Web ou Connect, e é incomum como API pública. O padrão usual é gRPC entre serviços com REST ou GraphQL por cima.
Tabela comparativa
| Critério | REST | GraphQL |
|---|---|---|
| Contrato | OpenAPI (opcional, externo) | Schema SDL (obrigatório, introspectável) |
| Formato da resposta | Definido pelo servidor, por endpoint | Definido pelo cliente, por query |
| Over/under-fetching | Comum; mitigado com fieldsets e includes | Resolvido por design |
| Round trips para dados aninhados | Um por recurso | Um por tela |
| Cache HTTP | Nativo (Cache-Control, ETag, CDN) |
Precisa de persisted queries como GET |
| Cache no cliente | Por URL, via bibliotecas | Normalizado por tipo e id |
| Versionamento | URL ou header, versões paralelas | Depreciação de campo, remoção guiada por uso |
| Erros | Códigos de status HTTP, Problem Details | Array errors, dados parciais, resultados tipados |
| Risco de N+1 | Dentro dos handlers, fácil de ver | Espalhado pelos resolvers, precisa de DataLoader |
| Modelo de segurança | Por rota, no gateway | Profundidade, complexidade, persisted queries, controle de introspecção |
| Upload de arquivos | Multipart nativo | Spec multipart ou endpoint REST separado |
| Tempo real | SSE ou WebSockets ao lado | Subscriptions embutidas |
| Curva de aprendizado | Baixa; universal | Moderada; conceitos novos para o backend |
| Melhor para | APIs públicas, recursos cacheáveis, CRUD simples | Vários clientes próprios, agregação, iteração rápida de UI |
Quando escolher qual: checklist por cenário
API pública para parceiros e terceiros
- REST com OpenAPI. Os consumidores conhecem, os SDKs são gerados sem atrito e o cache em CDN mantém o custo previsível.
- Adicione GraphQL só se os parceiros pedirem, atrás de persisted queries e orçamentos de complexidade.
App mobile (iOS e Android) mais web
- GraphQL se as telas agregam vários recursos e os três clientes precisam de campos diferentes. Uma query por tela, um cache normalizado e tipos gerados se pagam rápido.
- REST se o app é basicamente formulários e listas sobre poucos recursos.
Microservices internos, serviço a serviço
- gRPC, ou REST se o time quer a facilidade de debug do HTTP. O GraphQL adiciona overhead de resolvers sem o benefício de round trip em uma rede rápida.
- Se os clientes precisam de um read model unificado, coloque federation ou um BFF acima dos serviços em vez de fazer cada um falar GraphQL.
Dashboards administrativos e ferramentas internas
- GraphQL, especialmente com Hasura ou um engine parecido sobre o banco. Dashboards mudam o tempo todo, precisam de joins e filtros arbitrários, e a renderização com erro parcial combina com eles.
- tRPC se a stack inteira é TypeScript em um repositório e há exatamente um cliente.
Sinais que apontam para REST em qualquer cenário
- A maior parte do tráfego é de leituras anônimas dos mesmos dados.
- Você precisa suportar upload e download de arquivos como operações de primeira classe.
- O time não tem experiência com GraphQL e o prazo é curto.
Sinais que apontam para GraphQL em qualquer cenário
- O frontend espera o backend por "só mais um campo" toda sprint.
- Três ou mais aplicações cliente leem o mesmo domínio.
- Você precisa de dados de uso por campo para depreciar com segurança.
Exemplo mínimo: um schema, uma query, o equivalente em REST
Um pequeno read model de e-commerce:
type Query {
customer(id: ID!): Customer
}
type Customer {
id: ID!
name: String!
email: String!
orders(first: Int = 10): [Order!]!
}
type Order {
id: ID!
total: Money!
placedAt: DateTime!
items: [OrderItem!]!
shipment: Shipment
}
type OrderItem { sku: String! quantity: Int! product: Product! }
type Product { id: ID! name: String! imageUrl: String }
type Shipment { carrier: String! status: ShipmentStatus! eta: DateTime }
enum ShipmentStatus { PENDING IN_TRANSIT DELIVERED }
scalar Money
scalar DateTime
A query por trás de uma tela de "histórico de pedidos":
query OrderHistory($id: ID!) {
customer(id: $id) {
name
orders(first: 5) {
id
total
placedAt
shipment { status eta }
items { quantity product { name imageUrl } }
}
}
}
Uma requisição, uma resposta, só os campos que a tela renderiza. Com persisted queries, ela é enviada como GET e cacheada como qualquer outro GET.
A mesma tela contra uma API REST convencional:
GET /customers/42
GET /customers/42/orders?limit=5
GET /orders/1001/shipment (x5, um por pedido)
GET /products?ids=SKU-1,SKU-2,SKU-9
São oito requisições em três ondas sequenciais, ou duas se a API oferecer GET /customers/42?include=orders.shipment,orders.items.product, que é a resposta do REST quando o backend concorda em manter essa gramática de include. Os dois funcionam. A questão é qual time você quer que seja dono do formato da resposta.
Recomendação
Use REST por padrão para tudo que é exposto fora da empresa e para tráfego entre serviços, onde o gRPC é o outro candidato. Adote GraphQL quando pelo menos dois clientes próprios precisam de visões diferentes de um grafo de dados compartilhado, e se comprometa com o pacote que vem junto: DataLoader, persisted queries, limites de profundidade e complexidade, controle de introspecção e um schema registry. Se a stack é TypeScript de ponta a ponta com um único cliente, avalie tRPC primeiro. Na Arvucore, costumamos recomendar REST na borda e um único BFF GraphQL para os apps de produto, adicionando federation só quando o número de times contribuindo para o grafo transforma o BFF em gargalo.
Pronto para Transformar seu Negócio?
Vamos conversar sobre como nossas soluções podem ajudá-lo a alcançar seus objetivos. Entre em contato com nossos especialistas hoje mesmo.
Falar com um EspecialistaTags:
Equipe Arvucore
A equipe editorial da Arvucore é formada por profissionais experientes em desenvolvimento de software. Somos dedicados a produzir e manter conteúdo de alta qualidade que reflete as melhores práticas da indústria e insights confiáveis.
Perguntas frequentes
- GraphQL é mais rápido que REST?
- Não por si só. O GraphQL reduz round trips e o tamanho do payload para dados aninhados e específicos de cada cliente, o que ajuda em redes móveis lentas. O REST costuma ser mais rápido e mais barato para recursos simples e cacheáveis, porque CDNs e browsers fazem cache de respostas GET sem trabalho extra.
- Quando devo usar GraphQL em vez de REST?
- Use GraphQL quando muitos clientes diferentes precisam de visões diferentes dos mesmos dados, quando as telas agregam vários serviços de backend, ou quando o time de frontend muda as necessidades de dados mais rápido do que o backend consegue entregar endpoints. Fora isso, REST é o padrão mais simples.
- GraphQL e REST podem ser usados juntos?
- Sim, e é comum. Uma configuração típica expõe REST para parceiros e para o público, e roda uma camada GraphQL (um gateway ou BFF) sobre serviços REST e gRPC internos para os apps web e mobile da própria empresa.
- GraphQL substitui o versionamento de API?
- Ele troca o versionamento por URL pela evolução em nível de campo: você adiciona campos, marca os antigos com @deprecated e os remove quando o uso pelos clientes chega a zero. Ainda assim, você precisa de um schema registry e de rastreamento de uso para fazer isso com segurança.
- GraphQL é menos seguro que REST?
- Ele tem uma superfície de ataque diferente. Um endpoint único que aceita queries arbitrárias precisa de limites de profundidade, pontuação de complexidade, persisted queries em produção e introspecção desativada para usuários anônimos. Com esses controles, é tão seguro quanto uma API REST bem construída.
- E tRPC ou gRPC em vez de GraphQL ou REST?
- tRPC se encaixa em um monorepo TypeScript em que o mesmo time é dono do cliente e do servidor. gRPC se encaixa em chamadas internas entre serviços, em que desempenho binário e clientes gerados importam. Nenhum dos dois serve bem para uma API pública consumida por terceiros.
Artigos relacionados

Arquitetura Hexagonal em 2026: Portas, Adaptadores, Clean
Hexagonal vs Clean vs Onion explicadas com um exemplo real em TypeScript, estrutura de pastas, estratégia de testes e os erros que afundam a maioria dos times.

Jamstack em 2026: o que sobreviveu e o que o substituiu
O rótulo Jamstack sumiu, mas as práticas venceram. Estratégias de renderização comparadas, frameworks, hospedagem e um checklist para saber quando estático é a escolha certa.

Database Design: SQL vs NoSQL for Enterprise Aplicações
Este artigo da Arvucore analisa o design de bancos de dados corporativos e a escolha entre SQL e NOSQL para aplicações de banco de dados modernas. Comparamos compensações arquitetônicas, desempenho, escalabilidade e questões operacionais para ajudar tomadores de decisão de negócios e equipes técnicas europeias a escolher a abordagem correta. Orientações práticas e considerações do mundo real focam na manutenibilidade, custo e integração entre ecossistemas corporativos existentes. Priorizamos insights práticos e baseados em evidências hoje.

Arquitetura de Software: Design Orientado a Domínio na Prática
O design orientado a domínio (DDD) oferece uma abordagem pragmática para modelar domínios de negócios complexos dentro da arquitetura de software. Este artigo da Arvucore explica estratégias práticas de implementação de DDD, padrões e compensações para equipes que enfrentam desafios complexos de arquitetura de software. Ele orienta líderes técnicos e tomadores de decisão por meio de contextos delimitados, padrões táticos e alinhamento organizacional para entregar sistemas sustentáveis e alinhados aos negócios hoje mesmo.