REST vs GraphQL vs gRPC: Choosing the Right API Protocol


Your mobile app makes 12 API calls to render the home screen. Each call fetches more data than the screen needs. The user’s feed, their profile, their notifications, their friend suggestions - each is a separate round trip. On a slow mobile connection, the home screen takes 4 seconds to load.

A GraphQL API would let the client fetch exactly what it needs in a single request. But your internal microservices are chatty and need low-latency communication. gRPC would be better there. And your public API needs to be accessible from any HTTP client. REST is the right choice.

The answer is not “which is best” - it is “which fits this specific use case.”

REST: the default

REST (Representational State Transfer) is an architectural style for APIs built on HTTP. Resources are identified by URLs. Operations are expressed through HTTP methods (GET, POST, PUT, DELETE, PATCH).

Core principles:

  • Resources have URLs: /users/123, /orders/456
  • HTTP methods express intent: GET reads, POST creates, PUT/PATCH updates, DELETE removes
  • Stateless: each request contains all information needed to process it
  • Responses include status codes: 200 OK, 404 Not Found, 400 Bad Request

What REST is good at:

  • Simple, well-understood by every developer
  • Works with any HTTP client (browsers, curl, Postman)
  • Cacheable (GET responses can be cached by CDNs and browsers)
  • Easy to debug (human-readable URLs and JSON)
  • Versioning is straightforward (/v1/users, /v2/users)

What REST struggles with:

  • Over-fetching: endpoints return more data than the client needs
  • Under-fetching: multiple round trips needed to get related data (N+1 problem)
  • No strong typing or schema enforcement by default
  • Versioning becomes complex as APIs evolve
graph LR
subgraph rest["REST - Multiple Round Trips"]
  C1["Mobile client"] -->|"GET /users/123"| S1["Server"]
  S1 -->|"user data + extra fields"| C1
  C1 -->|"GET /users/123/posts"| S1
  S1 -->|"all posts + extra fields"| C1
  C1 -->|"GET /users/123/followers"| S1
  S1 -->|"all followers + extra fields"| C1
end

subgraph graphql["GraphQL - Single Request"]
  C2["Mobile client"] -->|"query with exact fields needed"| S2["Server"]
  S2 -->|"exactly what was requested"| C2
end

style C1 fill:#FAEEDA,stroke:#854F0B,color:#633806
style C2 fill:#E1F5EE,stroke:#0F6E56,color:#085041

GraphQL: query what you need

GraphQL is a query language for APIs. The client specifies exactly what data it needs. The server returns exactly that - no more, no less.

Core concepts:

  • Schema - Defines all types and operations. Strongly typed. Self-documenting.
  • Query - Read data. Specify exactly which fields you want.
  • Mutation - Write data. Returns the updated data.
  • Subscription - Real-time updates via WebSocket.
  • Resolver - Server-side function that fetches data for each field.

What GraphQL is good at:

  • Eliminates over-fetching and under-fetching
  • Single endpoint (/graphql) for all operations
  • Strong typing and schema introspection
  • Excellent for mobile clients with limited bandwidth
  • Enables rapid frontend iteration without backend changes

What GraphQL struggles with:

  • N+1 query problem: fetching a list of users and their posts triggers one query per user
  • Caching is harder (POST requests, dynamic queries)
  • Complex queries can be expensive (clients can request deeply nested data)
  • Learning curve for teams used to REST
  • File uploads are awkward

The N+1 problem in GraphQL

query {
  users {
    name
    posts {
      title
    }
  }
}

Naive implementation: fetch all users (1 query), then for each user fetch their posts (N queries). With 100 users, that is 101 database queries.

Fix: DataLoader (batching). Instead of fetching posts for each user individually, collect all user IDs and fetch all posts in one query. DataLoader is the standard solution in GraphQL servers.

gRPC: fast internal communication

gRPC is a high-performance RPC (Remote Procedure Call) framework from Google. It uses Protocol Buffers (protobuf) for serialization and HTTP/2 for transport.

Core concepts:

  • Proto file - Defines services and message types. Strongly typed. Generates client and server code.
  • Unary RPC - Single request, single response (like REST)
  • Server streaming - Single request, stream of responses
  • Client streaming - Stream of requests, single response
  • Bidirectional streaming - Stream of requests and responses

What gRPC is good at:

  • Very fast: binary serialization (protobuf) is 3-10x smaller than JSON
  • Strongly typed with code generation
  • Streaming support built in
  • Excellent for internal microservice communication
  • Multi-language support (generate clients in any language from the proto file)

What gRPC struggles with:

  • Not human-readable (binary protocol)
  • Browser support is limited (requires gRPC-Web proxy)
  • Harder to debug than REST
  • Proto schema changes require careful versioning
graph TB
subgraph comparison["Protocol Comparison"]
  REST2["REST
HTTP/1.1 or 2
JSON
Human readable
Cacheable
Any client"]
  GQL["GraphQL
HTTP/1.1 or 2
JSON
Flexible queries
Single endpoint
Strong schema"]
  GRPC["gRPC
HTTP/2
Protobuf binary
Code generated
Streaming
Fast"]
end

subgraph use["Best Used For"]
  U1["Public APIs
Simple CRUD
CDN caching needed"]
  U2["Mobile clients
Complex data needs
Bandwidth sensitive"]
  U3["Internal services
High throughput
Multi-language"]
end

REST2 --- U1
GQL --- U2
GRPC --- U3

style REST2 fill:#EEEDFE,stroke:#534AB7,color:#3C3489
style GQL fill:#E1F5EE,stroke:#0F6E56,color:#085041
style GRPC fill:#FAEEDA,stroke:#854F0B,color:#633806

Where it breaks or gets interesting

REST versioning complexity

REST APIs evolve. Adding fields is backward compatible. Removing or renaming fields breaks clients. The common solution is URL versioning (/v1/, /v2/), but maintaining multiple versions is expensive. Header versioning (Accept: application/vnd.api+json;version=2) is cleaner but less visible.

GraphQL handles this differently: fields are deprecated rather than removed. Clients can use old fields until they migrate. The schema evolves without breaking existing clients.

GraphQL security: query complexity

A malicious client can send a deeply nested query that causes the server to do enormous work:

query {
  users {
    friends {
      friends {
        friends {
          posts { comments { author { friends { ... } } } }
        }
      }
    }
  }
}

This could trigger millions of database queries. Mitigations: query depth limiting, query complexity scoring (each field has a cost, reject queries above a threshold), query whitelisting (only allow pre-approved queries in production).

gRPC and load balancers

gRPC uses HTTP/2 multiplexing. A single TCP connection carries multiple streams. Traditional L4 load balancers route at the connection level - all streams on one connection go to the same backend. This defeats load balancing.

Fix: use an L7 load balancer that understands HTTP/2 and can route individual streams (Envoy, nginx with gRPC support). Or use client-side load balancing where the gRPC client maintains connections to multiple backends.

Mixing protocols in one system

Most production systems use multiple protocols:

  • REST for public APIs (browser and third-party client compatibility)
  • GraphQL for the mobile app (bandwidth efficiency)
  • gRPC for internal microservice communication (performance)

An API gateway at the edge can translate between protocols: accept REST or GraphQL from external clients, translate to gRPC for internal services.

Real-world systems

GitHub - REST API v3 (public, widely used), GraphQL API v4 (more efficient for complex queries). Both are public.

Netflix - Uses gRPC for internal microservice communication. REST for external APIs.

Shopify - GraphQL API for their storefront and admin APIs. Allows partners to fetch exactly what they need.

Google - gRPC is used extensively internally. Many Google Cloud APIs expose both REST and gRPC interfaces.

Twitter - REST API for public access. Internal services use Finagle (Thrift-based RPC, similar to gRPC).

Airbnb - GraphQL for their web and mobile clients. Reduced the number of API calls and improved performance.

How to apply it in practice

Decision framework

Use REST when:

  • Building a public API that needs to be accessible from any client
  • You need CDN caching for GET responses
  • Your team is familiar with REST and the API is simple
  • You need human-readable, debuggable requests

Use GraphQL when:

  • Building for mobile clients where bandwidth matters
  • Your frontend needs vary significantly (different screens need different data)
  • You want to enable rapid frontend iteration without backend changes
  • You have complex, interconnected data that clients need to traverse

Use gRPC when:

  • Internal microservice communication
  • You need streaming (server push, bidirectional)
  • Performance is critical (high throughput, low latency)
  • You want strong typing and code generation across multiple languages

REST best practices

  • Use nouns for resources, not verbs: /users not /getUsers
  • Use HTTP methods correctly: GET for reads, POST for creates, PUT/PATCH for updates
  • Return appropriate status codes: 201 for created, 404 for not found, 422 for validation errors
  • Version your API: /v1/users
  • Use pagination for list endpoints: ?page=2&limit=20 or cursor-based
  • Document with OpenAPI/Swagger

GraphQL best practices

  • Use DataLoader for all database queries to prevent N+1
  • Implement query complexity limits
  • Use persisted queries in production (hash the query, send the hash)
  • Paginate list fields with cursor-based pagination
  • Use fragments to share field selections across queries

FAQ

Q: Can you use GraphQL for a public API?

Yes. GitHub’s GraphQL API is public and widely used. The main considerations: security (query complexity limits, rate limiting by complexity not just request count), documentation (GraphQL’s introspection makes self-documentation easy), and client support (any HTTP client can use GraphQL, but dedicated GraphQL clients make it easier). The main downside vs REST: CDN caching is harder because queries are POST requests with dynamic bodies.

Q: Is gRPC replacing REST?

Not for public APIs. gRPC is excellent for internal services but has limited browser support and is harder to debug. REST remains the standard for public APIs. The trend is to use gRPC internally and expose REST or GraphQL externally, with an API gateway translating between them.

Q: What is the performance difference between REST and gRPC?

Protobuf serialization is typically 3-10x smaller than JSON and 5-10x faster to serialize/deserialize. HTTP/2 multiplexing reduces connection overhead. In practice, gRPC is 2-5x faster than REST for the same operation. For most applications, this difference does not matter. For high-throughput internal services (thousands of calls per second), it is significant.

Interview questions

Q1: Your mobile app makes 8 API calls to render the home screen. How would you redesign this?

Strong answer: Two approaches. First, create a BFF (Backend for Frontend) - a dedicated API layer for the mobile app that aggregates data from multiple services and returns exactly what the home screen needs in one call. The BFF is a REST or GraphQL endpoint that internally calls multiple microservices. Second, use GraphQL - let the mobile client specify exactly what it needs in a single query. The GraphQL server resolves the query by calling the appropriate services. GraphQL is better if different screens need different data combinations. A BFF is better if the home screen data is stable and you want to optimize that specific call. In either case, the goal is to reduce round trips and over-fetching.

Q2: You are designing the API for a new microservices architecture. When would you use gRPC vs REST for service-to-service communication?

Strong answer: Use gRPC for synchronous service-to-service calls where performance matters. The benefits: binary serialization (smaller payloads), HTTP/2 multiplexing (fewer connections), code generation (type-safe clients in any language), streaming support. Use REST when: the service needs to be called from a browser or external client, you need CDN caching, or the team is not familiar with protobuf. In practice, most internal microservice communication benefits from gRPC. Use an API gateway at the edge to expose REST or GraphQL to external clients, translating to gRPC internally. This gives you the best of both: developer-friendly external API, high-performance internal communication.

Q3: A GraphQL query is causing your server to make 500 database queries. How do you fix it?

Strong answer: This is the N+1 problem. The query fetches a list of N items, then for each item fetches related data - resulting in N+1 queries. Fix with DataLoader: instead of fetching related data for each item individually, DataLoader batches all requests within a single event loop tick and fetches them in one query. Implementation: create a DataLoader for each type of related data. The DataLoader collects all requested IDs, then calls a batch function that fetches all of them in one database query. For example, fetching posts for 100 users: instead of 100 SELECT * FROM posts WHERE user_id = X queries, DataLoader batches them into one SELECT * FROM posts WHERE user_id IN (1, 2, 3, ..., 100). Also add query complexity limits to prevent clients from requesting deeply nested data that would cause this problem even with DataLoader.