Back to All Concepts
APINetworkingMicroservicesIntermediate

GraphQL vs REST vs gRPC

Comprehensive comparison of three major API paradigms: REST (resource-based), GraphQL (query-based), and gRPC (RPC-based), covering performance, use cases, and implementation trade-offs for modern distributed systems.

What are API Paradigms?

APIs (Application Programming Interfaces) enable communication between services. Three dominant paradigms have emerged: REST, GraphQL, and gRPC. Each solves different problems.

Key question: How should services communicate?

Quick Comparison

FeatureRESTGraphQLgRPC
ProtocolHTTP/1.1HTTP/1.1HTTP/2
FormatJSONJSONProtobuf (Binary)
PhilosophyResourcesQueriesRPC
PerformanceMediumMediumHigh
CachingBuilt-inComplexManual
Browser Support❌ (needs proxy)
Learning CurveLowMediumHigh
Best ForPublic APIsMobile/Complex UIsMicroservices

REST (Representational State Transfer)

Philosophy

Everything is a resource with a unique URL. Use standard HTTP methods to manipulate resources.

GET    /users/123      → Read user
POST   /users          → Create user
PUT    /users/123      → Update user
DELETE /users/123      → Delete user
Click to expand code...

Example API

javascript
// REST endpoint
GET /api/users/123
Response:
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "posts": [
    {
      "id": 1,
      "title": "Hello World",
      "comments": [...]  // All comments included!
    },
    ...
  ]
}
Click to expand code...

Problem: Over-fetching - you get ALL data even if you only need the name.

REST Implementation

javascript
// Express.js REST API
const express = require('express');
const app = express();

app.use(express.json());

// GET /users/:id
app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// POST /users
app.post('/users', async (req, res) => {
  const { name, email } = req.body;
  const user = await db.users.create({ name, email });
  res.status(201).json(user);
});

// PUT /users/:id
app.put('/users/:id', async (req, res) => {
  const user = await db.users.update(req.params.id, req.body);
  res.json(user);
});

// DELETE /users/:id
app.delete('/users/:id', async (req, res) => {
  await db.users.delete(req.params.id);
  res.status(204).send();
});

app.listen(3000);
Click to expand code...

REST Best Practices

javascript
// Versioning
GET /api/v1/users/123
GET /api/v2/users/123

// Filtering & Pagination
GET /api/users?role=admin&page=2&limit=20

// Nested resources
GET /api/users/123/posts
GET /api/users/123/posts/456/comments

// HTTP Status Codes
200 OK              // Success
201 Created         // Resource created
204 No Content      // Deleted successfully
400 Bad Request     // Invalid input
401 Unauthorized    // Not authenticated
403 Forbidden       // Not authorized
404 Not Found       // Resource doesn't exist
500 Internal Error  // Server error
Click to expand code...

Pros & Cons

Pros:

  • Simple, well-understood
  • Excellent caching (HTTP caching works out of the box)
  • Stateless, scalable
  • Great for public APIs
  • Tooling everywhere

Cons:

  • Over-fetching (getting too much data)
  • Under-fetching (need multiple requests)
  • Versioning challenges
  • No type safety

GraphQL

Philosophy

Client dictates exactly what data it needs. Single endpoint, flexible queries.

graphql
# Client query
{
  user(id: 123) {
    name
    email
    # Only get first 5 posts with titles
    posts(limit: 5) {
      title
    }
  }
}

# Server response (exactly what was asked)
{
  "data": {
    "user": {
      "name": "Alice",
      "email": "alice@example.com",
      "posts": [
        { "title": "Hello World" },
        { "title": "GraphQL Guide" }
      ]
    }
  }
}
Click to expand code...

Schema Definition

graphql
# schema.graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
}

type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
}

type Mutation {
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String, email: String): User!
  deleteUser(id: ID!): Boolean!
}
Click to expand code...

GraphQL Implementation

javascript
// Apollo Server
const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }
  
  type Post {
    id: ID!
    title: String!
    content: String!
  }
  
  type Query {
    user(id: ID!): User
    users: [User!]!
  }
  
  type Mutation {
    createUser(name: String!, email: String!): User!
  }
`;

const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      return await context.db.users.findById(id);
    },
    users: async (parent, args, context) => {
      return await context.db.users.findAll();
    }
  },
  
  Mutation: {
    createUser: async (parent, { name, email }, context) => {
      return await context.db.users.create({ name, email });
    }
  },
  
  User: {
    // Resolver for User.posts field
    posts: async (parent, args, context) => {
      return await context.db.posts.findByUserId(parent.id);
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({
    db: database,
    user: req.user  // From auth middleware
  })
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
Click to expand code...

Advanced Features

Fragments (Reusable queries):

graphql
fragment UserInfo on User {
  id
  name
  email
}

query GetUsers {
  users {
    ...UserInfo
    posts {
      title
    }
  }
}
Click to expand code...

Variables:

graphql
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

# Variables
{
  "userId": "123"
}
Click to expand code...

Subscriptions (Real-time):

graphql
subscription OnNewPost {
  postAdded {
    id
    title
    author {
      name
    }
  }
}
Click to expand code...
javascript
// Server-side subscription
const resolvers = {
  Subscription: {
    postAdded: {
      subscribe: () => pubsub.asyncIterator(['POST_ADDED'])
    }
  }
};

// Publish event
pubsub.publish('POST_ADDED', {
  postAdded: newPost
});
Click to expand code...

Pros & Cons

Pros:

  • No over/under-fetching
  • Single request for related data
  • Strong typing (schema)
  • Perfect for mobile (minimize bandwidth)
  • Introspection (self-documenting)

Cons:

  • Complex caching (can't use HTTP caching easily)
  • N+1 query problem (requires DataLoader)
  • Security risks (deeply nested queries can DoS server)
  • Harder to version
  • Files/uploads awkward

gRPC (Google Remote Procedure Call)

Philosophy

High-performance RPC using binary protocol. Call remote functions like local functions.

Client calls: getUser(123)
↓
gRPC converts to binary → Sent over HTTP/2
↓
Server executes: getUser(123)
↓
Returns binary response → Converted to object
Click to expand code...

Protocol Buffers (Protobuf)

protobuf
// user.proto
syntax = "proto3";

package users;

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc StreamUsers(StreamUsersRequest) returns (stream User);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  repeated Post posts = 4;
}

message Post {
  int32 id = 1;
  string title = 2;
  string content = 3;
}

message GetUserRequest {
  int32 id = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
}

message ListUsersResponse {
  repeated User users = 1;
  int32 total = 2;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message StreamUsersRequest {
  int32 min_id = 1;
}
Click to expand code...

gRPC Implementation

Server (Node.js):

javascript
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// Load proto file
const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).users;

// Implement service methods
const server = new grpc.Server();

server.addService(userProto.UserService.service, {
  getUser: async (call, callback) => {
    const userId = call.request.id;
    const user = await db.users.findById(userId);
    
    if (!user) {
      return callback({
        code: grpc.status.NOT_FOUND,
        message: 'User not found'
      });
    }
    
    callback(null, user);
  },
  
  listUsers: async (call, callback) => {
    const { page, page_size } = call.request;
    const users = await db.users.findAll({ page, page_size });
    callback(null, { users, total: users.length });
  },
  
  createUser: async (call, callback) => {
    const { name, email } = call.request;
    const user = await db.users.create({ name, email });
    callback(null, user);
  },
  
  // Server streaming
  streamUsers: (call) => {
    const stream = db.users.streamFrom(call.request.min_id);
    
    stream.on('data', (user) => {
      call.write(user);
    });
    
    stream.on('end', () => {
      call.end();
    });
  }
});

server.bindAsync(
  '0.0.0.0:50051',
  grpc.ServerCredentials.createInsecure(),
  () => server.start()
);
Click to expand code...

Client:

javascript
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).users;

const client = new userProto.UserService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

// Unary call
client.getUser({ id: 123 }, (error, user) => {
  if (error) {
    console.error(error);
  } else {
    console.log('User:', user);
  }
});

// Server streaming
const call = client.streamUsers({ min_id: 100 });

call.on('data', (user) => {
  console.log('Received user:', user);
});

call.on('end', () => {
  console.log('Stream ended');
});
Click to expand code...

Four Communication Patterns

protobuf
service UserService {
  // 1. Unary: Single request → Single response
  rpc GetUser(GetUserRequest) returns (User);
  
  // 2. Server streaming: Single request → Stream of responses
  rpc ListUsers(ListUsersRequest) returns (stream User);
  
  // 3. Client streaming: Stream of requests → Single response
  rpc UploadUsers(stream User) returns (UploadResponse);
  
  // 4. Bidirectional streaming: Stream ←→ Stream
  rpc Chat(stream Message) returns (stream Message);
}
Click to expand code...

Pros & Cons

Pros:

  • Extremely fast (binary, HTTP/2, multiplexing)
  • Type-safe (generated code)
  • Streaming support (built-in)
  • Multiple languages (code generation)
  • Compact payload (50-70% smaller than JSON)

Cons:

  • No browser support (needs envoy/grpc-web proxy)
  • Harder to debug (binary, not human-readable)
  • Learning curve (Protobuf, code generation)
  • Limited middleware ecosystem

Performance Comparison

Payload Size

Same data (1000 users):

JSON (REST/GraphQL): 120 KB
Protobuf (gRPC): 42 KB

Savings: 65%
Click to expand code...

Latency Benchmark

100,000 requests:

REST:     450ms average
GraphQL:  420ms average
gRPC:     180ms average

gRPC is 2.5x faster!
Click to expand code...

HTTP/2 Multiplexing

gRPC uses HTTP/2:
- Single TCP connection
- Multiple concurrent requests
- Header compression
- Server push

REST typically uses HTTP/1.1:
- One request per connection
- Head-of-line blocking
Click to expand code...

Real-World Usage

REST: Stripe API

bash
curl https://api.stripe.com/v1/charges \
  -u sk_test_xxx: \
  -d amount=2000 \
  -d currency=usd \
  -d source=tok_visa
Click to expand code...

GraphQL: GitHub API

graphql
query {
  viewer {
    repositories(first: 10) {
      nodes {
        name
        stargazers {
          totalCount
        }
      }
    }
  }
}
Click to expand code...

gRPC: Google Cloud APIs

protobuf
// Google Cloud Pub/Sub
service Publisher {
  rpc Publish(PublishRequest) returns (PublishResponse);
}
Click to expand code...

Companies using gRPC:

  • Netflix (internal microservices)
  • Uber (inter-service communication)
  • Square (payment processing)

When to Use What?

Use REST when:

  • Building public APIs
  • Need simple HTTP caching
  • Wide audience (mobile, web, third-party)
  • Team unfamiliar with GraphQL/gRPC
  • Examples: Twitter API, Stripe API, AWS API

Use GraphQL when:

  • Mobile app (minimize round trips)
  • Complex, nested data requirements
  • Rapid frontend iteration
  • Multiple client types with different needs
  • Examples: Facebook, GitHub, Shopify, Airbnb

Use gRPC when:

  • Internal microservices
  • Performance critical (low latency, high throughput)
  • Polyglot services (multiple languages)
  • Streaming data required
  • Examples: Netflix microservices, Uber backend, Google Cloud

Migration Strategies

REST → GraphQL

javascript
// Wrap existing REST endpoints
const resolvers = {
  Query: {
    user: async (parent, { id }) => {
      // Call existing REST API
      const response = await fetch(`/api/users/${id}`);
      return response.json();
    }
  }
};
Click to expand code...

REST → gRPC (Dual Support)

javascript
// Expose both REST and gRPC
app.get('/users/:id', restHandler);      // REST
server.addService(userService, grpcHandler);  // gRPC

// Use envoy proxy for gRPC-web (browser support)
Click to expand code...

Interview Tips 💡

When discussing API design in system design interviews:

  1. Identify audience: "For public API, use REST for simplicity and caching..."
  2. Performance needs: "Internal services need low latency, so gRPC..."
  3. Client requirements: "Mobile app with complex screens, GraphQL prevents over-fetching..."
  4. Trade-offs: "gRPC is fastest but doesn't work in browsers without proxy..."
  5. Real examples: "Netflix uses gRPC for microservices, GitHub uses GraphQL for flexible queries..."
  6. Hybrid approach: "Could use REST for public API, gRPC for internal services..."

Related Concepts

About ScaleWiki

ScaleWiki is an interactive educational platform dedicated to demystifying distributed systems, software architecture, and system design. Our mission is to provide high-quality, technically accurate resources for software engineers preparing for interviews or solving complex scaling challenges in production.

Read more about our Editorial Guidelines & Authorship.

Educational Disclaimer: The architectural patterns and system designs discussed in this article are based on common industry practices, technical whitepapers, and public engineering blogs. Actual implementations in enterprise environments may vary significantly based on specific product requirements, legacy constraints, and evolving technologies.

Related Articles