API Design: REST vs GraphQL vs gRPC — How to Choose
REST, GraphQL, and gRPC each solve a different problem. This guide compares their design philosophies, trade-offs, and ideal use cases so you can make an informed decision for your next API.
Every modern application talks to other applications through APIs. The question is no longer whether to build an API, but which style of API to build. REST has been the default for over a decade. GraphQL emerged as a response to REST's inflexibility. gRPC takes a completely different approach built on HTTP/2 and protocol buffers. Choosing between them requires understanding not just the syntax, but the underlying design philosophy each one represents.
This guide breaks down each protocol from a design perspective: how you structure resources or schemas, how clients interact with your API, how you handle versioning and pagination, and how real-time requirements affect the decision. By the end, you will have a framework for choosing the right tool for your specific constraints.
REST: Resource-oriented design that the web understands
REST — Representational State Transfer — is not a protocol. It is an architectural style defined by Roy Fielding in his 2000 doctoral dissertation. The core idea is that you model your domain as resources, each identified by a URL, and you manipulate those resources through a uniform set of HTTP verbs: GET, POST, PUT, PATCH, and DELETE.
Good REST API design is resource-oriented design. You do not design endpoints around actions — you design endpoints around nouns. Instead of /createUser or /getUserById, you design POST /users and GET /users/:id. This seems like a small distinction, but it deeply affects how the API scales, how it is documented, and how clients learn to use it.
A well-designed REST endpoint for a user resource looks like this:
GET /users/42
Accept: application/json
Response 200:
{
"id": 42,
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "admin",
"createdAt": "2025-11-14T09:00:00Z"
}The beauty of REST is its simplicity. The URL identifies the resource, the HTTP method identifies the operation, and the response body contains the representation. Every HTTP client in every language already understands this contract. Caching works out of the box through HTTP headers. Tooling like OpenAPI (formerly Swagger) lets you generate documentation, client SDKs, and server stubs from a single schema file.
REST runs into problems when clients need different shapes of data. A mobile client might only need the user's name and role, while a dashboard client needs the full profile with nested organization data. With REST, you either build multiple endpoints or force every client to download the full representation and discard what it does not need. This is the over-fetching problem, and it was the primary motivation for GraphQL.
GraphQL: Let the client describe exactly what it needs
GraphQL, developed by Meta and released in 2015, takes the opposite approach. Instead of the server defining fixed responses, the client sends a query that describes exactly the data it wants, and the server returns exactly that shape. One endpoint, any response shape.
A GraphQL schema defines the types and relationships in your domain. The client composes queries against this schema, requesting only the fields it needs. The same concept of nested resources from REST becomes nested fields in a GraphQL query, but the client controls the depth and selection.
// GraphQL schema (SDL)
type User {
id: ID!
name: String!
email: String!
role: Role!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
}
enum Role { USER ADMIN MODERATOR }
type Query {
user(id: ID!): User
}A client that needs only the user's name and post titles sends a query that asks for exactly those fields:
// Client query
query {
user(id: 42) {
name
posts {
title
}
}
}
// Response
{
"data": {
"user": {
"name": "Ada Lovelace",
"posts": [
{ "title": "Notes on the Analytical Engine" }
]
}
}
}The key design principle in GraphQL is that the schema is the contract. The schema defines what is possible, and the client decides what to request from the available options. This eliminates over-fetching, reduces the number of network requests (a single query can replace multiple REST round-trips), and gives frontend teams independence from backend changes because new client requirements do not always require new endpoints.
GraphQL introduces complexity of its own. Query performance is harder to predict because the client controls what loads. The N+1 problem — where resolving nested fields triggers an independent database query for each parent record — requires batching solutions like DataLoader. Caching at the HTTP level does not work because every query hits the same POST endpoint, so application-level caching strategies are necessary. And the learning curve for resolvers, mutations, subscriptions, and input types is steeper than REST's straightforward URL-and-verb model.
GraphQL gives frontend teams independence from backend changes. But that independence comes with a responsibility to understand the performance characteristics of every query your client sends to the server.
gRPC: High-performance RPC built on HTTP/2 and protobuf
gRPC, originally developed by Google, takes yet another approach. Instead of resources and queries, gRPC models APIs as remote procedure calls — you define a service with methods, and clients call those methods as if they were local functions. The key technical difference is that gRPC uses Protocol Buffers (protobuf) for serialization instead of JSON, and HTTP/2 for transport instead of HTTP/1.1.
A protobuf service definition looks nothing like a REST endpoint or a GraphQL schema:
syntax = "proto3";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser (CreateUserRequest) returns (User);
rpc StreamUserUpdates (StreamRequest) returns (stream UserUpdate);
}
message GetUserRequest {
int32 id = 1;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
Role role = 4;
string created_at = 5;
}
enum Role { ADMIN = 0; USER = 1; }
message StreamRequest {}
message UserUpdate {
User user = 1;
string event_type = 2;
}Protobuf serializes to a compact binary format that is significantly smaller than JSON and faster to parse. Combined with HTTP/2's multiplexing — multiple streams over a single connection — gRPC achieves dramatically lower latency and higher throughput than REST or GraphQL for high-volume communication. This makes gRPC the dominant choice for microservices-to-microservices communication, where every millisecond of serialization overhead and every byte on the wire matters.
gRPC also natively supports four kinds of streaming: unary (one request, one response), server-streaming (one request, stream of responses), client-streaming (stream of requests, one response), and bidirectional streaming (both sides stream simultaneously). This makes it the strongest option for real-time APIs among the three protocols.
The trade-off is that gRPC is harder to use from browsers. Browser clients cannot access gRPC services directly because they lack fine-grained control over HTTP/2 frames. gRPC-Web exists as a workaround but adds complexity. Protobuf messages are not human-readable in transit, which makes debugging harder — you need tools like grpcurl or a reflection service to inspect traffic. And the ecosystem for API documentation is less mature than OpenAPI or GraphQL's introspection system.
How they compare across practical concerns
Choosing between REST, GraphQL, and gRPC is rarely a purely technical decision. The right choice depends on who your clients are, what kind of data they need, and the constraints of your network and infrastructure. Here is how each protocol compares across the practical concerns that matter most in production.
API versioning
REST handles versioning through the URL or the Accept header. /v1/users and /v2/users can coexist indefinitely, letting clients migrate at their own pace. This is simple and battle-tested, but it encourages duplication — the v2 endpoint often duplicates most of the v1 logic with a few changes. GraphQL avoids versioning entirely by evolving the schema. Deprecated fields are decorated with a @deprecated directive and remain in the schema until every client has migrated. This works well when you control all clients but can cause friction in public APIs where clients update slowly. gRPC's protobuf schema is forward-compatible by design — you can add fields without breaking existing clients, and deprecated fields are removed after a migration period. This is the most elegant versioning strategy of the three, but it requires discipline to never rename or remove a field without a deprecation cycle.
Pagination patterns
REST pagination is typically done with cursor-based pagination using a nextCursor field, or offset-based pagination with page and limit parameters. The design principle is that the response contains a next link or token, and the client follows it. GraphQL pagination uses the Relay Connection spec, which wraps lists in edges with cursors and page info — more verbose but more consistent than ad-hoc REST approaches. gRPC pagination is handled manually in the request and response messages, usually with a page_token and page_size field. There is no standard, but the protobuf schema makes the contract explicit.
- REST: cursor-based pagination with nextCursor in the response is the recommended pattern for production APIs
- GraphQL: the Relay Connection spec provides a standardized pagination model with hasNextPage, hasPreviousPage, and cursors
- gRPC: pagination is defined in your proto messages; a common pattern is to include page_token and page_size in the request and next_page_token in the response
Authentication and authorization
All three protocols rely on the same underlying transport security. REST typically uses Bearer tokens in the Authorization header. GraphQL follows the same pattern — since all requests go through a single POST endpoint, the token is validated at the GraphQL layer or in a middleware. gRPC uses interceptors to attach authentication metadata to each call, usually in the form of JWT tokens carried in gRPC metadata headers. None of the three protocols prescribe a specific authentication method — the choice of OAuth2, API keys, or session-based auth is orthogonal to the API style.
Rate limiting
Rate limiting is simplest with REST because each endpoint represents a bounded operation. You count requests per endpoint per client and return 429 Too Many Requests when the limit is exceeded. GraphQL makes rate limiting harder because a single query can trigger vastly different amounts of work — a query for one field might cost one database lookup, while a query with deeply nested relations might cost fifty. GraphQL APIs typically implement cost-based rate limiting, where each field has a weight and the total query cost is calculated before execution. gRPC rate limiting is similar to REST but operates at the method level — you count RPC calls per method per client, with the additional consideration that streaming calls consume resources for their duration, not just their initiation.
Real-time APIs and streaming
REST can handle real-time updates through WebSockets or Server-Sent Events (SSE), but neither is part of the REST specification — they are separate protocols bolted on alongside the HTTP API. GraphQL has subscriptions, which are a first-class part of the specification. A client subscribes to an event and receives updates over a WebSocket connection whenever the event occurs. gRPC has the strongest real-time story with its native bidirectional streaming over HTTP/2. A single gRPC call can stream data in both directions simultaneously, which is ideal for real-time dashboards, chat applications, and event-driven systems. If real-time communication is a primary requirement, gRPC's streaming is the most mature and performant option, followed by GraphQL subscriptions, followed by REST plus WebSockets.
Documentation and developer experience
REST has the strongest documentation ecosystem thanks to OpenAPI (formerly Swagger). An OpenAPI specification describes every endpoint, request parameter, response schema, and authentication method in a machine-readable YAML or JSON file. Tools like Swagger UI render this into an interactive documentation page, and code generators produce client SDKs for dozens of languages. The maturity of this ecosystem means that a new developer can go from reading an OpenAPI spec to making their first successful API call in minutes.
GraphQL has introspection — a built-in system where you query the API's schema through a special endpoint. Tools like GraphiQL and Apollo Studio Explorer let developers browse the schema, write queries with autocomplete, and see documentation inline. This is a great experience for developers who already know your API exists, but it does not help with search engine discoverability or external API marketplaces the way an OpenAPI spec does.
gRPC relies on proto files as its source of truth. The proto file is both the schema and the documentation. Tools like protoc-gen-doc generate reference documentation from proto files, and gRPC reflection lets clients discover services dynamically. The developer experience for gRPC is improving but still trails REST and GraphQL in tooling maturity, especially outside of the microservices ecosystem.
When to use each one
REST is the right choice when your API is consumed by external developers, when you need maximum interoperability, and when your clients need standard HTTP caching. It is the safest default because every language, framework, and proxy understands HTTP. If you are building a public API, start with REST.
GraphQL is the right choice when your API has multiple client types with different data requirements, when frontend teams need to move independently from backend changes, and when you want to reduce the number of network requests. It excels in consumer-facing applications with mobile and web clients that need different shapes of the same data.
gRPC is the right choice for internal microservices communication, for high-throughput systems where serialization overhead matters, and for real-time streaming applications. If you are building a system where both ends are under your control and performance is the primary concern, gRPC is the strongest option.
None of these protocols are mutually exclusive. A common production architecture uses gRPC for service-to-service communication behind the firewall, exposes a GraphQL gateway that aggregates data from multiple gRPC services, and provides a REST API for external clients. The question is not which one protocol to use everywhere — it is which protocol to use for each interface in your system. Understanding the design philosophy behind each one gives you the judgment to make that choice correctly.
