APIsRESTGraphQLgRPCRPCWebSockets

API Architecture: REST, GraphQL, RPC, gRPC, WebSockets, Webhooks

Each API style exists because a previous one hurt — REST for cacheable resources, GraphQL for clients that need many shapes, gRPC for fast typed internal calls, WebSockets for server push, webhooks for event delivery across organisations — and each carries a failure mode you inherit the moment you choose it.

▶ InteractiveInterview questionDebug it
Progress
What problem does this solve?

Clients and services need a contract for exchanging data, and the shape of that contract decides caching, latency, type safety, and how painful the next version is. Choosing the style that matches the client and the traffic pattern avoids building a workaround layer later.

What each one is, and what hurt before it

REST: resources at URLs, verbs from HTTP, representations in JSON. It exists because HTTP already had caching, proxies, status codes and idempotent verbs, and reinventing them in a custom envelope threw all of that away. Its cost is fixed response shapes: a mobile screen that needs a user, their last three orders and each order’s first item makes four round-trips (under-fetching) or gets a 40 KB user object to read one field (over-fetching).

GraphQL: one endpoint, a typed schema, the client writes the query and gets exactly that shape. It exists because a company with a web app, two mobile apps and a partner API could not keep adding ?include=orders.items variants to REST. Its cost is that the server no longer knows the shape of the work in advance: a naive resolver for users { orders { items } } runs one query per user then one per order (N+1 resolvers), which is why every GraphQL server needs batching (DataLoader) and why HTTP caching mostly stops working — every query is a POST with a different body.

RPC / gRPC: call a function on another machine. gRPC adds Protocol Buffers (a binary, schema-first wire format), generated clients, and HTTP/2 multiplexing with bidirectional streams. It exists because internal service-to-service calls do not need the ceremony of resources; they need type safety, small payloads and low latency — a gRPC call is typically 3–10× smaller on the wire than the equivalent JSON. Its cost is that browsers cannot speak it natively (you need grpc-web and a proxy) and it needs HTTP/2 end to end; a load balancer that terminates HTTP/1.1 silently breaks streaming.

WebSockets: one long-lived, bidirectional TCP connection per client. It exists because polling every second to see if a chat message arrived is 3,600 requests per client per hour for mostly empty responses. Its cost is connection state: the server now holds a socket per user, which fights the stateless scaling in Stateless vs Stateful Services — you need connection routing, heartbeats, reconnect logic, and a way to reach "the server holding user 42’s socket". Server-Sent Events are the one-directional, HTTP-native alternative when only the server needs to push.

Webhooks: the provider calls *your* URL when something happens. They exist because polling a payment provider for "did the charge settle?" is wasteful and slow across organisations. Their cost is that the provider will retry — Stripe retries for up to three days — so your endpoint receives duplicates and must be idempotent (dedupe on the event id), must respond fast (2xx within seconds, do the work on a queue), and must verify the signature, because anyone can POST to a public URL.

Where each style usually sits
HTTPSREST / GraphQLgRPC, HTTP/2WebSocketwebhook, retriedBrowser / mobilePayment providerAPI GatewayWebSocket serverWebhook endpointGraphQL / REST edgeInternal services (gRPC)
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

The decision matrix

The rows are the questions an interviewer will actually ask. "Public" means third parties you do not control; "internal" means services your organisation deploys together. A finder that walks these questions in order lives at which-api-style; the usual answer for a product is REST at the edge, gRPC inside, WebSockets or SSE for the one screen that needs push, webhooks for partners — and GraphQL only when several client shapes are a real, measured cost.

Which style, for whom
RESTGraphQLgRPCWebSocketsWebhooks
Public vs internalBoth; best publicPublic or BFFInternalBothPublic (outbound)
Browser clientsNativeNativeNeeds grpc-web + proxyNativen/a (server to server)
StreamingNo (SSE alongside)Subscriptions (over WS)Yes: uni- and bidirectionalYes, bidirectionalNo; one event per call
Type safetyOpenAPI, optionalSchema, built inProtobuf, built inNone; you define framesProvider’s schema
Latency / payloadMedium; JSONMedium; JSON, 1 round-tripLow; binary, HTTP/2Lowest per messageProvider-controlled, async
Query flexibilityFixed shapesClient-definedFixed messagesn/an/a
VersioningURL or header; breaks explicitlyAdditive; deprecate fieldsField numbers; additiveProtocol-level, manualEvent versions per type
CachingHTTP caches, CDN, ETagHard; per-field client cacheNone built inNoneNone
Signature failureOver/under-fetchingN+1 resolvers, no HTTP cacheHTTP/1.1 in the path breaks itConnection state, reconnect stormsRetries → duplicates; no idempotency

Versioning and the cost of the next change

Every style has to survive its second version. REST tends toward explicit breaks — /v2/orders — which is honest but means two code paths for a year. GraphQL and Protobuf are designed for additive evolution: add a field, never remove or renumber one, mark old fields deprecated and watch their usage metrics until it hits zero. That works only with the discipline of never reusing a Protobuf field number and never changing a GraphQL field’s type. WebSocket protocols have no help at all; version the frame envelope from day one ({ v: 1, type: "msg", ... }) because you will not be able to add it later without breaking every connected client.

The shared rule: the client you cannot redeploy — a partner, an app-store binary from eighteen months ago — is the one that decides how conservative the contract must be. Internal gRPC between services you deploy together can be aggressive; a public REST API cannot.

A webhook receiver that survives retries: verify, dedupe, acknowledge fast, work later
1app.post('/webhooks/payments', async (req, res) => {
2 if (!verifySignature(req.rawBody, req.headers['x-signature'], SECRET)) return res.sendStatus(401)
3 const event = JSON.parse(req.rawBody) as { id: string; type: string; data: unknown }
4
5 // idempotent: the provider WILL redeliver; the event id is the dedupe key
6 const fresh = await db.insertIgnore('processed_webhooks', { id: event.id, received_at: now() })
7 if (!fresh) return res.sendStatus(200) // already handled; ack again, do nothing
8
9 await queue.publish('payment-events', event) // work happens off the request path
10 res.sendStatus(200) // ack within seconds or it is retried
11})

Key points

  • REST for cacheable public resources; GraphQL when many client shapes are a measured cost; gRPC for typed, fast internal calls; WebSockets for bidirectional push; webhooks for cross-organisation events.
  • Each style’s signature failure: over/under-fetching, N+1 resolvers, HTTP/1.1 in a gRPC path, connection state, and webhook duplicates.
  • GraphQL and Protobuf evolve additively; REST breaks explicitly; WebSocket protocols need a version field from day one.
  • A webhook endpoint must verify the signature, dedupe on event id, acknowledge in seconds, and do the work on a queue.
  • The client you cannot redeploy decides how conservative the contract has to be.

The same data in REST, GraphQL, gRPC and WebSocket

The same data in REST, GraphQL, gRPC and WebSocket
The need: user 42, their last 3 orders, each order's total. Same data, four shapes on the wire.
Request
GET /users/42
GET /users/42/orders?limit=3

(two round trips; the second waits for the first)
Response · greyed = fetched but unused
{ "id": 42, "name": "Ada",
"email": "ada@example.com", "avatar": "…",
"createdAt": "2019-03-01", "locale": "en-GB",
"preferences": { … 14 keys … } }
[ { "id": 901, "total": 42.50,
"items": [ … 6 items … ], "address": { … } },
{ "id": 902, "total": 12.00, "items": [ … ] },
{ "id": 903, "total": 99.90, "items": [ … ] } ]
Round trips
2
Payload
~2,900 B
HTTP-cacheable
yes
Typed contract
no
Browser-native
yes
Server push
no
Two resources, two requests, and every field the server thinks a "user" or "order" has — greyed lines are bytes the client throws away. In exchange you get URLs that any HTTP cache, proxy, CDN and curl understand.
Decision rule. Public API for unknown clients → REST (cacheable, boring, tooling everywhere). Many client shapes over the same graph (mobile + web + partners) → GraphQL, with dataloaders from day one. Internal service-to-service with latency budgets → gRPC. Server must push (chat, live prices, presence) → WebSocket, or Server-Sent Events when one direction is enough.
Style

How data moves through it

One request or event, hop by hop.

  1. 1Client → Gateway: TLS terminated, auth checked, the request routed by path or Content-Type to a REST, GraphQL or WebSocket upgrade handler (API Gateway).
  2. 2Gateway → Edge service: a REST handler or GraphQL resolver validates input and fans out to internal services.
  3. 3Edge service → Internal services: gRPC calls over pooled HTTP/2 connections, with deadlines propagated in metadata.
  4. 4Internal service → Database: the actual query; a GraphQL resolver batches here to avoid N+1.
  5. 5Provider → Webhook endpoint → Queue → Worker: an external event is verified, deduped, acknowledged and processed asynchronously.

When to use — and when not

Use it when
  • REST: public APIs, resource-shaped data, anything that benefits from CDN and browser caching.
  • GraphQL: several first-party clients with different screens, or a partner API where consumers need their own shapes.
  • gRPC: internal service-to-service calls where payload size and latency matter and both ends are generated from one schema.
  • WebSockets / SSE: chat, presence, live dashboards — anything where polling would be mostly empty responses.
Avoid it when
  • GraphQL for one client with fixed screens; you pay the resolver and caching cost for flexibility nobody uses.
  • gRPC straight to browsers or through infrastructure that terminates HTTP/1.1.
  • WebSockets for data that changes every few minutes; a cached REST endpoint polled at that interval is cheaper and stateless.
  • Webhooks without idempotent handling; the first provider retry storm will double-process every event.

Tradeoffs

Complexity
low → high
Ops cost
low → high
Latency
low → high
Consistency
weak → strong
Scalability
poor → strong

Ratings vary by style: REST is the simplest and most cacheable; gRPC the fastest; WebSockets add the most operational state.

How it fails

  • REST under-fetching: a mobile screen makes six sequential calls on a 200 ms link and renders after 1.2 s.
  • GraphQL N+1: a users { orders { items } } query for 50 users runs 1 + 50 + 50×n SQL statements; a DataLoader batches it to three.
  • gRPC behind an HTTP/1.1 load balancer: unary calls work, streams hang or drop, and the failure looks like random timeouts.
  • WebSocket reconnect storm: a deploy closes 200,000 sockets at once and all clients reconnect in the same second — add jittered backoff.
  • Webhook duplicate: the provider retries after your 5 s timeout, the second delivery ships a second order.

How it scales

  • REST and GraphQL scale as stateless HTTP behind a load balancer; REST additionally offloads reads to CDNs (CDN Architecture).
  • gRPC scales with HTTP/2 connection reuse — but L4 balancers pin a long-lived connection to one backend, so use L7 (gRPC-aware) balancing or client-side load balancing (Load Balancing).
  • WebSockets scale by connection count, not request rate: ~100k–1M sockets per server, a pub/sub layer (Redis) to route messages between servers, and sticky or hash-based routing to find a user’s socket.
  • Webhook receivers scale by queue depth; the HTTP handler only enqueues, so a burst of 50,000 events is a backlog, not an outage (Background Jobs and Workers).

How it interacts with databases, queues, caches, APIs and external systems

  • Database: REST and gRPC map naturally to fixed queries; GraphQL needs batching and per-resolver cost limits or it becomes an ad-hoc query engine against your tables.
  • Queue: webhook and WebSocket inbound events are enqueued rather than processed inline, so the connection path stays fast.
  • Cache: REST responses cache at browser, CDN and gateway via Cache-Control and ETag; GraphQL needs persisted queries to get any of that back (Caching Architecture).
  • External APIs: consumed as REST or gRPC clients with timeouts and breakers (Circuit Breaker); their events arrive as webhooks.
  • Agents and tools: an LLM tool call is an RPC with a JSON schema — the same contract-first discipline as gRPC (Tool Calling Basics).