REST vs GraphQL vs gRPC: Three API Styles and Where Each Shines
After five years shipping production systems across e-commerce, SaaS, and ride-hailing platforms, I've learned that choosing the right API style is less about which is "best" and more about matching the communication pattern to your constraints. REST vs GraphQL vs gRPC represent three fundamentally different philosophies, and getting this wrong early costs you technical debt for years. Let me walk you through each one, when I'd reach for it, and the real tradeoffs I've seen in the field.
Comparison: Quick Reference
Before we dive deep, here's how each style maps to real problems:
| Style | Best For | Overhead | Caching | Learning Curve |
|---|---|---|---|---|
| REST | Public APIs, simple CRUD, third-party integrations | Low | Native HTTP caching | Shallow |
| GraphQL | Varied frontends, complex nested data, mobile with poor connectivity | Medium to high (server-side) | Requires custom logic | Moderate |
| gRPC | Internal service-to-service calls, real-time streaming, latency-sensitive systems | Very low | Application-level only | Steep |
REST Is the Safe Default
REST wins the "understood by everyone" award, and that matters more than people admit. When I was building the CRM platform three years ago, we chose REST for the core API. Every junior developer, every third-party contractor, every integration partner understood the model without training. GET, POST, PUT, DELETE. Status codes. Standard headers.
Here's why REST still dominates public APIs and simple CRUD operations:
- HTTP caching is built-in. A GET request to
/api/users/123can be cached by proxies, CDNs, and browsers without you writing a line of code. In our e-commerce platform, this saved us 40% of database hits on product listings. - Debugging is trivial. Open your browser, hit an endpoint, see JSON. Compare that to debugging a GraphQL query or a binary gRPC payload.
- Third-party integrations work immediately. Zapier, Make, and other automation platforms speak REST fluently. We didn't have to build custom connectors.
- Standards are stable. HTTP 1.1 has been around for decades. The spec won't change tomorrow.
The cost? You often fetch too much data (over-fetching) or make multiple calls to assemble what you need (under-fetching). A mobile client on 3G bandwidth might request a user profile and get back their full transaction history, geolocation, and preferences—when it only needed the name and avatar.
Here's a typical REST endpoint I'd write:
// Express.js example
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.set('Cache-Control', 'public, max-age=300');
res.json(user);
});
// Client fetches entire user object
// {
// id: 123,
// name: "Alice",
// email: "alice@example.com",
// role: "admin",
// createdAt: "2024-01-15",
// preferences: { ... },
// recentOrders: [ ... ]
// }
REST makes sense for straightforward CRUD operations, public APIs where you control the contract for years, and systems where caching is a first-class concern. It's predictable. It's boring. That's exactly why I reach for it in production.
GraphQL Moves Complexity to the Server
GraphQL is brilliant when your frontend demands are chaotic. On the travel platform we built, web wanted full booking history with airline details and reviews, iOS wanted just the flight times, and the admin dashboard wanted cost breakdowns with supplier metadata. A REST API would have spawned five endpoints. GraphQL handled it with one schema.
GraphQL's core promise is ask for exactly what you need. The client specifies the shape of the response. The server obligates itself to deliver it.
// GraphQL query from web client
query GetUserBooking($userId: ID!) {
user(id: $userId) {
name
bookings {
id
flight {
airline
departure
arrival
}
reviews {
rating
comment
}
}
}
}
// GraphQL query from iOS client
query GetUserBooking($userId: ID!) {
user(id: $userId) {
name
bookings {
id
flight {
departure
arrival
}
}
}
}
// Same schema, different payloads
This is genuinely powerful. Mobile apps on poor connections don't waste bandwidth. Frontends decouple from backend changes because they request specific fields. You eliminate entire classes of versioning problems.
But here's what I've learned: GraphQL trades client simplicity for server complexity. A few hard-won lessons from production:
- N+1 queries are your constant enemy. A naive GraphQL resolver can trigger thousands of database queries if you're not careful. In our CRM platform, a single query requesting users and their related opportunities exposed a nested resolver that queried the database per user. We had to implement DataLoader to batch queries.
- Caching becomes your responsibility. HTTP caching doesn't work well with POST-based GraphQL. You lose CDN benefits. We built a Redis-backed query cache, but now we own the invalidation logic.
- Unauthorized queries can expose attack surface. A client can request deeply nested data that wasn't meant for them. Query complexity limits and field-level permissions are non-negotiable.
- Debugging distributed queries is harder. When a GraphQL request fans out to five backend services, tracing and error handling demand observability infrastructure.
GraphQL excels when you have multiple frontends with different data shapes (web, mobile, internal tools), when your clients are sophisticated enough to construct queries safely, and when you can invest in resolver optimization and caching layers. It's ideal for SaaS platforms with many UI variations and trading systems where clients need surgical data access.
gRPC Is Fast and Typed
gRPC is what you build when you stop optimizing for the human reading the documentation and start optimizing for latency. It uses HTTP/2 framing, binary serialization with Protobuf, and code generation from typed contracts. I reached for gRPC in the ride-hailing platform because we needed subsecond latency across microservices during peak demand—and REST simply couldn't deliver that predictably.
Here's a Protobuf contract I wrote for internal service communication:
// driver.proto
syntax = "proto3";
package example;
service DriverService {
rpc GetDriver(DriverRequest) returns (DriverResponse);
rpc StreamActiveDrivers(Empty) returns (stream DriverUpdate);
}
message DriverRequest {
string driver_id = 1;
}
message DriverResponse {
string id = 1;
string name = 2;
double latitude = 3;
double longitude = 4;
int32 rating = 5;
}
message DriverUpdate {
string driver_id = 1;
double latitude = 2;
double longitude = 3;
int64 timestamp = 4;
}
Generate TypeScript stubs from this, and both client and server speak the exact same language. No JSON parsing overhead. No runtime surprises because a field type changed. The protocol is version-managed in the Protobuf definition itself.
gRPC's advantages for service-to-service communication are substantial:
- Binary protocol means tiny payloads and fast serialization. A message that's 5 KB in JSON might be 500 bytes in Protobuf. Over thousands of requests per second, that matters.
- HTTP/2 multiplexing eliminates head-of-line blocking. Multiple requests share the same connection without waiting for responses in order.
- Streaming is built-in. We used
StreamActiveDriversto push driver location updates to the matching service without polling. Try that with REST. - Typed contracts prevent bugs. You can't accidentally pass a string where an integer is expected. The compiler catches it.
The tradeoffs are real, though:
- You can't browse a gRPC endpoint in your browser. Debugging requires grpcurl or Postman (and even then, it's awkward). We built internal gRPC reflection tools to make this manageable.
- Organizational friction is high. Every team needs to adopt Protobuf tooling, code generation, and versioning discipline. In a 20-engineer company, this might be overkill. In a 200-engineer company, it's essential.
- Load balancing requires smarter infrastructure. gRPC connections are long-lived multiplexed streams, not stateless HTTP requests. Traditional load balancers get confused. We had to use Envoy.
gRPC is my first choice for internal microservice communication, especially when latency budgets are tight or when you need streaming. It's not for public APIs or when you need maximum debugging simplicity.
Common Mistakes
Choosing based on hype instead of constraints: I've seen teams adopt GraphQL because it's trendy, not because multiple frontends actually demand different data shapes. If you have one or two clients, REST is faster to ship. Save GraphQL for when you've proven you need it.
Underestimating gRPC operational overhead: gRPC requires infrastructure knowledge: Envoy, Kubernetes service networking, protocol buffer tooling. If your team isn't already comfortable with these concepts, it's a heavy lift. We spent two weeks getting gRPC observability right on the ride-hailing platform.
Assuming GraphQL solves overfetching permanently: It doesn't. If every client requests every field, you're back to fetching everything anyway. GraphQL is a tool for expressing optionality, not a magic fix for bad schema design.
Building gRPC for external partners: Your API gateway should translate to REST or GraphQL for third parties. gRPC stays behind the edge. The cognitive load of asking external developers to learn Protobuf isn't worth the latency gains.
Forgetting about observability from the start: REST has built-in logging because everything is HTTP. GraphQL and gRPC require deliberate instrumentation. On every project, I'd add tracing (OpenTelemetry) and rate limiting from day one.
Key Takeaways
- REST remains the default for public APIs and third-party integrations because it's understood everywhere and HTTP caching is free.
- GraphQL shines when you have multiple frontends with different data demands, but you must invest in resolver optimization, caching, and security to avoid production disasters.
- gRPC is the right choice for internal service-to-service communication when latency and bandwidth matter, but accept the operational and organizational complexity.
- Most production systems use all three: REST for external APIs, GraphQL for a sophisticated web/mobile platform, and gRPC for critical internal paths.
- Choose based on your actual constraints—latency requirements, client diversity, third-party integration needs, team expertise—not on which technology sounds coolest.
- Invest in observability regardless of which style you pick. Tracing and metrics reveal the real problems long before load testing does.






Comments
Loading comments…