Saltar para o conteúdo
REST vs GraphQL vs gRPC: Escolhendo a API Certa
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/123 retorna 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érioRESTGraphQLgRPC
API pública✓✓✓✓✓
Clientes mobile✓✓✓✓✓
Microserviços internos✓✓✓✓✓
StreamingSubscriptions✓✓✓
Cache HTTP✓✓✓
Curva de aprendizadobaixamédiaalta
Performancemédiamédiaalta

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.