Stylesapi stylesdecisionrestgrpcgraphqlwebsockets

Which API Style Should I Use?

REST, RPC/gRPC, GraphQL, SSE, WebSockets, webhooks, async jobs — seven shapes, each answering a different question about who the consumer is and how data needs to move. The decision is made by consumer environment and operational budget, not by fashion.

Follow the failure

Frame the contract

API design starts with a consumer, a design question and a guarantee — never with a URL.

Design question
Given who consumes this API, where they run, and how data needs to flow, which contract shape fits — and what does that shape commit the team to operating?
Consumers
The team choosing a style for a new surface, and every consumer who inherits the choice: a browser that cannot speak raw HTTP/2 frames, a partner who needs curl-able docs, an internal service that wants generated clients, a dashboard that needs live updates.
The promise
The style is chosen from the consumer environment and data-flow shape, its costs are named before it ships, and the alternatives that were rejected are recorded — so the choice can be revisited when the consumers change.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

Start from the question, not the acronym

Style debates go wrong when they start with the technology. "Should we use GraphQL?" has no answer; "our mobile app renders one screen from six services and each release lives for eighteen months" does. Each API style is a good answer to a specific shape of problem, and the decision tool below is just those shapes written down. It is deliberately not a flowchart with one exit — most real APIs are a REST core with an SSE endpoint for progress and webhooks for partners, because the consumer tasks differ (Consumer-First Design).

Three questions do most of the work. Who calls it and from where? Browsers, third-party developers and curl all favor plain HTTP and JSON; a fleet of internal services with a shared build pipeline can afford generated clients and binary framing. Which way does data flow, and when? Request/response, server-push, bidirectional, or "come back later" are different shapes, and forcing one into another is where the pathologies live (holding a connection open for a 15-minute report — see Long-Running Operations: 202 and the Job Resource). What does the team operate? A style is also a runtime: WebSockets make the tier stateful, GraphQL makes every query a potential cost problem, gRPC needs a proxy story for browsers.

The recommendations below always come with four attachments: what the style buys, what it costs, what it commits you to operating, and when not to use it. A recommendation without the cost column is an advertisement.

The decision tool, with the price attached to every row
NeedStyleWhy it fitsTrade-offsOperational implicationsWhen not to use itAlternatives
Broadly accessible web API, unknown consumersREST over HTTP/JSON (REST as a Practical Style)Every language, proxy, cache and curl already speaks it; docs are the contractFixed response shapes → over/under-fetching; no built-in type contractCheap to run; caching and gateways work unmodifiedHot internal paths where serialization cost dominates; chatty aggregation screensgRPC internally, BFF for aggregation
Strongly typed internal service callsgRPC / RPC (gRPC: Schema, Codegen and Streams, RPC: Operation-Oriented Contracts)Generated clients, schema-enforced contracts, binary framing, streaming on HTTP/2Browsers need a proxy; payloads unreadable without tooling; proto discipline requiredBuild pipeline owns codegen; HTTP/2-aware load balancingPublic APIs for third parties; teams without shared build toolingREST with OpenAPI codegen
Client-defined field selection across many entitiesGraphQL (GraphQL: Client-Shaped Queries Over One Schema)One schema, many client shapes; kills endpoint-per-screen sprawlN+1 and cost control are now your problem; per-field authz; HTTP caching mostly lostQuery cost limits, persisted queries, resolver batching, schema governanceOne consumer with stable needs; write-heavy command APIs; teams unable to fund the cost machinery (What GraphQL Costs)REST plus sparse fieldsets, a BFF
Server → browser event streamSSE (Server-Sent Events)Plain HTTP, auto-reconnect with Last-Event-ID, works through most proxiesOne direction only; text framing; connection-per-clientLong-lived connections on the edge; idle timeouts tunedClient must send frequent messages back; binary streamsPolling for low-frequency; WebSockets for bidirectional
Bidirectional real-time conversationWebSockets (WebSocket Message Contracts)Full duplex, low per-message overhead, binary allowedYou now own a message protocol: types, acks, sequence, reconnectStateful tier; sticky sessions or a pub/sub fan-out; connection scalingOne-way notifications; anything that fits request/responseSSE + POST; long polling
Notify third parties asynchronouslyWebhooks (Webhooks: The Inverted Contract)Push without the consumer polling; decoupled from your request pathAt-least-once delivery, retries, signing, consumer outages are now your queueDelivery pipeline with retry schedule and dead-letterConsumers who cannot expose an endpoint; strict ordering needsPolling a feed endpoint; a message queue for internal consumers
Work that outlives a requestAsync job resource (The Async Job Pattern)202 + job id; the client waits on a resource, not a socketTwo round trips minimum; job retention and status semantics to designWorker fleet, job store, completion notification pathSub-second operations; results nobody will fetch laterSynchronous with a hard timeout; streaming progress

The comparison axes

When two styles both seem to fit, compare them on the axes that will matter in year two rather than on the demo. Public friendliness asks whether an unknown developer with curl and a browser can succeed on day one. Type safety asks whether the contract is enforced by tooling or by documentation and hope. Streaming asks whether partial results and long-lived flows are native or bolted on. Caching asks whether existing HTTP infrastructure — CDNs, gateway caches, browser caches — helps for free. Client flexibility asks who decides the response shape. Operational complexity asks what the team must run and tune. Observability asks whether standard tooling sees the operations or only sees POST /graphql (API Metrics: Rate, Errors, Duration, Sizes). Compatibility asks how a change reaches old clients.

No column wins everywhere, which is the point: the table exists to make the loss explicit. Choosing GraphQL means signing up for the cost-control column; choosing gRPC means giving up the public-friendliness column unless you also run a transcoding gateway. A team that picks a style without being able to say which column it just lost has not made a decision yet.

Styles across the axes that matter after launch
AxisRESTgRPCGraphQLWebSocketsSSEWebhooks
Public friendlinessHigh — curl and docsLow without a proxyMedium — needs a client mindsetMediumHighHigh for providers, work for consumers
Type safetyBy docs/OpenAPIEnforced by protoEnforced by schemaWhatever you defineWhatever you defineBy docs/schema
StreamingBolt-onNative (4 modes)Subscriptions (transport-dependent)NativeNative, one-wayEvent-at-a-time
HTTP cachingNativeNoneMostly lostNoneNoneN/A
Client flexibilityServer decides shapeServer decides shapeClient selects fieldsMessage protocol decidesServer decidesProvider decides
Operational complexityLowMedium — codegen, HTTP/2 LBHigh — cost control, batchingHigh — stateful tierMedium — long connectionsHigh — delivery pipeline
ObservabilityPer-endpoint out of the boxPer-method out of the boxPer-operation only with workPer-message only with workPer-stream with workPer-delivery pipeline
Compatibility modelAdditive JSON, versionsField numbers, reserved tagsAdditive schema, @deprecatedMessage versioning you inventEvent type versioningEvent schema versioning

Mixed styles are normal; unrecorded choices are not

A payment platform ends up with REST for the public contract, webhooks for settlement events, SSE for a dashboard, and gRPC between the ledger and the risk service. That is not indecision — it is four consumer environments getting four honest answers. What makes it maintainable is a written decision per surface: which style, why, which alternative was rejected, what it costs (Design Principles Without Commandments on recording reasoning). The next engineer then extends a pattern instead of relitigating the acronym war.

The failure mode to watch for is a style chosen for the wrong layer: GraphQL adopted because one mobile screen was chatty (a BFF would have solved it — Backend for Frontend); WebSockets adopted for a notification feed that changes twice an hour; gRPC exposed to partners who then spend a week finding a client library. Each is a good tool applied to a question it does not answer.

  • Unknown consumers, browsers, curl → REST; pay with fixed response shapes.
  • Internal, typed, high-volume → gRPC; pay with codegen discipline and a browser proxy.
  • Many client shapes over one graph → GraphQL; pay with cost control and batching.
  • Server push, one-way → SSE; two-way → WebSockets; pay with long-lived connections.
  • Third parties must react → webhooks; pay with a delivery pipeline.
  • Work outlives the request → async job resource; pay with two round trips and a job store.
A style decision, recorded the way the decision log expects
Surface:      Public Payments API
Style:        REST over HTTP/JSON, OpenAPI-described
Why:          third-party developers, curl-first onboarding, CDN-cacheable reads
Rejected:     gRPC (browser/partner friction), GraphQL (write-heavy command API, cost control burden)
Costs:        fixed shapes → sparse fieldsets added later if telemetry shows over-fetching
Operates:     gateway auth + rate limits, per-endpoint metrics, additive-only evolution
Revisit when: partner SDKs need streaming, or an internal consumer dominates traffic

Key points

  • The style is decided by consumer environment and data-flow shape, not by preference; each style answers a different question.
  • Every recommendation carries four attachments: what it buys, what it costs, what it commits you to operating, when not to use it.
  • Compare on year-two axes — public friendliness, type safety, streaming, caching, flexibility, operational load, observability, compatibility — and name the column you lose.
  • Mixing styles across surfaces is normal; the same API using one style for a task it does not fit is the smell.
  • Record the decision, the rejected alternative and the cost per surface so the choice can be revisited when consumers change.

Follow the failure

How the contract fails or gets misused, hop by hop — and what it costs when it completes.

  1. 1
    Team → whiteboard: chooses GraphQL because a competitor did, before listing who calls the API or how often the shape changes.
  2. 2
    Mobile team → API: gets flexible queries and immediately writes one that joins users → orders → items with no cost limit.
  3. 3
    Partner developers → API: try to integrate with curl, find no per-resource endpoints, and ask for "a normal REST API" in the support channel.
  4. 4
    Operations → dashboards: every request is POST /graphql with status 200; latency and error metrics are meaningless per operation.
  5. 5
    Platform team → roadmap: adds cost analysis, persisted queries and a REST facade — the machinery that would have been named on day one by asking the three questions.
What breaks
  • Consumers in the wrong environment pay integration cost the style was supposed to save (partners fighting gRPC, browsers fighting binary framing).
  • The team operates a runtime it never budgeted for: stateful WebSocket tiers, GraphQL cost control, webhook delivery pipelines.
  • Observability and caching that came free with the rejected style must be rebuilt by hand.
  • The style becomes the architecture: once every client has generated gRPC stubs or GraphQL fragments, changing course is a migration program (API Migration: Running the Change End to End).

Design, observe, evolve

A contract decision is incomplete until you know how you would notice it failing and how it changes later.

Design the contract
  • • Answer the three questions in writing — who calls it and from where, which way data flows, what the team can operate — before naming a style.
  • • Choose per surface, not per organization; a REST core with SSE progress and partner webhooks is a coherent design.
  • • Attach the cost column to the recommendation and record the rejected alternative in the decision log.
  • • Pick the boring style for unknown consumers; spend novelty budget only where a measured problem (chatty screens, typed internal traffic) demands it.
  • • Name the revisit trigger — the consumer or traffic change that would make a different style right.
Observe in production
  • • Support tickets asking for "a normal API" or a client library in language X signal a style mismatched to its consumers.
  • • A single endpoint carrying all traffic (`/graphql`, `/rpc`) with uniform 200s means per-operation metrics were never built.
  • • Connection counts and memory on the edge growing with users indicates a persistent-connection style chosen for a low-frequency need.
  • • Internal REST calls dominated by serialization CPU and repeated round trips is the signal that a typed RPC style would pay.
Evolve without breaking
  • • Add a second style beside the first for the consumer it serves (SSE next to REST, gRPC internally behind a REST facade) rather than replacing the first.
  • • Transcoding gateways let a gRPC core expose REST/JSON to browsers and partners without a second implementation.
  • • A GraphQL layer can be introduced as a BFF over existing REST services and retired the same way if it stops earning its cost.
  • • Revisit the decision when the recorded trigger fires — a new dominant consumer, a measured chattiness problem, a streaming requirement.
What it costs
  • • Answering the questions honestly takes a design session the "just use REST" or "just use GraphQL" shortcut skips — and is usually cheaper than the migration the shortcut causes.
  • • Multiple styles across surfaces means multiple toolchains, docs formats and on-call playbooks; each surface must earn its style.
  • • The boring default under-serves genuinely unusual consumers; the point is to notice them from evidence, not to forbid novelty.

Misconceptions

Claim
“One style should be standard across the company.”
Reality
One default is healthy; one mandate is not. Public partners, browsers and internal high-volume services are different consumer environments, and a company-wide "everything is gRPC" or "everything is GraphQL" rule makes one of them pay for the others.
Claim
“GraphQL or gRPC are the modern choice and REST is legacy.”
Reality
They are answers to specific problems — client-shaped queries and typed internal traffic — with real operational costs. For unknown consumers over the public internet, plain HTTP/JSON with good docs remains the lowest-friction contract, and that is a design outcome, not nostalgia.
Claim
“The style decision can be changed later cheaply.”
Reality
Once clients have generated stubs or fragment libraries, the style is embedded in every consumer's codebase. Reversing it is a multi-quarter migration; recording the decision and its revisit trigger is what keeps the option open.

Apply it