REST vs. gRPC vs. GraphQL: Choosing the Right API Contract
A practical guide to choosing REST, gRPC, or GraphQL by communication pattern rather than fashion. Compare how each style models requests, where its efficiency comes from, what it costs at scale, how public and internal boundaries can use different styles, and which design-review questions expose coupling, evolvability, payload, and operational risks.
Every API Is a Contract Between Processes
A network call crosses a boundary between two processes that do not share memory and cannot safely depend on each other’s internal types. Without an explicit agreement, one side can send a value the other cannot interpret, omit a field the other requires, or return an error the caller treats as success. The result is not merely an awkward interface: callers retry the wrong operations, make extra round trips to assemble missing data, or fail together when an implementation detail changes.
Trace the request from the client’s private types through the request contract and API boundary, then compare the service’s response shape and failure behavior with what the client can safely consume.
An API is that agreement made operational. It specifies what a caller may ask for, what shape comes back, and how failure is represented. The contract is deliberately narrower than either process: the client should not need the service’s database model, and the service should not need the client’s object types. Azure’s guidance describes this boundary as one where clients can call the web API regardless of the internal implementation and where the client and web service can evolve independently.
The pressure on that contract depends on the boundary. A public or partner-facing API must be discoverable by clients you do not control. A browser client values universal access and a payload that fits a screen. A service-to-service call may instead prioritize strict schemas, efficient payloads, or streaming. A read-heavy screen may need several related pieces of data in one interaction; a mutation-heavy workflow cares more about precise commands and failure handling. The same interface choice can therefore be convenient at one boundary and expensive at another.
That is why API design is not a contest to find one universally superior style. You choose the contract that matches who calls it, how much control those callers have, and what communication pattern the boundary must sustain. The job of an API style is to make that cross-process agreement explicit while keeping the resulting coordination cost under control.
REST: Model Nouns, Operate with HTTP Verbs
REST is an architectural style for a stateless, loosely coupled interface between a client and a service. It models the domain as named resources: a user, an order, or a collection of orders. Each resource has a URI, and the client uses standard HTTP verbs to operate on that URI. The response carries a representation of the resource—often JSON—and an HTTP status code that communicates the result.
The resource-and-verb path
Start by naming the business entity, not the action. /orders identifies the collection; /orders/99 identifies one order. The collection and the individual order are separate resources, so each has its own URI. The verb supplies the operation: clients use standard HTTP verbs such as GET, POST, PUT, PATCH, and DELETE to operate on resources. This uniform interface lets clients remain independent of the service's internal implementation.
GET /users/42
GET /orders/99
POST /orders
PATCH /orders/99
DELETE /orders/99The important discipline is that the URI stays noun-shaped while the verb changes. A client can use the same contract from a browser, curl, a mobile application, or a partner's backend; it does not need a service-specific calling library. Requests are independent and can arrive in any order, so the server does not need to retain transient client state between them. That makes the boundary easier to scale out, document, and consume across platforms.
Read the resource URI as the noun and each outgoing HTTP verb as the operation selected for that resource.
Where the simplicity starts to hurt
The same fixed resource shape that makes a public API predictable can force a client to make several calls for one screen. Twitter's v1 public REST API was straightforward to explore in a browser, yet mobile clients could become notoriously chatty: assembling one screen might require multiple sequential requests for related resources. REST does not prevent that design, but each extra round trip adds another dependency and another opportunity for partial failure. Once an external API is live, changing a resource's shape also requires care: a breaking change can force consumers to migrate, so teams commonly preserve compatibility through additive changes or an explicitly managed version boundary.
CHECK YOUR UNDERSTANDING
What makes `/orders/99` more REST-like than `/create-order`?
SHOW ANSWERHIDE ANSWER
/orders/99 names the order resource, while the HTTP verb selects the operation. /create-order puts an action in the URI and weakens the uniform resource-and-verb interface.
gRPC: Call Typed Procedures Over HTTP/2
RPC starts with a procedure, not a resource. You define operations such as GetUser and PlaceOrder, along with the request and response types for each operation. The client then calls a generated local stub; the stub serializes the arguments, sends the remote call, deserializes the response, and returns it through the client API. The network is hidden behind a function-call-shaped interface, but it has not disappeared: the call can fail, exceed its deadline, or return a status that the caller must handle.
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
// Generated client call:
stub.SayHello(HelloRequest);The contract is a .proto service definition. Its rpc declarations name the callable procedures, while the message fields carry numeric tags such as greeting = 1. Both endpoints need the same schema to generate compatible client and server code. That shared schema is tighter coupling than a generic JSON endpoint, but it catches mismatched types and method signatures before requests reach production.
A unary call follows a concrete sequence: the client invokes the stub; the server receives the method name, metadata, and any deadline; the server decodes the Protocol Buffers request and runs the method; it encodes a response and returns status details; the client completes when it receives a successful response. A client can also set a deadline, after which the RPC terminates with DEADLINE_EXCEEDED. Treat that boundary as part of the contract: a server may finish successfully while the client has already timed out.
Protocol Buffers provide gRPC's binary, schema-first wire format. Compared with REST plus JSON, this reduces serialization overhead and produces payloads that are significantly smaller and faster to parse, which matters when services exchange many messages. gRPC also runs over HTTP/2: one connection can carry concurrent RPCs through multiplexing, and streaming methods can carry an ordered sequence of messages. In bidirectional streaming, the client and server read and write independently, so a long-lived connection can support ongoing updates rather than repeated polling calls.
Follow the shared .proto contract to both endpoints, then trace the single HTTP/2 connection carrying concurrent GetUser and PlaceOrder calls alongside the bidirectional stream.
The boundary costs determine where this contract fits. Browsers do not call gRPC natively; browser clients need a proxy layer such as grpc-web. Plain curl is also a less natural inspection tool than it is for an HTTP resource endpoint. Google uses gRPC internally, and it is used in service-to-service meshes such as Lyft and Netflix, where binary payloads, generated clients, multiplexing, and streaming justify the tighter contract. For a low-volume or one-off call, those benefits may not repay schema-generation work and browser-integration constraints.
CHECK YOUR UNDERSTANDING
You are building an internal microservice that streams real-time GPS coordinates from many drivers to a matching engine. Which API style would you reach for first, and which specific properties make it the right fit?
SHOW ANSWERHIDE ANSWER
Reach for gRPC first. A bidirectional or server-streaming RPC can keep an HTTP/2 connection open for ordered coordinate messages, while HTTP/2 multiplexing carries other concurrent calls on the same connection. Protocol Buffers provide a typed binary contract, and generated clients reduce hand-written serialization and interface drift. You would still set deadlines for calls that must complete, and verify that the operational cost of the shared schema is acceptable.
GraphQL: Let the Client Declare the Data Shape
GraphQL changes the unit of a read from a server-defined resource response to a client-defined selection set. The server exposes one GraphQL endpoint; the client sends a query document naming the fields and nested relations it wants. The response mirrors that selection, so the client receives the requested shape rather than an entire resource.
How a query is resolved
- The client builds a query document against the server's typed schema. A read uses a
query; a write uses amutation; event-based or streaming updates use asubscription. - The client sends the document to the GraphQL endpoint. GraphQL requests use a data format describing the objects and fields that should be returned, and internally each request is sent as an HTTP
POST. - The server validates the document against its schema. The schema defines object types, the fields on those objects, and resolver functions that perform the operation for each field.
- The server resolves the selected root field, follows the requested nested relations, and returns a response containing the selected data. Fields omitted from the document are omitted from the response.
That last step is the practical difference from fixed-structure resource responses. Suppose a screen needs a person's phone number and last purchase. A resource-oriented API may require one request for the person and another for the purchase; GraphQL can express both selections in one query and one request/response exchange. Conversely, if the screen needs only the phone number, the client can select only that field instead of receiving unrelated attributes such as a person's name, date of birth, address, and phone number. This attacks both under-fetching and over-fetching at the contract boundary.
The field graph is therefore assembled per request: a repository root can lead to its open pull requests, each pull request can lead to reviewers, and each pull request can lead to CI status. One client query can select that connected shape instead of making separate resource calls for every hop. The trade is that the server must execute every selected field and relation and apply its own operational controls; the endpoint is singular, but the work behind it can fan out across many resolvers.
Trace the nested selection set from the single endpoint into repository, pull-request, reviewer, and CI-status resolution; the branching is the field graph that replaces separate resource calls.
Worked Example: Rebuilding a Repository Screen
Consider a repository screen that shows the repository itself, its open pull requests, each pull request’s reviewers, and its CI status. GitHub’s v4 API moved from REST to GraphQL for this kind of client-shaped request. The comparison is not “old technology versus new technology”; it is client-side fan-out versus one nested selection.
Compare the five-to-six sequential REST hops with the single GraphQL query that carries the repository, pull requests, reviewers, and CI-status selection.
With REST, the client assembles the screen across 5–6 calls: one call for the repository, calls for the open pull requests, calls for reviewers, and calls for CI status. The exact fan-out depends on how the API exposes those relationships, but this comparison uses five to six calls for the screen. With GraphQL, the client sends one query containing the nested field graph for all four parts. The request-count arithmetic is explicit: 5 ÷ 1 = 5 and 6 ÷ 1 = 6. The GraphQL version uses one request instead of five to six, so it uses roughly 5–6× as many REST requests in the comparison, or about 80–83% fewer requests with GraphQL.
That ratio describes request count, not end-to-end latency or total backend work. A single GraphQL request can still trigger several resolver calls or database queries on the server; it has removed the client’s sequential fan-out, not made the underlying data free. Conversely, the REST client may issue some calls concurrently rather than strictly one after another. The useful result is that the client can ask for the repository, open pull requests, reviewers, and CI status in the shape the screen needs, while avoiding unrelated fields and separate requests for related data. GraphQL’s queries can return the exact data in only one API request and response exchange.
The design is especially useful when multiple surfaces have wildly different data needs. Instead of creating a new REST aggregation endpoint for every screen—or teaching each client another sequence of resource calls—the client declares the shape it needs. That improves payload efficiency and removes under-fetching at the client boundary, but it does not remove the need for resolver efficiency, query limits, or server-side observability.
CHECK YOUR UNDERSTANDING
A mobile app makes six REST calls to render its home screen. What are two ways to improve it, and what new problems does each introduce?
SHOW ANSWERHIDE ANSWER
You could add a REST aggregation endpoint that assembles the home-screen data server-side, reducing the client’s six calls to one; the server then owns the fan-out and must manage its latency, failures, and payload shape. Or you could expose GraphQL so the client requests the required fields and nested data in one query; this reduces over-fetching and under-fetching, but introduces query-cost control, depth limits, resolver-level batching, and more complex caching and observability.
Choose the Contract That Matches the Boundary
The boundary determines which contract is cheapest to live with. A public API must be understandable by clients you do not control; an internal call can trade that universality for generated code, stricter schemas, and efficient transport. A mobile client may need a third shape again: one screen assembled from several backend domains without making six sequential requests.
| Style | Contract shape | Best-fit boundary | Efficiency/call behavior | Main price |
|---|---|---|---|---|
| REST | Resource URIs with standard HTTP verbs such as GET, POST, PUT, PATCH, and DELETE | Platform-independent HTTP clients; clear documentation and familiar formats such as JSON or XML | Fixed resource responses can cause overfetching; related data may require multiple requests | Clients and services can evolve independently only when they agree on exchanged data formats |
| gRPC | Typed remote methods defined in a .proto service definition; generated client and server code | Service-to-service calls where typed methods or streaming are useful | Protocol Buffer request and response messages; unary, server-streaming, client-streaming, and bidirectional-streaming RPCs | Both sides depend on the service definition; browser clients need a proxy layer and plain HTTP inspection is less natural |
| GraphQL | A single endpoint where the client specifies the requested data structure; query, mutation, and subscription operations | Clients that need flexible resource data from a server-side schema | Queries return only the fields specified by the client in one API request and response exchange; caching remains possible | Server-side resolver functions and caching require deliberate operational design |
Read the Main price column as an operating constraint, not a footnote. REST keeps entry friction low because platform-independent HTTP clients, familiar formats, and clear documentation are enough. Its fixed resource responses can return fields the client does not need, while related resources may require more requests. gRPC moves complexity into the shared .proto service definition and generated client/server code; that coupling catches interface drift early, but both sides must coordinate schema evolution. GraphQL moves flexibility to the client, while the server must deliberately operate resolvers and caching for arbitrary query shapes.
A mixed boundary is usually the practical answer
A ride-share system should not force one style across every edge. The customer-facing mobile app can call a GraphQL gateway for driver location, surge, and ETA in one client-shaped query. The driver-location service can use a gRPC stream to send location updates to the matching service, where streaming and typed service-to-service calls matter more than browser accessibility. A public developer API can expose REST, with resource-shaped operations and documentation that partner teams can use with ordinary HTTP tooling.
The comparison that is easiest to get wrong is GraphQL versus gRPC. Both can be typed and efficient in the right design, but they optimize different boundaries: GraphQL lets each client select its response shape, while gRPC gives cooperating services a procedure contract and generated clients. Choose GraphQL when different surfaces genuinely need different combinations of data and reducing client round trips is the constraint. Flip to gRPC when the callers are controlled services, call volume is high, or streaming and bidirectional communication are central. Choose REST when reach, discoverability, and a stable resource model outweigh those specialized benefits.
The style does not remove API-design work. Decide how clients add fields without breaking: additive JSON for REST, field-number-stable Protocol Buffer evolution for gRPC, and deprecation directives for GraphQL. Also decide how you will observe the contract: REST maps naturally to HTTP access logs, whereas gRPC needs per-method instrumentation and GraphQL needs per-operation instrumentation. Write the breaking-change policy before external consumers arrive; changing it after adoption means negotiating with every existing client.
CHECK YOUR UNDERSTANDING
A colleague says, “Just use REST; it is simpler” for a high-volume internal service between two Go microservices. How would you push back, and what trade-offs would you acknowledge?
SHOW ANSWERHIDE ANSWER
I would first ask whether the call pattern is high-volume, latency-sensitive, or streaming. If it is, gRPC is a stronger starting point because a shared .proto schema provides typed procedures and generated clients, while Protocol Buffers and HTTP/2 support efficient messages and concurrent or streaming calls. The trade-off is tighter coupling: both services must coordinate schema evolution, and the interface is less convenient for browser clients or plain HTTP inspection. REST remains reasonable when universal tooling, independent evolution, or a stable resource model matters more than those internal efficiency and streaming requirements.
Failure Mode: Chattiness and Breaking Contracts
A resource-shaped API can become expensive when one screen crosses several resource boundaries. A feed request may first fetch /posts, then fetch /users/{id} and /likes/{id} for every post. The client has turned one screen into a fan-out whose number of round trips grows with the number of posts; each extra round trip adds dispatch, serialization, network, and backend work.
The sequence below makes the amplification explicit. The initial feed request is not the expensive part by itself: the loop is. The client cannot finish assembling the response until those per-post calls complete, so latency and failure probability accumulate across the chain.
feed = GET /posts
for post in feed:
user = GET /users/{id}
likes = GET /likes/{id}
return feed
# Additive change: preserve old clients
response = GET /v1/users
response preserves existing fields and adds a new field
old clients continue using existing fields
# Breaking rename: old clients are incompatible
response = GET /v1/users
response renames an existing field
old clients still read the previous field name and breakThe second half shows a different kind of failure. Adding new_field while preserving existing fields lets old clients continue using the response. Renaming old_field to new_field under the same /v1/ contract does not: clients compiled against the old field still make valid requests, but their response parsing or business logic no longer finds the data it expects.
Trace the two failure paths: the feed branches into per-post calls, while the old /v1/ client reaches a response whose renamed field it cannot read.
Public APIs make that compatibility cost persistent. Common REST strategies include separate URL paths such as /v1/users and /v2/users, a custom version header, an Accept media type, or a query parameter. None is a universal protocol-level answer: changing the path affects routing and cache keys, while header- and media-type-based schemes have different trade-offs for caching, routing, and client ergonomics. Once external consumers depend on a response, a breaking rename forces coordinated client changes or a compatibility period; a version label alone does not make the change safe.
Choose the compatibility rule before the first external consumer arrives. Define which changes are additive, which are breaking, how long an old representation remains available, and how clients learn that a version is retiring. Apply the same discipline to whichever style you use: a REST path, a typed procedure, or a client-declared schema. Retrofitting the policy after dependent clients exist turns a field change into a migration across organizations, release schedules, and cached traffic.
CHECK YOUR UNDERSTANDING
Your company launched a public REST API at `/v1/`. Six months later, you need to rename a field. What are your options, and what breaks if you simply rename it?
SHOW ANSWERHIDE ANSWER
Prefer an additive change: keep the old field and add the new one, then give clients a migration and a retirement policy. If the rename must be breaking, expose a new representation such as /v2/, use an explicitly chosen header or media-type version, or maintain a compatibility layer while clients migrate. Simply renaming the field under /v1/ leaves old clients requesting a valid endpoint but unable to find the field they read, so their parsing or application logic breaks.
Failure Mode: Moving the Fan-Out Into the Server
GraphQL removes client-side fan-out, but it does not remove fan-out from the system. A query can ask for a repository, its pull requests, each pull request’s reviewers, and each reviewer’s profile. The server walks that nested shape through field resolvers, then issues reads to the underlying services or database. One network request can therefore become many backend operations—and the client no longer sees the multiplication clearly.
The classic failure is resolver-level N+1. Suppose a resolver first loads a user’s friends, receives 50 friend IDs, and then loads each friend separately. The request creates 1 + 1 + 50 round-trips for a single screen: one user read, one friends-list read, and 50 friend reads. That is 52 backend operations instead of one batched operation—roughly 52 times the operation count. Under load, the operator sees backend read throughput and database connection usage rise with the number of nested items, while GraphQL request latency follows the slowest or most contended resolver path.
Follow the query through the guard into the resolver layer: one accepted request fans out into repeated backend reads, while an excessive query is rejected before reaching the resolvers.
Batch at the resolver boundary with DataLoader or equivalent batching. Use request-scoped batching so nested IDs are fetched together rather than with one backend read per nested object. This preserves GraphQL’s one-query response shape without issuing one database read per nested object. The trade-off is that batching adds request-scoped coordination and does not make an arbitrarily deep query cheap; a query can still traverse too many objects or invoke expensive fields.
Treat the query document as an input that needs admission control. Run query-cost analysis before execution, assign higher cost to expensive or multiplying fields, and reject requests above a configured cost budget. Enforce a maximum depth as a separate guard: depth limits stop recursive or deeply nested traversals even when each individual field looks inexpensive. These controls protect resolvers at the price of rejecting some legitimate complex queries.
Caching also changes shape. HTTP caching is naturally organized around REST URLs and methods; a single GraphQL endpoint can carry many different query documents, variables, and authorization contexts. Caching becomes more application-specific because one endpoint can represent many requested shapes and execution contexts. This is more application-specific than endpoint caching, so a cache hit is no longer an automatic property of the URL alone.
CHECK YOUR UNDERSTANDING
A GraphQL query is one HTTP request but contains a list field with 50 nested objects. What two controls and what resolver technique should you apply before accepting it at scale?
SHOW ANSWERHIDE ANSWER
Apply query-cost analysis and a maximum-depth limit before execution, then use DataLoader-style request-scoped batching so nested IDs are fetched together rather than with one backend read per object.
Defend the Choice in a Design Conversation
A strong design answer starts at the boundary, not with a favorite technology: who calls whom, what shape of data crosses the boundary, how often, and whether the client or server should control that shape. Then name the contract you would expose, the alternative you rejected, and the cost you are accepting. You are choosing a fit for each communication pattern—not declaring REST, gRPC, or GraphQL the universal winner.
Use a boundary-by-boundary decision sequence
- For a public, partner-facing, or browser-consumed API over a stable resource domain, start with REST. Resources, standard HTTP verbs, and familiar clients lower the barrier to adoption. RESTful APIs use HTTP as a standard protocol and can be called regardless of the client's internal implementation.
- For high-volume internal service calls, especially streaming or bidirectional communication, start with gRPC. The shared typed contract and generated clients make drift visible; streaming matches communication that does not fit one request and one response. gRPC service definitions specify remotely callable methods with parameters and return types, and its bidirectional streams let both sides read and write independently.
- For mobile or multi-surface clients with materially different data needs, start with GraphQL. Let each client select the fields and nested data it needs through one query rather than forcing every surface to consume the same resource representation. GraphQL operates over a single endpoint and its schema defines the types and fields clients can access.
- State the rejected alternative and its failure mode. REST may create chatty clients for a composite screen; GraphQL adds query-cost, resolver, and cache controls; gRPC raises the cost of exposing a browser- and partner-friendly contract.
Close by making evolution part of the design, not a future cleanup task. Before external consumers exist, document which changes are additive, what counts as breaking, how long old contracts remain supported, and how clients learn about a replacement. REST commonly evolves through additive JSON fields or an intentional versioning policy; gRPC requires deliberate Protocol Buffer field evolution; GraphQL uses deprecation directives. The exact mechanism differs, but an undocumented breaking-change policy turns every consumer into an emergency migration.
The question that exposes operational experience
Ask: “What happens when this contract is slow, overloaded, or changed?” A design that has been operated can name the per-operation metrics and failure signals, the timeout or deadline behavior, the query or stream limits, and the client migration path. REST maps naturally to HTTP access logs; gRPC and GraphQL need instrumentation that identifies the method or operation rather than treating every request as an undifferentiated endpoint. If the answer only compares serialization formats, it has not covered the production contract.
CHECK YOUR UNDERSTANDING
A new ride-share system has a public developer API, a mobile home screen with driver location and ETA, and a location stream into matching. What would you choose at each boundary, and what trade-off would you state?
SHOW ANSWERHIDE ANSWER
Choose REST for the public developer API because its resource-shaped, HTTP-based contract is broadly consumable; GraphQL for the mobile screen because the client can request the combined shape it needs; and gRPC streaming for location-to-matching communication because the internal boundary needs an ongoing stream. State the costs as well: REST may require deliberate evolution, GraphQL needs query and resolver protection, and gRPC couples the services to a shared typed contract.
KEY TAKEAWAYS
- REST fits public and partner-facing boundaries where resource-shaped operations, discoverability, and universal HTTP tooling matter.
- gRPC fits controlled service-to-service calls where typed procedures, efficient messages, multiplexing, or streaming justify tighter schema coupling.
- GraphQL fits clients with materially different data needs because each client can select its response shape in one query.
- GraphQL removes client-side fan-out, not backend work; resolver batching, query-cost analysis, depth limits, and observability remain necessary.
- Choose the contract per boundary, and define compatibility, failure, and operational policies before consumers depend on it.
SOURCES
- Web API Design Best Practices - Azure Architecture Center (opens in a new tab)
learn.microsoft.com · claytonsiemens77 · May 8, 2025 · Accessed 10 Aug 2026
- Core concepts, architecture and lifecycle (opens in a new tab)
grpc.io · Accessed 10 Aug 2026
- GraphQL vs REST API - Difference Between API Design Architectures - AWS (opens in a new tab)
aws.amazon.com · Amazon Web Services · Accessed 10 Aug 2026
- REST API Disadvantages: 8 Trade-offs Every Engineering Team Eventually Hits (opens in a new tab)
www.moesif.com · Derric Gilling · 2019-05-27T00:00:00+00:00 · Accessed 10 Aug 2026