REST, GraphQL & gRPC
REST, GraphQL, and gRPC are three dominant approaches for designing APIs between clients and services, or between services themselves. Each makes different trade-offs around flexibility, performance, type safety, and tooling. Choosing the right API style for each use case — public API, internal microservice communication, mobile client, real-time feed — is a recurring system design decision.
REST
REST (Representational State Transfer) is an architectural style built on HTTP. It treats everything as a resource identified by a URL, and uses standard HTTP methods to manipulate those resources.
Core constraints:
- Stateless: Each request contains all the information needed to process it. The server holds no client session state between requests.
- Resource-oriented: APIs are designed around nouns (resources), not verbs (actions).
GET /orders/42, notPOST /getOrder. - Uniform interface: HTTP methods have defined semantics —
GETreads,POSTcreates,PUT/PATCHupdates,DELETEremoves. Clients know what to expect from each method. - Cacheable: Responses can be cached at the HTTP layer using
Cache-Control,ETag, andLast-Modifiedheaders. CDNs, browsers, and proxies understand HTTP caching natively.
GET /api/orders/42 → fetch order 42
POST /api/orders → create a new order
PATCH /api/orders/42 → partially update order 42
DELETE /api/orders/42 → cancel order 42
GET /api/orders/42/items → fetch items for order 42
Strengths: Universal support — every language, framework, and tool speaks HTTP. Simple to understand, debug with curl, and document. HTTP caching works out of the box. Statelessness makes horizontal scaling trivial.
Weaknesses:
- Over-fetching:
GET /users/42returns the full user object even if the client only needs the name. Wasted bandwidth, especially on mobile. - Under-fetching: To display a user’s profile with their recent orders and follower count, the client may need 3 separate API calls — each a round trip.
- No strong contract: REST APIs are described by documentation (OpenAPI/Swagger), not enforced by the protocol. Breaking changes are only caught at runtime.
GraphQL
GraphQL is a query language for APIs and a runtime for executing those queries. Clients specify exactly what data they need — fields, nested relations, filters — in a single query. The server returns precisely that data, nothing more.
query {
user(id: "42") {
name
email
orders(last: 5) {
id
status
total
items {
productName
quantity
}
}
}
}
This single query fetches the user, their 5 most recent orders, and the items in each order — data that would require 3+ REST calls and significant over-fetching.
Strengths:
- No over-fetching or under-fetching: Clients get exactly the fields they request. Mobile clients can request minimal payloads; desktop clients can request richer ones — same API, different queries.
- Single endpoint: All queries go to
POST /graphql. No URL design debates. - Strongly typed schema: The schema is the contract. Client tooling (code generation, IDE autocomplete) is derived directly from the schema. Breaking changes are detectable statically.
- Introspection: Clients can query the API’s own schema at runtime. Tools like GraphiQL provide interactive documentation automatically.
Weaknesses:
- Caching is hard. HTTP caching works on URL —
GET /orders/42can be cached. GraphQL usesPOST /graphqlwith a request body, which HTTP caches don’t cache by default. Persisted queries and client-side caches (Apollo Client) are workarounds. - N+1 query problem. A query for 100 users each with their orders can trigger 101 database queries. DataLoader (batching and caching at the resolver layer) is the standard solution, but it must be implemented deliberately.
- Complexity. A GraphQL server requires a schema, resolvers, and usually a DataLoader layer. More moving parts than a REST endpoint.
- Arbitrary queries can be expensive. A malicious or poorly written client query can request deeply nested data that generates enormous database load. Query depth limiting and complexity analysis are necessary safeguards.
gRPC
gRPC is a high-performance RPC framework developed by Google. Services are defined in Protocol Buffers (protobuf) — a language-agnostic, binary serialization format. The proto definition serves as both the service contract and the source of generated client/server code.
// orders.proto
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order);
rpc CreateOrder (CreateOrderRequest) returns (Order);
rpc ListOrders (ListOrdersRequest) returns (stream Order);
rpc TrackOrder (stream TrackRequest) returns (stream TrackEvent);
}
From this definition, protoc generates type-safe client and server stubs in Go, Java, Python, C++, and other languages. Clients call generated methods as if they were local function calls; the gRPC framework handles serialization, connection management, and transport over HTTP/2.
Strengths:
- Performance. Protobuf binary encoding is 3–10x smaller than JSON and faster to serialize/deserialize. HTTP/2 multiplexes multiple RPC calls over a single connection, eliminating the connection overhead of HTTP/1.1.
- Streaming. gRPC natively supports server streaming (one request, many responses), client streaming, and bidirectional streaming — all over a single HTTP/2 connection.
- Strong contracts. The proto file is the contract. Generated code is always in sync with the server implementation. Mismatches are compile-time errors, not runtime surprises.
- Polyglot. Generate clients in any supported language from the same proto file. A Go service and a Java service communicate with type-safe generated code, no hand-written HTTP clients.
Weaknesses:
- Not browser-native. Browsers don’t support HTTP/2 framing at the level gRPC requires. gRPC-Web (with a proxy) or gRPC-Gateway (which exposes a REST/JSON API from proto definitions) are workarounds for browser clients.
- Less human-readable. Binary protobuf payloads can’t be inspected with curl.
grpcurland Postman support gRPC, but the debugging experience is more involved than REST. - Schema evolution requires care. Protobuf field numbers must never be reused. Adding optional fields is safe; removing or changing fields requires a deprecation cycle. The schema is more rigid than JSON.
Side-by-Side Comparison
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2 | HTTP/2 |
| Payload format | JSON (typically) | JSON | Protobuf (binary) |
| Contract | OpenAPI (optional) | GraphQL schema (required) | Proto file (required) |
| Type safety | Optional (via OpenAPI codegen) | Strong (schema-enforced) | Strong (compile-time) |
| Caching | Native HTTP caching | Complex; client-side | No standard caching |
| Streaming | SSE / chunked transfer | Subscriptions (WebSocket) | Native bidirectional |
| Browser support | Native | Native | Requires gRPC-Web |
| Performance | Good | Good | Excellent |
| Discoverability | Good (OpenAPI UI) | Excellent (introspection) | Moderate (grpcurl) |
When to Use Each
Use REST when:
- Building a public API consumed by third parties. REST’s ubiquity means any language and any tool can consume it with no special libraries.
- HTTP caching is important (CDN-cacheable read-heavy APIs).
- The team is small or the project is simple — REST’s lower setup cost pays off.
- Clients are browsers that need simple fetch() calls without extra tooling.
Use GraphQL when:
- Multiple client types (web, iOS, Android) need different data shapes from the same backend.
- Clients are over-fetching significantly and bandwidth or latency is a concern.
- Rapid product iteration requires frequent API shape changes — GraphQL’s schema evolution (additive changes) is more forgiving than REST versioning.
- Building a BFF layer that aggregates data from multiple backend services into client-optimized responses.
Use gRPC when:
- Internal service-to-service communication where browser access is not required.
- Performance is critical — high-throughput, low-latency microservice calls where JSON overhead matters.
- Bidirectional streaming is needed (real-time data feeds, live telemetry).
- Polyglot microservices — generated clients ensure type-safe cross-language communication without hand-written HTTP clients.
Design Considerations
- Mix styles deliberately. Most large systems use more than one API style. A common pattern: gRPC for internal service-to-service communication, REST or GraphQL at the API gateway for external clients. Each style where it fits best.
- Version REST APIs explicitly. Include the version in the URL (
/v1/,/v2/) or in a header. Unversioned REST APIs are a maintenance liability — you can’t make breaking changes without breaking clients. - GraphQL requires query guards. Implement depth limiting (max 5 levels of nesting), complexity scoring, and query timeouts. Without these, a single malicious or buggy query can bring down your backend.
- Proto backwards compatibility. Never reuse a protobuf field number, even after deleting a field. Reserve deleted numbers (
reserved 5, 6;) to prevent accidental reuse. Add new fields as optional; never change field types. - Document your API as code. OpenAPI specs for REST, proto files for gRPC, and GraphQL schemas are machine-readable contracts. Generate client SDKs, mocks, and documentation from them automatically — not manually.