What you'll learn: By the end of this lesson you will be able to describe REST-style HTTP APIs, gRPC, GraphQL and WebSockets by what they promise in five areas (contract, how a call travels, how errors are reported, how the interface changes, and what shape of conversation they allow), read the primary sources behind each, predict what each does when a call times out or a connection drops, and choose a style for each conversation in a system instead of one style for everything.
A restaurant offers four ways to be served. The menu is printed, the same for every table, and any waiter can take an order from it. The supplier's order form is a strict form with numbered boxes that two kitchens agreed on, quick to fill in and easy to check. The build-your-own plate lets the guest list exactly the items wanted and brings them in one trip. The open phone line stays connected, so either side can speak when something changes. Each fits a different conversation, and each fails differently: a printed menu cannot warn you that the soup ran out a minute ago, and a phone line that drops leaves both sides unsure what was heard last.
Those four are, in order, resource-style HTTP APIs (usually called REST), gRPC, GraphQL and WebSockets. The API Design lesson teaches how to design a good REST API and gives a three-row comparison; ShopStream Stage 8 picks styles in a few bullets. This lesson goes further: what each style promises on the wire, how it reports errors, how it evolves, and what happens when things break.
Verified against RFC 9110, RFC 6455, the GraphQL specification (October 2021 edition), the gRPC documentation and protocol notes, the Protocol Buffers proto3 guide and MDN, on 2026-11-08.
The core idea
An API style is a bundle of five decisions, not a brand. Compare the styles on the same five questions and the choice becomes a reasoned one.
| Decision | The question it answers |
|---|---|
| Contract | What is written down, and who can check a call against it? |
| Transport mapping | How does one call travel over the network? |
| Error model | How does a failure reach the caller, and can a generic layer see it? |
| Evolution | What changes are safe without breaking callers that have not upgraded? |
| Conversation shape | One request and one reply, a stream, or messages in both directions at any time? |
Three examples run through the lesson, each labelled: a minimal example (one "get order" call written in all four styles), a realistic example (ShopStream's browser, service and courier conversations, in "In practice") and a failure example (an error that a generic monitor cannot see).
The shared floor: HTTP semantics
Three of the four styles run on HTTP, so its semantics set the floor. RFC 9110 is the specification.
- Safe and idempotent methods. A safe method is one whose semantics are essentially read-only (section 9.2.1). A method is idempotent if multiple identical requests have the same intended effect as one; PUT, DELETE and the safe methods are idempotent (section 9.2.2). A client may repeat an idempotent request automatically after a communication failure, and should not automatically retry a non-idempotent one unless it knows the request is actually idempotent or can detect that the original was never applied.
- GET is where caching lives. The RFC calls GET "the primary mechanism of information retrieval and the focus of almost all performance optimizations" (section 9.3.1). A response to POST is cacheable only with explicit freshness information and a matching
Content-Location(section 9.3.3). - POST is open-ended. It asks the target resource to process the enclosed representation "according to the resource's own specific semantics" (section 9.3.3).
Every style below builds on these rules (REST-style APIs), tunnels through one method (gRPC and GraphQL use POST), or leaves them behind after a handshake (WebSockets). What each does with them explains much of its failure behaviour.
Minimal example: one call, four styles
The call is "get the status of order o_123". Everything below is illustrative text in each style's own notation.
Resource-style HTTP. The identifier is in the URL and the method says what to do:
GET /v1/orders/o_123 HTTP/1.1
Accept: application/json
gRPC. The contract is a Protocol Buffers file; by default gRPC uses protocol buffers as its interface definition language:
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
}
message GetOrderRequest { string order_id = 1; }
message Order {
string order_id = 1;
string status = 2;
int64 total_minor = 3;
}
GraphQL. One endpoint, and the client names the fields it wants:
query {
order(id: "o_123") {
status
totalMinor
}
}
WebSocket. After a handshake, either side sends messages on one open connection. RFC 6455 defines the handshake, framing, ping, pong and close; what a message means is the application's job, so this JSON is an invented convention:
{ "type": "subscribe", "topic": "order.o_123" }
The first three are pull: the client asks and the server answers. The fourth is a conversation the server can start, which is the first difference to decide.
Resource-style HTTP APIs
A resource-style API exposes nouns at URLs and uses HTTP methods and status codes as the vocabulary: GET /v1/orders/o_123 reads, POST /v1/orders creates, and a 201 with a Location header (RFC 9110 section 9.3.3 says the server SHOULD send one when POST creates a resource) points at the result.
- Contract. Usually a document plus conventions; nothing in HTTP forces a call to match it, so checking is a tool concern.
- Errors. The status code is the first signal, and every HTTP-aware layer understands it: proxies, load balancers, monitors and retry libraries. A 503 means temporary overload or maintenance and may carry
Retry-After(sections 15.6.4 and 10.2.3). RFC 9457 defines a JSON body format,application/problem+json, with members such astypeanddetail, so errors can be machine-readable. - Evolution. Adding fields and endpoints is safe for tolerant clients; removing or reinterpreting them is not.
- Shape. Request and reply. Long-running work returns
202 Accepted, which RFC 9110 says is "intentionally noncommittal": the work was accepted, not finished, and the representation should point to a status monitor. - Strength. The widest tooling, and the style standard HTTP caches handle most naturally.
- Cost. A screen that needs five resources makes five calls or needs a purpose-built aggregation endpoint; the Backend for Frontend pattern exists for that.
gRPC
gRPC is a remote-procedure-call framework: the caller invokes a named method with typed input and gets typed output, as if calling a local function. The core concepts page lists four kinds of call: unary, server streaming, client streaming and bidirectional streaming, each with message order preserved within the call.
How a call travels. The gRPC over HTTP/2 document specifies the mapping. Every call is an HTTP/2 POST to a path built from the service and method name, with content type application/grpc and an optional grpc-timeout header. The status of the call is not the HTTP status. The reply carries :status 200, and the result arrives in trailers as grpc-status and grpc-message. A call that fails immediately can send a "Trailers-Only" response. The document also says to expect broken deployments that return other HTTP statuses, and requires implementations to synthesise a status when that happens.
Figure 1. A unary gRPC call and its two endings. How to read this: the status of a successful call lives in trailers, after the message, not in the HTTP status line. If the deadline passes first, the client ends the call and the server is cancelled; the server application still has to stop work it started, because changes made before a cancellation are not rolled back.
Deadlines and failure. The deadline guide says gRPC sets no deadline by default, so clients should always set a realistic one. Propagation of a deadline to outgoing calls is language-dependent: on by default in some (Java and Go are named) and explicit in others (C++). The status codes page gives each failure its own code: UNAVAILABLE ("most likely a transient condition, which can be corrected by retrying with a backoff"), DEADLINE_EXCEEDED (for state-changing operations the call may still have succeeded), ABORTED (retry at a higher level) and FAILED_PRECONDITION (do not retry until the state is fixed). Retries are configured per method in a service config: the retry guide says retries are enabled by default but there is no default retry policy, that a call is "committed" once response headers arrive, and that retry throttling is a token bucket.
Evolution. The proto3 guide is explicit. Each field has a number that identifies it in the wire format; it cannot be changed once the type is in use and must never be reused. Adding fields is safe, removing fields is safe if the number is reserved, and changing a field's number is not safe. A parser that meets a field it does not know preserves it, and a missing field reads as the default value. That is why gRPC contracts evolve well between teams that deploy independently, as long as numbers are treated as permanent.
Browsers. The gRPC-Web project lets browser clients reach gRPC services. Its README says clients connect through a special proxy (Envoy by default), that it supports unary and server-side streaming, and that client-side and bidirectional streaming are not currently supported. The gRPC-Web basics tutorial uses an Envoy proxy and mentions that CORS setup may be needed.
GraphQL
GraphQL is a query language and execution model. The specification defines three operation types: query (a read-only fetch), mutation (a write followed by a fetch) and subscription (a long-lived request that fetches data in response to source events). A typed schema describes what can be asked, and the client chooses the fields.
Execution rules that matter for failure.
- Fields of a query may be executed in whatever order the executor chooses, normally in parallel. The top-level fields of a mutation are executed serially, in the order written. A mutation with two top-level changes is therefore ordered. The rules specify order, not rollback: they do not say the first change is undone if the second fails.
- A response is a map that may contain
data,errorsandextensions. If a request fails before execution (syntax or validation error),datais absent anderrorsis not empty. If a field error occurs during execution, the field is handled as null, the error is added to theerrorslist, and the response is a partial result:dataanderrorscan both be present. - If a field of a non-null type would be null, the error propagates to the parent field. If every field on the path is non-null,
dataitself becomes null.
How a call travels. The spec is largely transport-independent; the practical rules are in the Serving over HTTP page, which describes recommendations aligned with the GraphQL-over-HTTP specification, still a draft at the time of reading. A GraphQL HTTP server must handle POST for queries and mutations and may accept GET for queries only. The request body carries query, optionally operationName and variables. For the response, the page says that if the response has a non-null data, the server should respond with a 2xx status "even if the response includes errors, since HTTP does not yet have a status code representing 'partial success'". It also notes that GET URLs can hit length limits in browsers and CDNs and mentions persisted documents (a short identifier in place of the full query text) as a workaround.
Strengths and costs.
- One round trip can fetch what a screen needs.
- Because the endpoint is one URL reached mostly by POST, and RFC 9110 makes POST responses cacheable only with explicit freshness information, standard HTTP caching has little to work with.
- Whether a query is expensive depends on what the client wrote, so protection against deep or wide queries has to be added on the server. The spec describes execution, not cost limits. This follows from the design rather than from a cited rule, so treat it as design advice.
- Evolution is by adding fields and types, and the schema is introspectable, so tooling can check a client's query against it.
WebSockets
WebSockets give a long-lived, two-way connection. RFC 6455 describes the problem it replaces: bidirectional communication built from HTTP polling forces many connections, repeats headers on every message and makes the client match replies to requests. A single TCP connection in both directions is the simpler solution.
How a connection travels. The client sends an HTTP Upgrade request carrying a Sec-WebSocket-Key; the server answers 101 Switching Protocols with Sec-WebSocket-Accept. Any other status means the handshake did not complete. The schemes are ws (default port 80) and wss (default port 443, over TLS). After the handshake, messages travel in frames, a client must mask every frame it sends, and control frames include Ping (the receiver must reply with Pong), Pong and Close. Section 5.5.2 notes that Ping can serve as a keepalive or to check that the other side still responds.
Figure 2. Reconnect as part of the design. How to read this: the protocol does not reconnect for you. Section 7.2.3 of RFC 6455 tells clients to use backoff after abnormal closures, with a first delay chosen randomly (it suggests 0 to 5 seconds as reasonable) and longer delays after failures, for example truncated binary exponential backoff.
Failure behaviour. Close code 1006 is reserved to mean the connection closed abnormally, without a close frame, and an endpoint must not send it. RFC 6455 warns that if every client reconnects immediately and persistently after a non-transient problem, the server may experience what amounts to a denial-of-service attack. The MDN WebSockets API page adds a limit of the browser interface: it does not support backpressure, so when messages arrive faster than the application handles them, the device can run out of memory, become unresponsive, or both; MDN points to WebTransport when backpressure matters. Backpressure covers what a design can do about it.
Security. Servers that serve only certain sites should verify the Origin header and answer 403 for an origin they do not expect (section 10.2). The check protects against malicious script running in a trusted browser; it does not stop non-browser clients.
Evolution. None is built in. The message format is yours, so versioning the messages is your job.
Server-Sent Events. If only the server needs to push, MDN's guide to server-sent events describes a simpler, one-way option: a response with media type text/event-stream and an EventSource that by default restarts a closed connection, with a retry field to set the reconnection time. When not used over HTTP/2 the limit is six open connections per browser and domain; over HTTP/2 the number of simultaneous streams is negotiated and defaults to 100.
Comparing the styles
| Resource-style HTTP | gRPC | GraphQL | WebSocket | |
|---|---|---|---|---|
| Contract | Document plus conventions | Protobuf file, code generated | Typed schema | None built in |
| One call travels as | One method and URL | HTTP/2 POST to a method path | POST (or GET for queries) to one URL | Frames on an open connection |
| Where the status lives | HTTP status | grpc-status in trailers, HTTP 200 |
errors in the body, often 2xx |
Close code or your message |
| Safe to retry | By method (GET, PUT, DELETE) | By the method's meaning and UNAVAILABLE |
Queries yes; mutations by design | Your protocol |
| Evolution | Additive changes | Field numbers, reserved | Add fields and types | Your convention |
| Shape | Request and reply | Unary and three streams | Request and reply, subscriptions | Two-way, any time |
| Caches | Standard HTTP caches | Not by URL | Hard: one endpoint, POST | Not applicable |
| Browser | Native | Needs gRPC-Web and a proxy | Native over HTTP | Native |
Failure example: the error a generic monitor cannot see
The failure example is not a crash but silence. A shopper's order page runs a GraphQL query with three fields. The loyalty field fails; the other two succeed. By the rule quoted earlier, the server answers with a 2xx status and a body with data and errors. A dashboard that counts HTTP 5xx responses shows a healthy service while a field fails for every user. The same trap exists in gRPC, where an error response still has HTTP status 200 and the real status is in the trailers.
The fix is to count failures where the protocol puts them: parse errors for GraphQL, read grpc-status for gRPC, and alert on those rates. A generic retry layer is equally blind to a 200 that carried a transient error, so choosing a style is partly choosing where retry and monitoring tools must look. The wider design is in Failure Handling End to End.
Choosing: a decision flow
Figure 3. A first-pass decision per conversation. How to read this: ask the questions for each conversation, not once for the system. The answer for the browser-facing edge is usually different from the answer between two services.
The flow is a starting point; team skills and tooling can override it, so say so in the decision record.
The other side of this topic
The browser sees these styles as a data-fetching choice: how a screen caches, cancels and retries responses, and how it keeps a live view in step. The Frontend System Design track plans a lesson on consuming REST, GraphQL and tRPC from the client; it is not published yet, so there is no link.
In practice: styles per conversation in ShopStream
This is the realistic example, an educational scenario on the course's running example. Numbers marked illustrative are not measurements.
This is a realistic scenario, not a case history.
The situation
ShopStream's Stage 8 has seven services behind a gateway. The browse path peaks near 2,300 requests per second and checkout near 58 per second (the course's shared illustrative figures). Three conversations need an API style: the shopper's app reading and placing orders, services calling each other, and live courier tracking. The team asks whether one style should serve all three.
Starting architecture
Figure 4. The proposed split. How to read this: the solid arrows are request and reply; the dashed arrow is a push. The shopper's app never talks gRPC or holds a socket to the order services; only the Tracking service holds long-lived connections.
Options considered
| Option | Gain | Cost |
|---|---|---|
| A. HTTP and JSON everywhere, tracking by polling | One style, easy to cache | Polling load, delayed positions |
| B. GraphQL for all client calls | Flexible screens | Cost limits, weaker caching, new skills; only one or two screens would benefit |
| C. HTTP and JSON at the edge, gRPC inside, WebSocket for courier updates | Each conversation gets a fitting style | Three contracts to maintain and a socket service to operate |
The decision
Option C. The edge stays HTTP so the CDN and browsers work as they do today. Between services, gRPC gives typed contracts with deadlines that each caller must set explicitly (the guide says none is set by default). Courier positions arrive over a WebSocket because the courier app is a controlled client and the traffic is frequent and two-way. The shopper's tracking view starts with short polling of the BFF and moves to server-sent events when the polling load justifies the change. GraphQL is deferred until several client teams need different shapes of the same data.
Rollout and verification
A rollout would start with one gRPC call (catalogue lookups), with a deadline set from the call's measured latency and retries limited to UNAVAILABLE. Verification checks that gateway and BFF metrics read grpc-status, not only the HTTP status, and watches how many tracking connections drop in a rolling deploy and how fast they return.
The 2 a.m. failure
The page reads: "tracking connections dropped to zero, then login errors spiking". A deploy restarted every Tracking instance at once. Each courier app lost its connection and reconnected immediately, and the authentication service received the whole fleet in a few seconds. First checks: connection count over time, reconnect attempts per second, the auth service's error rate. Mitigation: stop the rollout and shed reconnects at the gateway by answering the handshake with a non-101 status such as 503, which RFC 6455 treats as a handshake that did not complete. Lasting fix: random first delay and growing backoff in the client (the shape in Figure 2), draining instances gradually, and a connection budget per instance.
Architect's notes
- Choose per conversation, and record the five decisions for each in the design document.
- Treat the contract as the product. Protobuf field numbers and GraphQL fields are promises that outlive the code.
- Teach generic layers about errors. Monitors, retries and gateways must read
grpc-statusand GraphQLerrors. - Set a deadline on every gRPC call, and design reconnect and backoff before launching any socket.
- Defer GraphQL until the pain is named, and add cost limits the day it ships.
Common mistakes
- One style for everything, including conversations it fits badly.
- Reading only the HTTP status of a gRPC or GraphQL response.
- Calling GraphQL mutations atomic. The spec orders top-level fields; it does not roll back.
- Reusing a protobuf field number after deleting a field.
- Assuming WebSockets reconnect and replay on their own.
- Leaving gRPC calls without deadlines.
Key takeaways
- A style is five decisions: contract, transport mapping, error model, evolution, conversation shape.
- Resource-style HTTP uses method and status semantics from RFC 9110 and is the style standard caches handle most naturally.
- gRPC calls are HTTP/2 POSTs whose result lives in trailers; deadlines, status codes and field numbers carry its failure and evolution rules.
- GraphQL lets the client shape the response, orders top-level mutation fields, and reports field errors in a partial result, usually with a 2xx status.
- WebSockets give two-way messages after an HTTP upgrade; reconnect, backoff and message format are the application's job, and the browser interface has no backpressure.
- Server-sent events are the simpler choice when only the server pushes.
- Choose per conversation, and make monitoring and retry layers read each style's own error signal.
Practice exercises
Exercise 1 (30 min). Write the "cancel order" call in all four styles. For each, say what a client should do after a timeout.
Exercise 2 (45 min). Take the proto in this lesson. Remove status, add currency, and write the reserved line. Which changes are safe, per the proto3 guide?
Exercise 3 (1 hour). Write the alert rules for GraphQL errors and gRPC grpc-status for the ShopStream order page.
Exercise 4 (2 hours). Apply the decision flow to a food-delivery app's four conversations (menu browse, order placement, kitchen display, driver position). Where do you override the flow, and why?
Related reading
- API Design: resources, pagination, versioning, idempotency keys, error formats.
- ShopStream Stage 8: Services: the same choice as a short list of bullets.
- API Gateway and Service Mesh: the gateway and the BFF pattern.
- Failure Handling End to End and Backpressure.
- Synchronous vs Asynchronous Communication.
Official documentation
- IETF, RFC 9110: HTTP Semantics and RFC 9457: Problem Details for HTTP APIs.
- IETF, RFC 6455: The WebSocket Protocol.
- GraphQL Foundation, GraphQL specification, October 2021 and Serving over HTTP.
- gRPC, Core concepts, Status codes, Deadlines and gRPC over HTTP/2.
- Protocol Buffers, Language guide (proto3).
- MDN, WebSockets API.