Kunal Dubey
All field notes

Field note

REST vs GraphQL vs gRPC: Choosing the Right API Boundary

Teams often frame REST, GraphQL, and gRPC as competing technologies. That framing leads to the wrong first question: “Which one is best?” The useful question is: “Which contract makes this interaction easy to evolve, operate, and understand for its consumers?”

These options make different trade-offs. REST centers a public interface on resources and HTTP semantics. GraphQL lets a client describe the shape of the data it needs. gRPC defines typed remote procedures, commonly using Protocol Buffers and HTTP/2. They can all carry business operations, and none removes the need to design those operations well.

At staff level, the choice is less about syntax and more about boundaries: who owns the contract, how many kinds of clients depend on it, what failure modes matter, and what capabilities the organization can support over time.

Start with the interaction

Consider a product page that needs a product, its price, availability, and a small set of recommendations. A mobile client may need a compact response; an internal fulfillment service may need a strongly typed operation to reserve inventory; a partner may need a stable, cacheable catalog interface.

Those are three different interactions. They need not share one API style simply because they touch the same product data. An API boundary should reflect the consumer and its job, while domain ownership remains clear behind that boundary.

Before selecting a style, write down:

  • The consumers: browsers, mobile apps, partner systems, internal services, or event processors.
  • The interaction: read, command, streaming exchange, or long-running workflow.
  • The change pattern: who adds fields or operations, and how quickly clients can upgrade.
  • The operating constraints: latency, payload size, caching, network intermediaries, and observability.
  • The ownership boundary: which team is accountable for behavior, availability, and compatibility.

Without this context, comparisons tend to collapse into slogans: REST is simple, GraphQL is flexible, or gRPC is fast. Each slogan is incomplete.

REST: a durable resource interface

REST works well when the domain can be expressed as resources and the interaction benefits from HTTP’s standard behavior. A catalog API might expose:

GET /products/42

and return a representation of product 42. Updates and actions can be modeled through resource state and HTTP methods, or through explicit subresources when an operation has its own lifecycle:

POST /orders/ord_123/cancellations

This interface gives teams familiar tools: status codes, headers, conditional requests, content negotiation, and common proxy and client support. Cacheable reads can use HTTP caching semantics. Requests are straightforward to inspect with ordinary HTTP tooling, which is useful across organizational boundaries.

REST’s apparent simplicity does not make resource modeling automatic. A poorly modeled API becomes a set of ad hoc endpoints whose semantics clients have to guess. Teams still need to define pagination, filtering, idempotency, error formats, concurrency behavior, and compatibility rules. A PATCH request, for example, is not self-explanatory unless the patch format and omitted-field behavior are specified.

REST can also create over-fetching or under-fetching for screens assembled from many resources. A client might need several round trips, or receive fields it does not use. That can be addressed with purpose-built read models, aggregation at a backend-for-frontend (BFF), or carefully designed representations. Adding arbitrary query parameters to every endpoint may solve an immediate issue while creating a hard-to-govern query language by accident.

Choose REST when the interface is naturally resource-oriented, broad HTTP interoperability matters, and standard caching and operational tooling are valuable. It is often a strong default for public APIs and partner integrations, provided the contract is designed and governed deliberately.

GraphQL: client-shaped reads over a schema

GraphQL lets a client request a selected shape from a schema. A product-page query could ask for the product fields, price, availability, and recommendations in one operation. This can reduce client round trips and let different clients request different fields without requiring a separate endpoint for every view.

That flexibility is especially useful when a platform serves many independently shipped clients, such as web, mobile, and partner experiences, and their data needs differ. A schema can provide a discoverable contract, and additive schema evolution can let clients adopt new fields on their own schedules.

But GraphQL moves complexity; it does not erase it. A single query may fan out across several services, so request count alone says little about its cost. Nested fields can trigger N+1 database or network calls unless resolvers batch and share work. Deep or broad queries can consume unbounded CPU and memory. Production systems need query limits, depth or complexity controls, timeouts, sensible pagination, and a plan for persisted or allowlisted operations when the client population is known.

HTTP caching can be less direct when many operations are sent to one endpoint with query text and variables in the request body. Persisted queries and cache-aware operation design can help, but they require shared conventions. Authorization needs field-level thought: hiding a field in the UI is not access control, and a schema that exposes data must enforce policy on every relevant path. Observability also needs to identify the operation and its resolver or downstream costs without logging sensitive query variables.

GraphQL schemas are shared products. If each team adds fields without clear ownership, deprecation practice, and resolver budgets, the graph becomes a coupling surface. Schema federation can distribute ownership, but it adds composition, rollout, and runtime concerns; it is an organizational architecture, not a free scaling feature.

Choose GraphQL when many clients need materially different read shapes and the organization is ready to operate a governed graph. It is particularly effective as an experience-facing aggregation layer. It is not automatically the best interface for every write, workflow, or service-to-service call.

gRPC: typed operations between services

gRPC defines services and methods in Protocol Buffers and generates client and server code from the contract. A fulfillment service might expose an operation such as ReserveInventory. The contract is explicit, typed, and suited to service-to-service communication where both sides can use generated clients and coordinate schema evolution.

Unary calls are familiar request-response interactions; gRPC also supports client, server, and bidirectional streaming. Streaming is useful for workloads such as continuous updates or long-lived exchanges where repeatedly polling a resource would be awkward. Protobuf’s compact binary encoding and HTTP/2 transport can make gRPC efficient, especially for high-volume internal traffic, though the actual result depends on payloads, network conditions, implementation, and workload. Measure before claiming a performance win.

The trade-offs show up at the edges. Browser clients generally need a gRPC-Web proxy or a different public-facing interface. Binary payloads are less convenient to inspect manually than JSON. HTTP/2 support, load balancing, deadlines, cancellation, retries, and health checks need to be handled consistently across the infrastructure. Generated code improves consistency, but it does not make a breaking schema change safe: field numbers and meanings must remain compatible, and clients and servers still roll out at different times.

Retries deserve particular care. A transport failure does not prove that the server did not perform the operation. Commands need clear idempotency semantics, deadlines, and error classification; retrying every failed call can duplicate effects or amplify an outage. Streaming calls add lifecycle and backpressure concerns that ordinary unary-call dashboards may not reveal.

Choose gRPC when typed service contracts, efficient internal communication, or streaming are important and the teams share enough platform support to use it consistently. For a public browser-facing API, consider whether the client ecosystem and network path make it a practical fit before adopting it as the only interface.

The decision is about the boundary, not the brand

Concern REST GraphQL gRPC
Contract shape Resources and HTTP semantics Schema and client-selected fields Typed services and methods
Strong fit Public and partner APIs; resource lifecycles Aggregating varied client reads Internal service calls; streaming
Client flexibility Representations designed by the server High flexibility per operation Generated clients follow service methods
Caching Natural HTTP cache support for suitable reads Requires deliberate operation and cache strategy Usually handled within service infrastructure
Main operational risk Inconsistent endpoint semantics Expensive or unbounded query execution Retry, deadline, and infrastructure complexity
Common evolution risk Breaking representation or behavior Unowned schema and resolver coupling Incompatible schema or rollout assumptions

The table describes typical tendencies, not hard limits. REST can serve internal systems; GraphQL can support writes; gRPC can be exposed through gateways. The right question is whether the interface’s costs match the consumers and the team’s operating model.

A practical selection sequence

Start from the consumer contract, then choose the smallest set of API styles that serves it well.

  1. If consumers need stable resources, broad HTTP reach, and useful intermediary caching, design a REST interface.
  2. If several independently evolving clients repeatedly need different combinations of the same domain data, consider GraphQL as a governed aggregation layer.
  3. If services need typed operations, efficient internal transport, or streaming, consider gRPC where the platform can support it.
  4. If different consumers have different needs, use more than one boundary over the same domain rather than forcing one protocol everywhere.

The fourth option is common in mature systems: public REST for partners, GraphQL for product experiences, and gRPC between selected backend services. It only works when each boundary has a clear owner and purpose. Multiple interfaces multiply operational and compatibility work; they are not free adapters.

Avoid choosing a protocol to compensate for a missing domain model or unclear ownership. GraphQL will not repair a service boundary that nobody owns. gRPC will not make a synchronous dependency reliable by itself. REST will not make a workflow understandable if its state transitions are hidden in undocumented endpoint behavior.

Design for change and failure

Whichever style you choose, the staff-level work is to make change safe:

  • Define compatibility rules and review them as part of the contract, not after clients break.
  • Make command semantics explicit: idempotency, concurrency, authorization, and valid state transitions.
  • Set budgets for latency, payload size, fan-out, and query or resolver cost.
  • Instrument user-visible operations and downstream work with consistent correlation and error data.
  • Establish ownership for schemas, generated artifacts, deprecations, and incident response.
  • Test gradual rollout paths where clients and servers run different versions at the same time.

These practices matter more than the protocol’s marketing category. An API is a long-lived dependency, and its quality is visible in how safely teams can change it under real production conditions.

Closing perspective

REST, GraphQL, and gRPC solve different interface problems. REST makes good use of resource semantics and the web’s shared infrastructure. GraphQL gives data-hungry clients control over read shapes. gRPC makes typed service calls and streaming first-class.

Pick based on who calls the interface, what they need to do, and what your teams can reliably operate. Keep the number of boundaries intentional, give each one an owner, and judge the design by how it behaves through growth, partial failure, and change—not by whether the protocol is fashionable.