REST vs. GraphQL vs. gRPC: A Brutally Honest Guide to API Design
Bad API design isnât just annoyingâitâs a productivity black hole that makes integration feel like debugging assembly code blindfolded. Good API design makes consuming your service feel inevitable and obvious.
The internet is full of religious wars about which API pattern is âbest.â Thatâs the wrong question. There are only tools with different strengths and catastrophic failure modes. Choose based on your actual requirements, not whatâs trending.
REST: The Reliable Default
REST isnât a protocolâitâs architectural principles built on HTTP. Think of it as the Honda Civic of APIs: boring, reliable, and everyone knows how to use it.
How REST Works
Use standard HTTP verbs (GET, POST, PUT, DELETE) to manipulate resources at predictable URLs:
GET /users/123 # Retrieve user
POST /users # Create user
PUT /users/123 # Update user
DELETE /users/123 # Delete user
GET /users/123/posts # Get user's posts
Itâs stateless (every request contains all needed information) and cacheable (standard HTTP caching works).
What REST Does Well
Simplicity: If you understand HTTP, you understand REST. The learning curve is gentle and every developer already knows it.
Caching: Standard HTTP caching works perfectly. CDNs, browser caches, and HTTP proxies handle it automatically. This is massive for performance.
Ubiquity: Every language, framework, and tool supports HTTP requests. Documentation, libraries, and community support are endless.
Debuggability: curl and browser DevTools work perfectly. You can debug REST APIs with basic tools.
Where REST Fails You
Over-fetching: Need a userâs name? The /users/123 endpoint returns their entire profile including fields you donât need. Youâre wasting bandwidth.
Under-fetching: Building a blog post view requires multiple round trips:
GET /posts/456 # Get post
GET /users/123 # Get author
GET /posts/456/comments # Get comments (N requests for N comments)
This is slow, inefficient, and requires coordinating multiple requests in your application.
Endpoint proliferation: Different clients need different data, so you create /users/minimal, /users/full, /users/with-posts. Your API becomes a graveyard of specialized endpoints.
Use REST When
- Building a public API for external developers
- Your data maps to simple CRUD resources
- Caching and HTTP compatibility matter
- You want simplicity over optimization
- Youâre building a standard CRUD application
Donât use REST when: You have complex data aggregation needs across many clients, or mobile apps where multiple round trips kill performance.
GraphQL: The Precision Tool
GraphQL is a query language that lets clients request exactly the data they need. Instead of dozens of endpoints, you typically have one (/graphql).
How GraphQL Works
The client sends a query describing the exact data structure it needs:
query {
post(id: "456") {
title
author {
name
avatar
}
comments(limit: 10) {
body
author {
name
}
}
}
}
One request replaces the three REST calls, and returns only requested fields.
What GraphQL Does Well
Eliminates Over/Under-fetching: This is its entire reason for existing. Clients get exactly what they need in one request.
Strong Type System: The schema serves as a contract between frontend and backend. This enables powerful developer tools, auto-completion, and compile-time validation.
Frontend Autonomy: Frontend developers can change data requirements without waiting for backend teams to create new endpoints.
Self-Documenting: The schema IS the documentation. Tools can introspect your API automatically.
Where GraphQL Destroys You
The N+1 Problem: Your elegant query can trigger catastrophic database load:
query {
users { # 1 query
name
posts { # N queries (one per user)
title
comments { # N*M queries (one per post)
body
}
}
}
}
10 users with 10 posts each with 10 comments = 1,110 database queries. Your innocent query just took down production.
DataLoader Complexity: Solving N+1 requires DataLoader or similar batching/caching libraries. You now need to understand request-scoped caching, batch scheduling, and cache invalidation. This isnât optionalâitâs survival infrastructure.
Caching Nightmare: RESTâs URL-based caching? Gone. GraphQL queries are POST requests to a single endpoint. You need:
- Application-level server-side caching
- Client-side normalized caches (Apollo Client)
- Query analysis and complexity estimation
- Custom cache invalidation strategies
Backend Complexity Explosion: Every field needs a resolver. Schema changes ripple through your codebase. Performance debugging requires tracing through resolver chains. Your âsimpleâ API is now a complex distributed system.
Unpredictable Performance: In REST, you know /users/:id costs one database query. In GraphQL, the same queryâs cost depends entirely on what fields clients request. Youâre flying blind until monitoring reveals the damage.
Use GraphQL When
- You have multiple diverse clients (web, iOS, Android) with different data needs
- Mobile apps where round trips kill performance
- Youâre willing to accept significant backend complexity
- Your team has dedicated GraphQL expertise
- Over/under-fetching is a genuine, measured problem
Donât use GraphQL when: You have simple CRUD needs, a small team without GraphQL experience, or youâre building a public API for external developers (theyâll struggle with it).
gRPC: The Performance Specialist
gRPC is Googleâs Remote Procedure Call framework built for speed. It uses Protocol Buffers (binary format) over HTTP/2. This is not for public APIsâitâs for high-performance internal communication.
How gRPC Works
Define your service in a .proto file:
syntax = "proto3";
service UserService {
rpc GetUser (UserRequest) returns (User) {}
rpc ListUsers (ListUsersRequest) returns (stream User) {}
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
The compiler generates client and server code. Communication happens as compact binary over HTTP/2.
What gRPC Does Well
Raw Performance: Binary Protocol Buffers are tiny and parse incredibly fast. Orders of magnitude more efficient than JSON.
Bidirectional Streaming: HTTP/2 enables true streamingâclient and server send continuous data streams over one connection. Perfect for real-time systems.
Strong Contracts: The .proto file is the source of truth. Type mismatches are caught at compile time, not in production.
Code Generation: Generate client libraries automatically in any language. No manual API client writing.
Where gRPC Becomes Your Problem
Not Human-Readable: Binary payloads are indecipherable. You canât use curl to debug. You need specialized tools like grpcurl.
Browser Incompatibility: Browsers canât call gRPC services directly. You need gRPC-Web proxy translation, adding infrastructure complexity.
Learning Curve: Protocol Buffers, code generation, and gRPC concepts are less familiar than REST/JSON. Onboarding takes longer.
Tooling Gaps: REST has decades of mature tooling. gRPC tooling is improving but less comprehensive.
Use gRPC When
- Building internal microservices that need extreme performance
- Latency measured in milliseconds matters
- Youâre comfortable with binary formats and specialized tooling
- Streaming is a core requirement
- Your team is already in the Google/Cloud ecosystem
Donât use gRPC when: Youâre building a public API, need browser compatibility, or your team lacks Protocol Buffer experience.
The Decision Matrix
Stop debating ideology. Answer these questions:
| Your Requirement | Best Choice | Why |
|---|---|---|
| Public API for external developers | REST | Simplicity, universal compatibility, easy debugging |
| Multiple clients (web, mobile, desktop) with different data needs | GraphQL | Eliminates endpoint proliferation, client flexibility |
| Internal microservices with extreme performance needs | gRPC | Lowest latency, smallest payload, bidirectional streaming |
| Simple CRUD application | REST | Donât over-engineer, keep it simple |
| Mobile app where network requests are expensive | GraphQL | Minimize round trips, fetch exactly whatâs needed |
| Real-time bidirectional communication | gRPC | Streaming is a first-class feature |
| Team without specialized expertise | REST | Everyone already knows it |
Common Mistakes
Mistake 1: Choosing Based on Hype
"We should use GraphQL because Facebook uses it"
You are not Facebook. Facebook's problems aren't your problems.
Mistake 2: REST Over-engineering
Bad: /users/123/full
/users/123/minimal
/users/123/with-posts
/users/123/with-posts-and-comments
Good: /users/123?fields=name,email
/users/123?include=posts
Mistake 3: GraphQL Without DataLoader
// This will kill your database
const resolvers = {
Query: {
users: () => db.users.findAll()
},
User: {
posts: (user) => db.posts.findByUserId(user.id) // N+1 nightmare
}
}
Mistake 4: gRPC for Public APIs
"Let's use gRPC for our mobile app"
Now your mobile developers need Protocol Buffer tooling,
gRPC-Web proxies, and specialized debugging tools.
The Bottom Line
REST trades performance for simplicity and ubiquity. Use it for public APIs and simple CRUD.
GraphQL trades backend complexity for frontend flexibility. Use it when you have multiple diverse clients with complex data needs.
gRPC trades human-readability for raw performance. Use it for internal microservices where latency matters.
Your job isnât being a fanboy for one technology. Itâs understanding trade-offs and choosing the right tool for your actual requirements.
The best API is the one thatâs well-documented, performs adequately, and actually ships. Stop arguing about patterns and start solving your specific problem.
Most teams should start with REST. Only switch to GraphQL or gRPC when you have specific, measured problems that justify their complexity. âModernâ and âtrendingâ arenât problemsâtheyâre distractions.
Build the API your users need, not the one that looks impressive in your architecture diagrams.