REST vs GraphQL vs gRPC: How to Actually Pick an API Style

Most REST vs GraphQL vs gRPC posts list features in a table and stop. The real decision isn't about features. It's about who consumes your API and what hurts at scale.

Tech Talk News Editorial11 min read
ShareXLinkedInRedditEmail
REST vs GraphQL vs gRPC: How to Actually Pick an API Style

I've watched the same meeting happen at three different companies. A team is about to build a new service, and someone asks the question that eats the next ninety minutes: REST, GraphQL, or gRPC? The whiteboard fills up with a feature table. One column says “over-fetching,” another says “strongly typed,” a third says “streaming.” Everyone nods. Nobody decides anything. The table told them what each option is, which is the least useful thing to know.

Here's the part the table never captures. The right API style isn't a property of your data. It's a property of who's on the other end of the wire, and what's going to hurt when you have a hundred times more traffic than you do today. Those two questions decide almost everything. The rest is detail.

Summary

REST is the boring default and it's right most of the time. GraphQL earns its complexity when one backend feeds many demanding clients with different data needs. gRPC is for high-throughput internal traffic between services you control. Pick by your consumer, not by the feature list. And almost nobody needs all three.

Quick framing before we go deep. These three aren't really competing for the same job, even though every blog post lines them up like they are. REST and GraphQL both live on the public edge, where browsers and mobile apps and third parties talk to you. gRPC mostly lives in the basement, where your own services talk to each other. Once you see that split, half the “versus” framing dissolves.

The same request, three ways

Let's make it concrete. Say you want a user and the titles of their posts. Nothing exotic. This one request shows you the whole personality of each style.

With REST, you talk in resources. A user is a resource, posts are a resource, and you fetch them by hitting URLs that name them.[1] The catch is that the classic version takes more than one round trip:

REST: resource-oriented, multiple round tripshttp
# Round trip 1: get the user
GET /users/42
200 OK
{ "id": 42, "name": "Ada", "email": "ada@example.com",
  "createdAt": "2021-03-01", "plan": "pro" }

# Round trip 2: get that user's posts
GET /users/42/posts
200 OK
[ { "id": 7, "title": "On compilers", "body": "...", "views": 9100 },
  { "id": 9, "title": "Why I like Rust", "body": "...", "views": 220 } ]
Two requests, and each one hands back fields you didn't ask for. You wanted name and post titles. You got email, plan, post bodies, and view counts too.

That extra baggage is over-fetching: the endpoint returns more than the screen needs. The flip side, where one endpoint doesn't give you enough and you have to call a second, is under-fetching. Both are baked into REST's model, because the server decides the shape of each resource ahead of time and every client gets the same shape.

GraphQL flips that. There's one endpoint, and the client sends a query describing exactly the fields it wants.[2] The server hands back that shape and nothing else:

GraphQL: client asks for exactly what it needs, one round tripgraphql
# Single POST to /graphql
query {
  user(id: 42) {
    name
    posts {
      title
    }
  }
}

# Response: precisely the shape you asked for
{
  "data": {
    "user": {
      "name": "Ada",
      "posts": [ { "title": "On compilers" }, { "title": "Why I like Rust" } ]
    }
  }
}
One request, no wasted fields. The client controls the shape. That's the whole pitch, and on a slow mobile connection it's a real win.

gRPC doesn't play on this field at all. You don't hand-write URLs or queries. You define a contract in a .proto file, the schema language for Protocol Buffers, and a code generator turns it into typed client and server stubs in whatever language you use.[3] Calling a method feels like calling a local function:

user.proto: a strongly-typed contract, compiled to both endsprotobuf
syntax = "proto3";

service UserService {
  rpc GetUserWithPosts(UserRequest) returns (UserReply);
}

message UserRequest {
  int32 id = 1;
}

message UserReply {
  int32 id = 1;
  string name = 2;
  repeated Post posts = 3;
}

message Post {
  int32 id = 1;
  string title = 2;
}
The contract is the source of truth. Both sides generate code from it, so a field-name typo is a compile error, not a 2am pager alert. The wire format is binary, not JSON.

Three answers to one question, and they're shaped by three different worldviews. REST thinks in nouns you fetch. GraphQL thinks in a graph you query. gRPC thinks in functions you call. Hold onto that, because it's the thread through everything below.

REST: the boring default, and why boring wins

REST is resource-oriented design over plain HTTP. You model your domain as resources, give each a URL, and use the HTTP verbs (GET, POST, PUT, DELETE) to act on them.[1]It's been the default since the 2010s, and that's not an accident of fashion. It's the right call for a real reason: it rides on the web's own machinery instead of fighting it.

The biggest thing REST gets for free is caching. Because a GET /users/42 is just an HTTP GET with a URL, every layer of the web already knows how to cache it: the browser, the CDN, a reverse proxy, all of it, using standard HTTP cache headers.[4] You don't build anything. The infrastructure that already exists does the work. That single property quietly carries an enormous amount of real-world traffic, and it's the thing people forget to value until they give it up.

REST also has the lowest floor of the three. Anyone can hit an endpoint with curlor a browser address bar and read JSON. New engineers understand it on day one. Third parties integrate without a special client. When you're publishing an API for the outside world, that legibility is most of the value.

Why this matters

The web's entire caching and tooling stack assumes HTTP resources with URLs. REST is the only one of these three that gets all of it for free. Walk away from REST and you're signing up to rebuild some slice of that yourself.

So what does REST actually cost? Two things, and they're the same two we saw above. Over-fetching and under-fetching mean clients either haul down fields they don't use or fire several requests to stitch one screen together. And as the product grows, you fight that by adding endpoints: /users/42/posts/summary, /dashboard/feed, a custom endpoint for the mobile home screen. That's endpoint sprawl. It works. It just slowly turns into a pile of bespoke routes nobody wants to own, each tuned to one screen that shipped two years ago.

Here's my actual opinion: that sprawl is annoying, not fatal. A well-kept REST API with a handful of purpose-built endpoints for your heaviest screens handles the vast majority of products just fine. People treat endpoint sprawl like a crisis that demands GraphQL. Most of the time it's just a tidy-up job you keep putting off.

GraphQL: a query language, and a server you now have to defend

GraphQL gives the client the steering wheel. One endpoint, a typed schema, and queries that ask for exactly the fields a screen needs.[2] Where this genuinely shines is when you have many different clients hitting one backendand each wants a different slice of the data. A web dashboard wants a dense table. The iOS app wants three fields and a thumbnail. The Apple Watch wants almost nothing. With REST you'd build three endpoints or ship over-fetched payloads to the small screens. With GraphQL, each client writes the query it needs and the server doesn't change.

That's also why product teams that iterate fast like it. The front-end can add a field to a screen without waiting on a back-end deploy, as long as the field already exists in the schema. For a company shipping UI changes daily across web and mobile, that decoupling is real velocity, and it's the honest case for GraphQL.

Heads up

GraphQL's flexibility is also its bill. The moment clients can ask for any shape, your server has to be ready for shapes you never planned for, including ones designed to hurt you. Flexibility on the client side is complexity on the server side. It doesn't vanish. It moves to your team.

Three costs come with the territory, and you should price them in before you commit.

Caching gets hard. Every GraphQL request is typically a POST to a single /graphqlURL, so the HTTP-level caching that REST gets for free just doesn't apply. The URL is always the same and the body is always different.[5] You can claw caching back with persisted queries and client-side normalized caches, but now caching is a project you staff, not a header you set.

The N+1 resolver trap.GraphQL resolves a query field by field. Ask for 50 users and each user's posts, and a naive server does one query for the users and then one more per user for posts. That's the N+1 problem, and it'll quietly melt your database. The standard fix is a batching layer like DataLoader, which coalesces those calls.[6]It works well. It's also one more thing you have to know about and wire up correctly.

Expensive queries are an attack surface.Because clients control the shape, a client can request a deeply nested, wildly expensive query and hand your database a bill you didn't agree to. Defending a public GraphQL endpoint means query depth limits, complexity scoring, and timeouts, none of which you needed when the server decided the shape.[7]This is exactly why I'm wary of GraphQL on a fully public, third-party-facing API. You're handing strangers a query planner.

gRPC: fast, typed, and not for browsers

gRPC is a different animal. It's binary Protocol Buffers sent over HTTP/2, with strongly-typed contracts defined in .proto files and code generated for both ends.[3]It also supports streaming in both directions natively, so a server can push a flow of messages without the client re-polling. When you need raw throughput and low latency between services you own, this is the right tool, and it isn't close.

The binary format is the whole point. JSON is text your CPU has to parse and your network has to carry, field names and all, on every message. Protobuf packs the same data into a compact binary frame, which is both smaller on the wire and faster to encode and decode.[8] At the scale of a microservice mesh doing millions of internal calls a minute, that difference compounds into real money and real latency budget.

Free
CDN, browser, proxy all understand it
REST: HTTP caching out of the box
1 round trip
vs several REST calls for the same screen
GraphQL: arbitrary nested data per request
Binary
Protobuf over HTTP/2 vs JSON over HTTP/1.1
gRPC: smaller payloads, faster encode/decode

The catch is that gRPC pays for that speed with friction everywhere a human or a browser is involved.

Browsers can't speak it directly.A web page can't call a gRPC service straight from JavaScript, because browsers don't expose the low-level HTTP/2 control gRPC needs. You have to run a proxy layer, gRPC-Web, that translates between the browser and the real gRPC backend.[9]That's extra infrastructure for the privilege of talking to your own front-end, which is exactly why gRPC rarely belongs on the public edge.

It's hard for humans to read.A Protobuf message on the wire is binary. You can't curl it and eyeball the response. Debugging means special tooling like grpcurlor a reflection-aware client. That's a fair trade inside a platform team that lives in this stack. It's a tax on everyone else.

The tooling has weight.You need the Protobuf compiler, a build step that regenerates stubs when the contract changes, and that pipeline wired into every language your services use. None of it is hard. All of it is overhead you don't carry with REST, where the “tooling” is an HTTP client you already have.

Context

That same typed-contract discipline is why gRPC is so good for internal traffic. The .protofile is a shared, version-controlled agreement between teams. Change a field type and the other team's build breaks immediately, not in production. Inside a company, that strictness is a feature. Facing the public, it's a wall.

Takeaway

REST trades efficiency for legibility and free caching. GraphQL trades server simplicity for client flexibility. gRPC trades human friendliness for raw speed. You're not picking the best one. You're picking which trade you can live with.

The decision rule

Strip away the table and it comes down to three questions about your consumer. Here's the rule I actually use.

  • Public API, third-party integrators, or plain CRUD? Use REST. If outsiders consume it, or it's straightforward create-read-update-delete on resources, REST's legibility and free caching win. Don't overthink it. This is most APIs.
  • One backend feeding many demanding clients with varied data needs? Consider GraphQL. Web plus iOS plus Android, each wanting a different slice, with a product team iterating fast. The flexibility pays for the caching and resolver complexity you take on.
  • High-throughput internal service-to-service traffic? Use gRPC.A microservice mesh where you own both ends, latency matters, and nobody's pointing a browser at it directly. The typed contracts and binary speed are exactly what you want down there.

Now the opinion I'll actually defend. Most teams reaching for GraphQL would be perfectly fine with REST plus a couple of tailored endpoints for their heaviest screens. They're buying a query engine, a caching project, and an N+1 footgun to solve a problem that a /dashboard/feedendpoint solves in an afternoon. GraphQL is a great answer to a question a lot of teams don't actually have yet. Be honest about whether you have it.

Side note

And please don't run all three. I've seen the architecture diagram with REST on the edge, GraphQL as a “gateway,” and gRPC underneath, and for most companies it's three sets of tooling, three debugging stories, and three ways to be on call. A common, sane setup is gRPC between internal services and REST at the public edge. That's two, with a clear reason for each. Add GraphQL only when a genuine many-clients problem shows up and bites.

What I'd do on Monday

If you're starting a new service and you're not sure, start with REST. It's the lowest-regret choice. It caches for free, anyone can debug it, and if you outgrow it you'll know exactly which screens hurt and why. That knowledge is what tells you whether the next move is GraphQL or just three more endpoints.

Reach for gRPC the moment you're writing service-to-service calls that need to be fast and you control both ends. That decision is usually easy once you see the traffic. And reach for GraphQL only when you can name the clients, count them, and show that their data needs genuinely diverge. If you can't name them, you don't have the problem GraphQL solves. You have a REST API that needs a little tidying.

The feature table was never the decision. Who's on the other end of the wire was the decision the whole time.

Written by

Tech Talk News Editorial

Tech Talk News covers engineering, AI, and tech investing for people who build and invest in technology.

ShareXLinkedInRedditEmail