REST vs GraphQL vs gRPC
REST for public APIs, GraphQL for flexible client data needs, gRPC for high-performance internal services — and many systems use all three.
The Problem
REST, GraphQL, and gRPC each excel at different things. How should teams choose the right API paradigm — or combination of paradigms — without over-engineering or making a choice they will regret at scale?
Mental Model
Like choosing between a buffet (REST), a personal chef (GraphQL), and a pre-set menu with instant service (gRPC)
Architecture Diagram
How It Works
This isn't a "which is best" article. It's a decision framework. REST, GraphQL, and gRPC solve different problems, and the best engineering teams use the right tool for each context — often all three in the same system.
The Core Differences
REST models the API as resources with URLs. Interactions happen through standard HTTP methods. It's the lingua franca of web APIs.
GET /api/users/42 → Read user
POST /api/users → Create user
PUT /api/users/42 → Replace user
PATCH /api/users/42 → Update fields
DELETE /api/users/42 → Delete user
GraphQL models the API as a graph of types. The client writes a query describing exactly what data it needs, and the server returns exactly that shape.
query {
user(id: 42) {
name
email
orders(last: 5) {
id
total
items { name, quantity }
}
}
}
gRPC models the API as procedure calls with strongly-typed contracts. The client calls a method with a typed request and receives a typed response, serialized as compact binary protobuf.
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListOrders(ListOrdersRequest) returns (stream Order);
}
The Comparison Matrix
This is the table that actually matters when making a decision:
| Dimension | REST | GraphQL | gRPC |
|---|---|---|---|
| Latency | Medium (text JSON) | Medium (text JSON) | Low (binary protobuf) |
| Payload size | Fixed per endpoint | Exact client needs | Minimal (binary) |
| Caching | Excellent (HTTP native) | Hard (POST-based, custom) | Hard (needs app-level) |
| Type safety | Optional (OpenAPI) | Built-in (schema) | Built-in (protobuf) |
| Browser support | Native | Native (over HTTP) | Needs gRPC-Web proxy |
| Tooling maturity | Excellent | Good | Good (but specialized) |
| Learning curve | Low | Medium | Medium-High |
| Streaming | SSE or WebSocket add-on | Subscriptions (add-on) | Native (4 modes) |
| Error handling | HTTP status codes | Always 200, errors in body | gRPC status codes |
| Discoverability | HATEOAS / OpenAPI docs | Introspection queries | Reflection / proto files |
| Over-fetching | Common problem | Solved by design | N/A (typed responses) |
| Under-fetching | Common (N+1 requests) | Solved by design | N/A (design the RPCs well) |
When to Use REST
REST is the right default for:
Public APIs — Every developer on Earth knows how to make an HTTP request. Stripe, Twilio, and GitHub built empires on REST APIs. The ecosystem of tools (Postman, curl, OpenAPI, API gateways) is unmatched.
CRUD-heavy services — When the API maps cleanly to resources with standard operations, REST's resource-oriented model is natural and intuitive.
Cacheable data — REST leverages HTTP caching natively. A GET request with proper Cache-Control and ETag headers can be cached at every layer: browser, CDN, reverse proxy. GraphQL and gRPC can't do this without significant custom work.
# REST caching just works
curl -H "If-None-Match: abc123" https://api.example.com/products/42
# 304 Not Modified — zero bandwidth, zero server processing
The REST anti-pattern: Building a REST API where clients need to make 5-10 requests to assemble a single view (e.g., user + orders + reviews + recommendations). This is the over-fetching/under-fetching problem that GraphQL was invented to solve.
When to Use GraphQL
GraphQL shines when:
Multiple clients need different data shapes — A mobile app needs a user's name and avatar. A web dashboard needs name, email, role, last login, and recent activity. A TV app needs name and preferences. With REST, the options are returning everything (wasting mobile bandwidth) or building per-client endpoints (duplicating logic). GraphQL lets each client request exactly what it needs.
Rapid frontend iteration — Frontend teams can change their data requirements without waiting for backend changes. Add a field to the query, and it works — no new endpoint needed.
Aggregating multiple backends — GraphQL federation (Apollo, Hasura) composes multiple services into a single graph. The client queries one endpoint, and the gateway distributes the query across services.
// Apollo Federation — compose microservice schemas
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://users-service/graphql' },
{ name: 'orders', url: 'http://orders-service/graphql' },
{ name: 'inventory', url: 'http://inventory-service/graphql' },
],
}),
});
The GraphQL anti-patterns:
- Not implementing query cost analysis — a malicious client can craft a deeply nested query that takes down the server
- Ignoring the N+1 problem — a naive resolver fetches one database row per item in a list. Use DataLoader.
- Using GraphQL for simple CRUD where REST would be simpler
When to Use gRPC
gRPC is the right choice for:
Internal service-to-service communication — When both sides are services under the same team's control, the protobuf toolchain's overhead pays for itself in performance, type safety, and automatic code generation.
High-throughput, low-latency paths — Protobuf is 3-10x smaller and 20-100x faster to parse than JSON. For services handling millions of requests per second, this matters.
Streaming use cases — gRPC's four streaming modes (unary, server stream, client stream, bidirectional) are first-class citizens, not bolt-on features.
Polyglot microservices — A single .proto file generates client and server code in Go, Java, Python, Rust, C++, and more. The contract is the code.
# gRPC is compact
# JSON: {"user_id": 12345, "name": "Alice", "active": true} = 49 bytes
# Protobuf: same data = 12 bytes (4x smaller)
# At 1M requests/sec, that's 37 MB/sec saved
The gRPC anti-patterns:
- Using it for public-facing browser APIs without gRPC-Web
- Not setting deadlines (timeouts propagate through the call chain — this is a feature, use it)
- Generating code manually instead of integrating protoc into the build pipeline
The Hybrid Architecture
The most successful architectures use all three:
┌─────────────────┐
Browser/Mobile → │ API Gateway │
│ (REST + GraphQL)│
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User Svc │ │Order Svc │ │ Inv Svc │
│ (gRPC) │←→│ (gRPC) │←→│ (gRPC) │
└──────────┘ └──────────┘ └──────────┘
- External: REST for partners and simple integrations, GraphQL for frontend apps
- Internal: gRPC for service-to-service, with deadlines and streaming
- Gateway: Translates between external protocols and internal gRPC
This is the Netflix, Airbnb, and Uber pattern. It's not over-engineering — it's using the right tool at each boundary.
Migration Guide
REST → GraphQL: Start with a GraphQL gateway that wraps the existing REST endpoints. No backend changes needed initially. Migrate resolvers to direct database access over time.
REST → gRPC: Define .proto files that mirror the existing REST resources. Run both in parallel during migration. Use Envoy's gRPC-JSON transcoding to maintain backward compatibility.
GraphQL → gRPC (internal): Keep GraphQL as the frontend-facing layer. Replace REST-based resolvers with gRPC calls to backend services. The GraphQL server becomes a BFF (Backend for Frontend).
The key insight: these aren't mutually exclusive choices. They're tools in the toolbox. Use the right one at each boundary in the system.
Key Points
- •There is no universally best choice — the right answer depends on the client types, team size, performance needs, and caching requirements
- •REST is the default choice for public APIs because every language, tool, and developer already knows HTTP
- •GraphQL solves the over-fetching/under-fetching problem but introduces query complexity, N+1 issues, and caching challenges
- •gRPC is 5-10x faster than JSON-based APIs but sacrifices human readability and browser compatibility
- •Many production systems use all three — REST for public APIs, GraphQL for mobile/frontend BFFs, gRPC for internal services
Key Components
| Component | Role |
|---|---|
| REST (Representational State Transfer) | Resource-oriented architecture using standard HTTP methods, URLs, and status codes |
| GraphQL | Query language that lets clients request exactly the data they need in a single request |
| gRPC | Binary RPC framework with strongly-typed contracts and streaming over HTTP/2 |
| API Gateway | Often sits in front of all three, handling routing, auth, and protocol translation |
| Schema/Contract | OpenAPI for REST, SDL for GraphQL, .proto for gRPC — each defines the API surface differently |
When to Use
Use REST for maximum compatibility and cacheability (public APIs, CRUD services). Use GraphQL when multiple clients need different data shapes from the same backend (mobile vs web vs TV). Use gRPC for internal service-to-service calls where performance and type safety matter more than human readability.
Tool Comparison
| Tool | Type | Best For | Scale |
|---|---|---|---|
| Express / Fastify (REST) | Open Source | Building REST APIs in Node.js with minimal boilerplate and maximum ecosystem support | Startups to enterprise |
| Apollo Server (GraphQL) | Open Source | Full-featured GraphQL server with federation, caching, and extensive plugin ecosystem | Medium to large frontend-driven applications |
| gRPC-Go / gRPC-Java | Open Source | High-performance internal service communication with code generation from protobuf | Google-scale microservice architectures |
| Hasura | Open Source | Instant GraphQL API over PostgreSQL with real-time subscriptions and authorization | Rapid prototyping to production |
Debug Checklist
- For REST: Check HTTP status codes, Content-Type headers, and response body for error details
- For GraphQL: Inspect the errors array in the response — GraphQL returns 200 even for partial failures
- For gRPC: Check gRPC status codes (not HTTP codes) — UNAVAILABLE, DEADLINE_EXCEEDED, PERMISSION_DENIED
- Compare payload sizes: measure JSON vs protobuf for the actual data shapes in the system
- Profile serialization time: JSON.parse vs protobuf decode for the relevant message sizes
Common Mistakes
- Choosing GraphQL because it's trendy without considering the operational complexity of query analysis and N+1 prevention
- Using REST for internal high-throughput service-to-service calls where gRPC would eliminate serialization overhead
- Building a GraphQL API without implementing query depth limiting and cost analysis — opening the system to denial-of-service
- Assuming gRPC replaces REST for public APIs — browser support requires gRPC-Web, which adds deployment complexity
- Over-engineering with multiple paradigms when a simple REST API with good pagination would suffice
Real World Usage
- •Stripe uses REST for their public API — maximum compatibility and the best documentation in the industry
- •GitHub offers both REST and GraphQL APIs — REST for simple operations, GraphQL for complex data fetching
- •Netflix uses GraphQL as a BFF (Backend for Frontend) layer while internal services communicate via gRPC
- •Shopify migrated their public API to GraphQL to let merchants request exactly the product data they need
- •Google uses gRPC internally for nearly all inter-service communication, with REST/JSON for public-facing APIs