Arquitetura de Software
REST vs GraphQL vs gRPC: Escolhendo a API Certa
28 de junho de 2024·Paulo de Paula
Não existe paradigma de API universalmente melhor. A escolha depende de quem consome, como os dados trafegam e qual a tolerância da equipe à complexidade operacional.
REST: o padrão que funciona bem
REST mapeia operações para verbos HTTP e recursos para URLs. Simples, amplamente entendido, excelente suporte de ferramentas.
// GET /users/123
// GET /users/123/posts
// POST /users
// PATCH /users/123
// DELETE /users/123
// Node.js com Express
router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Usuário não encontrado' });
res.json(user);
});
router.get('/users/:id/posts', async (req, res) => {
const posts = await postService.findByUserId(req.params.id);
res.json(posts);
});Pontos fortes:
- Cache HTTP nativo (GET é cacheável por padrão)
- Ferramentas maduras: Swagger/OpenAPI, Postman, curl
- Stateless: escala horizontalmente sem esforço
- Todos os devs entendem
Pontos fracos:
- Overfetching:
GET /users/123retorna 20 campos quando você precisa de 2 - Underfetching: precisa de 3 requests para montar uma tela complexa
- Versionamento de API (
/v1/,/v2/) complica manutenção
GraphQL: flexibilidade para o cliente
O cliente define exatamente quais campos quer. Uma única endpoint, queries tipadas.
# Schema (servidor define o contrato)
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
followers: [User!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [String!]!
}
type Query {
user(id: ID!): User
posts(limit: Int = 10, offset: Int = 0): [Post!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}// Servidor com Apollo
const resolvers = {
Query: {
user: (_, { id }) => userService.findById(id),
},
User: {
posts: (user) => postService.findByUserId(user.id), // resolver aninhado
followers: (user) => userService.findFollowers(user.id),
},
};
// Cliente pede exatamente o que precisa
const GET_USER_PROFILE = gql`
query GetUserProfile($id: ID!) {
user(id: $id) {
name
posts(limit: 3) { # só 3 posts, só title
title
}
}
}
`;Problema N+1: sem DataLoader, cada campo aninhado dispara uma query separada.
// ❌ Sem DataLoader: 1 query para usuários + N queries para posts
User: {
posts: (user) => db.query('SELECT * FROM posts WHERE user_id = $1', [user.id])
}
// ✅ Com DataLoader: batching automático
const postLoader = new DataLoader(async (userIds: string[]) => {
const posts = await db.query(
'SELECT * FROM posts WHERE user_id = ANY($1)',
[userIds]
);
return userIds.map(id => posts.filter(p => p.userId === id));
});
User: {
posts: (user) => postLoader.load(user.id) // batched automaticamente
}Pontos fortes:
- Zero overfetching/underfetching
- Schema como contrato vivo, auto-documentado
- Ideal para BFF (Backend for Frontend) e apps mobile
Pontos fracos:
- N+1 queries requer DataLoader (complexidade extra)
- Cache HTTP complexo (tudo é POST)
- Curva de aprendizado para equipes novas
- Queries maliciosas podem sobrecarregar o servidor (depth limiting, query cost)
gRPC: performance e contratos estritos
gRPC usa Protocol Buffers (Protobuf) — formato binário mais compacto que JSON — e HTTP/2. Ideal para comunicação interna entre serviços.
// user.proto
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User); // streaming!
rpc CreateUser (CreateUserRequest) returns (User);
}
message User {
string id = 1;
string name = 2;
string email = 3;
repeated Post posts = 4;
}
message GetUserRequest {
string id = 1;
}// Servidor gRPC em Node.js
const server = new grpc.Server();
server.addService(UserService, {
getUser: async (call, callback) => {
const user = await userRepo.findById(call.request.id);
callback(null, user);
},
// Server-side streaming
listUsers: async (call) => {
const users = await userRepo.findAll();
for (const user of users) {
call.write(user);
await sleep(10); // simula processamento
}
call.end();
},
});Pontos fortes:
- ~5-10x mais rápido que JSON/REST para serialização
- Contratos fortemente tipados com geração de código
- Streaming bidirecional nativo
- Ideal para microserviços internos
Pontos fracos:
- Browsers não suportam gRPC diretamente (precisa de gRPC-Web + proxy)
- Debugging difícil (binário, não legível por humanos)
- Menos ferramentas que REST
Matriz de decisão
| Critério | REST | GraphQL | gRPC |
|---|---|---|---|
| API pública | ✓✓✓ | ✓✓ | ✗ |
| Clientes mobile | ✓✓ | ✓✓✓ | ✓ |
| Microserviços internos | ✓✓ | ✓ | ✓✓✓ |
| Streaming | ✗ | Subscriptions | ✓✓✓ |
| Cache HTTP | ✓✓✓ | ✗ | ✗ |
| Curva de aprendizado | baixa | média | alta |
| Performance | média | média | alta |
A abordagem híbrida (o que times maduros fazem)
┌─────────────────┐
Mobile / Web ────▶ │ GraphQL (BFF) │
└────────┬────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Users │ │ Orders │ │ Payments │
│ Service │ │ Service │ │ Service │
│ (gRPC) │ │ (gRPC) │ │ (gRPC) │
└──────────┘ └──────────┘ └──────────┘
Parceiros / Público: REST (OpenAPI documentado)- REST para APIs públicas e parceiros (documentação, compatibilidade)
- GraphQL no BFF (Backend for Frontend) para apps mobile/web
- gRPC para comunicação interna entre microserviços (performance)
Não escolha um paradigma para tudo. Escolha o certo para cada interface.