APIs & Protocols

The contract between clients and services: how they talk, what shape the messages take, and where each style earns its keep.

REST gRPC GraphQL Sync vs async Pagination Idempotency
๐Ÿ”—

REST: The Default

REST models everything as resources addressed by URL and manipulated with HTTP verbs. GET /users/42 reads, POST /users creates, PUT/PATCH updates, DELETE removes. Status codes carry the outcome: 2xx success, 4xx the client's fault, 5xx the server's.

It's the default for a reason. It rides plain HTTP, so every browser, proxy, and CDN understands it; GETs are cacheable for free; and it's stateless โ€” each request carries everything it needs, so any server can handle any request and you scale by adding boxes (Scaling Patterns).

CodeMeansTypical use
200 / 201OK / CreatedRead succeeded / resource created
400Bad requestMalformed input, failed validation
401 / 403Unauthenticated / forbiddenNo valid identity / not allowed
404Not foundResource doesn't exist
409ConflictVersion clash, duplicate create
429Too many requestsRate limited (Rate Limiter)
5xxServer errorRetryable if the operation is idempotent
โš ๏ธ
REST creaks when the resource shape doesn't match what the client needs. Mobile screens that need data from five resources make five round trips (chatty), or a fat endpoint returns fields nobody reads (over-fetching). Those two pains are exactly what GraphQL and gRPC-streaming set out to fix.
โšก

gRPC: Fast, Typed, Internal

gRPC defines the contract in a Protobuf schema, generates client and server stubs in every language, and sends compact binary frames over HTTP/2. Smaller payloads than JSON, a real typed contract checked at compile time, and multiplexed connections. It also supports streaming โ€” client, server, or bidirectional โ€” which REST can't do cleanly.

That combination wins inside a system: high-volume, low-latency calls between services that you control on both ends (Microservices).

Where it shines east-west traffic

Internal service mesh, tight latency budgets, strict contracts across many languages, and streaming RPCs. The binary encoding and HTTP/2 multiplexing pay off most where call volume is highest.

Where it doesn't the browser wall

Browsers can't speak raw gRPC โ€” you need a gRPC-Web proxy. Payloads aren't human-readable, so debugging needs tooling, and plain HTTP caches can't see inside. For public, browser-facing APIs, REST is still the safer default.

โ—ˆ

GraphQL: Client-Shaped Queries

GraphQL exposes a single endpoint and a typed schema. The client sends a query describing precisely the fields it wants, across whatever entities, and gets back that exact shape in one round trip. No over-fetching, no under-fetching, no five calls to stitch a screen together.

The cost lands on the server and the edges. Caching is hard โ€” every query is a unique POST body, so HTTP caching by URL no longer works. And the flexible query surface makes the N+1 problem easy to trigger: one query for a list, then one more per item, unless you batch with a loader.

  REST                              GraphQL
  GET /users/42        โ”€โ”           POST /graphql
  GET /users/42/posts  โ”€โ”ผโ”€ 3 trips  { user(id:42) {          โ”€โ”
  GET /users/42/photos โ”€โ”˜             name                   โ”‚  1 trip,
                                      posts { title }         โ”‚  exact shape
   over/under-fetch,                  photos { url } } }     โ”€โ”˜
   fixed shapes                     server resolves each field
๐Ÿ’ก
GraphQL earns its complexity when you have many client types pulling different shapes from the same data โ€” a web app, an iOS app, and partners all hitting one graph โ€” or as an aggregation layer over several backend services. For a single client with stable needs, it's usually more machinery than the problem deserves (Tradeoffs).
๐Ÿ”€

Request-Response vs Event-Driven

All three styles above are synchronous: the caller sends a request and blocks for the answer. Simple, immediate, and the right default โ€” but it couples caller to callee. If the callee is slow or down, the caller waits or fails with it.

The alternative is event-driven: the producer emits an event and moves on; consumers react whenever they can. That decouples failure domains and absorbs spikes, at the price of eventual consistency and harder debugging โ€” you trade a clear call stack for a chain of messages (Messaging).

Request-responseEvent-driven
CouplingCaller waits on calleeProducer and consumer independent
ResultImmediate, in-bandLater, out-of-band
FailurePropagates to callerContained; retried from the queue
SpikesHit the callee directlyAbsorbed by the queue
Best forReads, anything needing an answer nowFire-and-forget work, fan-out, decoupling
โ„น๏ธ
The common pattern is both: respond synchronously with an acknowledgment, then do the heavy work asynchronously. "Upload accepted" returns in 50ms; transcoding runs off a queue. The user isn't blocked on work they don't need to wait for (Video Streaming).
๐Ÿงท

Details That Come Up

A handful of API details show up in almost every design round. Knowing the right answer to each is cheap credibility.

Pagination: cursor beats offset at scale

Offset (LIMIT 20 OFFSET 10000) is easy but the DB scans and discards every skipped row, so deep pages get slow, and inserts shift the window and cause skips or dupes.

Cursor passes an opaque pointer to the last item seen (WHERE id > last). Constant-time regardless of depth and stable under inserts. The default for feeds and large lists.

Idempotency keys safe retries on writes

A retried POST /payments must not charge twice. The client sends a unique Idempotency-Key; the server records it on first use and, on a replay, returns the stored result instead of re-executing.

This is what makes mutations safe to retry over a flaky network (Concurrency Control).

Versioning and rate-limit headers the rest of the contract

Versioning keeps you from breaking existing clients when the shape changes: a URL prefix (/v2/) is the blunt, visible option; a header or media-type version is cleaner but easier to miss. Whichever you pick, additive changes shouldn't need a new version โ€” only breaking ones.

Rate-limit headers (X-RateLimit-Remaining, Retry-After) tell a well-behaved client how much budget is left and when to come back, turning a 429 into a signal instead of a surprise (Rate Limiter).

๐Ÿ“‹

Quick Reference

RESTgRPCGraphQL
TransportHTTP/1.1+HTTP/2HTTP (usually POST)
PayloadJSON (text)Protobuf (binary)JSON, client-shaped
ContractConvention / OpenAPIStrict, generated stubsTyped schema
StreamingNo (awkward)Yes, bidirectionalVia subscriptions
HTTP cachingFree on GETNoHard
BrowserNativeNeeds proxyNative
Best fitPublic, browser-facing APIsInternal service-to-serviceMany clients, aggregation layer
PaginationHowCost
OffsetLIMIT/OFFSETSlow deep pages, drifts on insert
CursorPointer to last item seenConstant-time, stable โ€” use at scale
โ„น๏ธ
The one-liners to keep ready. REST for public and browser-facing; gRPC for internal high-volume; GraphQL when many clients pull different shapes. Sync for answers you need now, async to decouple and absorb spikes. Cursor pagination at scale, idempotency keys on retryable writes, and a versioning story before you ship v1.