REST vs GraphQL vs gRPC

Three API Styles, Three Sets of Tradeoffs

REST vs GraphQL vs gRPC

The real tradeoffs between REST, GraphQL, and gRPC — over/under-fetching, typing, streaming, caching, browser support — and how each looks from Node.

9 min read Level 3/5 #system-design#api#rest
What you'll learn
  • Contrast REST, GraphQL, and gRPC across the dimensions that matter
  • Pick the right paradigm for a given client and traffic shape
  • Recognize what each looks like from a Node client and server

Once you’ve picked a transport, you pick an API paradigm — the contract between client and server. The three you’ll defend in an interview are REST, GraphQL, and gRPC. None is universally “best”; each optimizes for a different problem. Your job is to match the paradigm to the client and the traffic.

REST: resources and verbs

REST models your system as resources addressed by URLs, manipulated with HTTP verbs (GET, POST, PUT, DELETE). It’s the default for public web APIs because it’s simple, debuggable with curl, and — crucially — plays perfectly with HTTP caching. A GET /users/42 is a cacheable, idempotent request that CDNs and browsers already know how to store.

Its two classic pains are over-fetching and under-fetching. A GET /users/42 returns the whole user when the client only wanted the name (over-fetch). Rendering a profile screen might need the user, their posts, and their followers — three round trips (under-fetch). REST’s coarse-grained endpoints don’t bend to each screen’s exact needs.

GraphQL: the client shapes the response

GraphQL exposes one endpoint and a typed schema; the client sends a query describing exactly the fields it wants, nested as deeply as needed, and gets back that exact shape. One round trip, no over- or under-fetching.

One query, exactly the fields the screen needs script.js
const query = `
  query Profile($id: ID!) {
    user(id: $id) {
      name
      posts(last: 3) { title }      # nested — no extra round trip
      followers { count }
    }
  }
`;

const res = await fetch('/graphql', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ query, variables: { id: '42' } }),
});
const { data } = await res.json(); // shaped exactly like the query
▶ Preview: console

The costs: HTTP caching gets harder (everything is a POST to one URL, so you lean on client-side caches like Apollo and persisted queries instead). The server must guard against expensive queries — deep nesting can trigger the N+1 problem, which you fix with DataLoader batching. And the schema/resolver layer is real machinery to operate.

gRPC: typed, binary RPC

gRPC is remote procedure calls over HTTP/2 with Protocol Buffers. You define services and messages in a .proto file, and codegen produces strongly typed client and server stubs. The payload is compact binary, calls are fast, and HTTP/2 gives you bidirectional streaming for free.

The catch is browser support: browsers can’t speak raw gRPC, so client-facing use needs a gRPC-Web proxy that translates. That, plus the codegen step, makes gRPC shine for internal service-to-service traffic where you control both ends and care about throughput.

The comparison

DimensionRESTGraphQLgRPC
TransportHTTP/1.1+HTTP (usually 1.1)HTTP/2
PayloadJSON (text)JSON (text)Protobuf (binary)
Schema/typingOptional (OpenAPI)Built-in, typedBuilt-in, typed
Over/under-fetchCommonSolvedPer-method
HTTP cachingExcellentHardN/A (POST-like)
StreamingSSE/WebSocket bolt-onSubscriptionsFirst-class
Browser-friendlyYesYesNo (needs gRPC-Web)
Best forPublic web APIsAggregating many sourcesInternal microservices

How to choose

REST vs GraphQL vs gRPC — architecture diagram

A useful default: REST at the edge, gRPC in the back. Public clients and partners get a cacheable REST surface; your internal services talk to each other over gRPC for speed and strong contracts. Reach for GraphQL when one frontend aggregates many backend sources and you’re tired of shipping a bespoke “backend-for-frontend” endpoint per screen.

The JavaScript angle

All three are first-class in Node. REST is an Express route. GraphQL servers run on Apollo Server or GraphQL Yoga. gRPC uses @grpc/grpc-js (a pure-JS implementation, no native addon) driven by your .proto.

A gRPC server and client in Node script.js
import grpc from '@grpc/grpc-js';
import protoLoader from '@grpc/proto-loader';

const def = protoLoader.loadSync('users.proto');     // from the .proto contract
const { users } = grpc.loadPackageDefinition(def);

// --- server: implement the RPC method ---
const server = new grpc.Server();
server.addService(users.UserService.service, {
  GetUser: (call, callback) => {
    callback(null, { id: call.request.id, name: 'Ada' });
  },
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () =>
  server.start(),
);

// --- client: call it like a local function ---
const client = new users.UserService('localhost:50051', grpc.credentials.createInsecure());
client.GetUser({ id: '42' }, (err, user) => console.log(user.name)); // "Ada"
▶ Preview: console

Notice the client calls GetUser like a local function — that’s the whole point of RPC. Contrast it with the REST fetch and the GraphQL query above: same data, three different contracts, three different cost profiles.

These three paradigms all assume the client asks first. Next we flip that: how do you let the server push data to the client?