REST vs GraphQL vs gRPC
The decision
When designing APIs, three dominant protocols each represent a different philosophy about how clients and servers should communicate. REST (Representational State Transfer) organizes APIs around resources and HTTP verbs, using URLs to identify resources and standard HTTP methods (GET, POST, PUT, DELETE) for operations. GraphQL provides a query language where clients specify exactly what data they need in a single request. gRPC uses Protocol Buffers and HTTP/2 to provide a strongly typed RPC framework optimized for performance and code generation.
REST is the default for public APIs and web applications because it maps naturally to HTTP, is understood by every HTTP client, and requires no special tooling. GraphQL solves REST's most common pain points—over-fetching (getting more data than needed) and under-fetching (making multiple requests to assemble what's needed)—by letting clients declare their data requirements in a single query. gRPC prioritizes performance and developer ergonomics for internal service-to-service communication where both sides are controlled by the same organization.
These are not mutually exclusive. Many architectures use all three: gRPC for internal service communication, REST for public and partner APIs, and GraphQL as a gateway layer for mobile and web clients. The question is which protocol is right for each interface boundary, not which one to use universally.
Why it matters
The choice of API protocol shapes every subsequent layer of the system. REST's statelessness and cacheable responses are fundamental to how the web scales—CDNs, browser caches, and proxy caches all depend on HTTP's caching semantics. GraphQL's query language provides power that requires careful security design: without depth limiting and query complexity analysis, a malicious client can craft queries that join every table in your database. gRPC's binary encoding provides 5-10x smaller payloads and 2-8x faster serialization than JSON REST, which matters enormously in high-throughput internal APIs.
Developer experience is a significant secondary concern. REST's ubiquity means every language has HTTP clients, every engineer can read a curl command, and every tool understands HTTP response codes. GraphQL requires a schema introspection step, a specialized client library, and familiarity with the query language. gRPC requires proto file compilation and generated code, which adds a build step but provides IDE autocompletion, compile-time type checking, and automatic documentation generation.
The long-term evolution story differs substantially. REST APIs evolve through URL versioning (/v2/users) or header versioning. GraphQL schemas evolve by adding fields (always backward compatible) and deprecating old ones, providing a smooth migration path. gRPC evolves through Protobuf's field number system—adding new fields is backward compatible as long as field numbers are never reused. Getting schema evolution wrong leads to breaking API changes that ripple through every client, which is why understanding each protocol's versioning story is critical before committing.
Choose REST when
- The API is public-facing or exposed to third-party developers who may use any language or toolset
- Browser-native fetch/XMLHttpRequest clients need to call the API without library dependencies
- HTTP caching is important—GET endpoints with stable URLs can be cached at CDN layer
- The data model is simple and resource-oriented with predictable access patterns
- The team is more familiar with REST and tooling (OpenAPI, Swagger, Postman) is already established
- The API needs to be easily testable with curl or simple HTTP tools without schema compilation
Choose GraphQL when
- Multiple clients (web, mobile, TV) need different subsets of the same data and REST forces over-fetching
- Data is highly relational and clients frequently need to traverse multiple resource types in one screen
- The API serves as a federation layer aggregating data from multiple backend services
- Rapid product iteration requires clients to evolve their data requirements without backend changes
- Precise bandwidth control matters (mobile clients on limited data connections benefit from exact field selection)
Comparison
REST GraphQL gRPC
══════════════════ ══════════════════ ══════════════════════
Request: Request: Request (proto binary):
GET /users/123 POST /graphql message GetUserRequest {
GET /users/123/orders string user_id = 1;
GET /orders/456/items { user(id: "123") { }
name
orders { rpc GetUser(GetUserRequest)
Response per request: id returns (User);
{ items {
"id": 123, name
"name": "Alice", price
"email": "..." } Response (binary, ~40% of JSON):
} } User {
} string name = 1;
Multiple round trips ↓ repeated Order orders = 2;
to assemble one Single response: }
screen's data {
"data": { Wire format: Protocol Buffers
Multiple endpoints: "user": { Transport: HTTP/2
GET /users "name": "Alice", Streaming: bidirectional
POST /users "orders": [{ Code gen: automatic stubs
PUT /users/:id "id": "1", IDL: .proto files
DELETE /users/:id "items": [...]
}]
HTTP verbs: }
GET = read }
POST = create One round trip for
PUT = replace any data shape
PATCH = partial
DELETE = remove Schema introspection:
{ __schema { types { name }}}
WHEN TO USE EACH:
REST → Public APIs, simple CRUD, browser clients, CDN caching
GraphQL → Complex data needs, multiple clients, API gateway layer
gRPC → Internal services, high performance, streaming, polyglot RPC
PERFORMANCE:
REST/JSON Serialization: moderate | Payload: verbose
GraphQL/JSON Serialization: moderate | Payload: optimized (no over-fetch)
gRPC/Protobuf Serialization: fast | Payload: compact binary (~60-80% smaller)
Trade-offs
gRPC strengths
- Binary Protocol Buffers encoding is significantly more compact and faster to serialize than JSON
- Generated client and server stubs eliminate boilerplate and provide compile-time type safety
- HTTP/2 multiplexing enables multiple concurrent streams on one connection and bidirectional streaming
- Schema-first design with .proto files provides a machine-readable contract for documentation and tooling
gRPC weaknesses
- Not natively supported by browsers—requires a proxy layer (gRPC-Web) for browser clients
- Proto file compilation adds a build step; managing generated code across languages adds complexity
- Binary encoding is not human-readable, complicating debugging and manual testing with simple tools
- Ecosystem is smaller than REST—less tooling, fewer tutorials, steeper learning curve for new team members
Implementation considerations
For REST APIs, invest in an OpenAPI (Swagger) specification from the start. Auto-generate client SDKs, API documentation, and server validation from the spec rather than maintaining them manually. Use consistent error response formats across all endpoints—a standard problem detail format (RFC 7807) with machine-readable error codes saves enormous client-side integration effort. Design URLs around resources, not actions: POST /orders rather than POST /createOrder. Implement API versioning strategy early—URL versioning (/v1/) is explicit and cacheable; header versioning is cleaner but less visible.
For GraphQL, security configuration is non-negotiable. Implement query depth limiting (max depth 10), query complexity analysis (max complexity score), and per-query timeout to prevent denial-of-service through expensive queries. Disable introspection in production if the API is public-facing—introspection reveals your entire schema to potential attackers. Use DataLoader pattern (or equivalent) to batch and cache database calls within a single request, preventing the N+1 query problem where resolving a list of N users triggers N separate database queries for their associated data.
For gRPC, design your proto files with backward compatibility in mind from the start. Never change field numbers—Protobuf uses field numbers (not names) for binary encoding. Adding new fields is safe; removing or renaming is not. Use proto3 syntax which treats all fields as optional. For public APIs or inter-team boundaries, consider gRPC-gateway to automatically generate a REST/HTTP JSON API from your proto definitions, giving you both gRPC for high-performance clients and REST for broad compatibility from a single proto definition.
Common mistakes
- GraphQL N+1 queries: Without DataLoader batching, resolving a list of 100 users and their orders fires 100 separate database queries; always use batching on any list resolver that touches the database.
- REST verbs as resource names:
POST /getUserorGET /deleteOrderare REST anti-patterns that confuse caches, proxies, and developers; use HTTP verbs for their intended semantics. - No API versioning strategy: Making breaking changes to a REST API without versioning breaks existing clients silently; define a versioning policy before the first external consumer integrates.
- Reusing field numbers in Protobuf: Removing a field from a proto message and using its number for a new field causes data corruption when old clients or stored binary data is decoded; always reserve removed field numbers.
- GraphQL for internal service APIs: Using GraphQL for service-to-service calls where both sides are controlled gives the complexity of GraphQL without the flexibility benefit; gRPC or REST is typically better for internal APIs.
Decision checklist
- Who are the API consumers—external developers, browser clients, mobile apps, or internal services?
- Do different clients need different subsets of the same data? (Favors GraphQL)
- Is performance critical enough that binary encoding overhead savings (gRPC) justify the tooling complexity?
- Does the API need to be cacheable at the CDN or browser level? (Requires REST GET semantics)
- Will you need bidirectional or server-push streaming? (gRPC has native support; REST requires WebSockets or SSE)
- What is your team's existing expertise and what tooling is already established in your stack?
- Is this a public API that third-party developers will integrate with, or an internal API within your organization?
Real-world examples
- GitHub: Offers both REST API (v3) and GraphQL API (v4). The GraphQL API was introduced specifically to address REST's over-fetching and multiple round-trip problems for clients building complex views of repositories, pull requests, and issues—a classic polyglot API strategy serving different client needs.
- Netflix: Uses gRPC extensively for internal service-to-service communication across its microservices, where the performance benefits of Protobuf binary encoding and HTTP/2 multiplexing across thousands of inter-service calls per second translate to measurable infrastructure cost savings.
- Shopify: Uses GraphQL as its primary storefront API (Storefront API) to enable merchants to build custom storefronts that fetch exactly the product data their unique UI needs—avoiding the over-fetching of a generic REST product endpoint that returns hundreds of fields regardless of what the client displays.
Related decisions
- Synchronous vs Asynchronous Communication — all three protocols are synchronous; async adds a different dimension
- REST API Design — deep-dive on REST resource modeling and best practices
- GraphQL Architecture — schema design, federation, and query optimization
- gRPC — Protobuf schema design and service definition best practices