Architectural Styles · Lesson 01
Web API architectural styles overview
Before you can choose the right tool, you need to know what tools exist. Three families dominate web APIs today — and knowing the axis on which they differ is more valuable than memorising any single spec.
By the end you'll be able to
- Name the three main web API style families and give a real-world use-case for each.
- Explain at least four axes on which the styles differ — coupling, response flexibility, performance, caching.
- Describe why production systems commonly use more than one style at the same time.
Why styles exist at all
A web API is a contract that travels over a network. The style is the blueprint that governs how those contracts are shaped — what the request looks like, who decides the response structure, and what the wire format is. Different blueprints optimise for different things. Just as hammers and screwdrivers are both "tools that join things" but are suited to different jobs, REST, GraphQL, and gRPC are all "ways to call remote code" but are tuned for different constraints.
Think of a large newspaper publishing company. The website editors need flexibility to compose article pages with exactly the fields they need. The print-layout system exchanges very high-frequency, tightly-typed messages with typesetters. An external partner portal needs a simple, predictable interface they can bookmark. Each of these is the same business — three different API styles sitting side by side.
The three families
1. Request / response — REST
REST (Representational State Transfer) treats the network as a collection of resources. You identify each resource with a URL and operate on it using the HTTP verbs (GET, POST, PUT, DELETE). The server defines the response shape. REST maps naturally onto the web's own transport and is the dominant style for public APIs.
2. Query — GraphQL
GraphQL flips responsibility for the response shape: the client describes exactly which fields it needs in a query language, and the server returns exactly that — nothing more, nothing less. All queries go to a single endpoint. This buys precision at the cost of more complex server infrastructure and harder caching.
3. Remote Procedure Call — gRPC
gRPC (Google Remote Procedure Call) models interaction as calling a function on a remote machine. The contract is defined in a typed schema language (protobuf), compiled into client and server stubs. Wire encoding is binary, transport is HTTP/2. It excels at internal service-to-service communication where performance and type-safety matter more than human readability.
The axes that separate them
When you compare styles, five axes matter most:
| Axis | REST | GraphQL | gRPC |
|---|---|---|---|
| Coupling | Loose — URL + HTTP verbs | Moderate — schema is shared | Tight — generated from .proto |
| Response flexibility | Server defines shape | Client declares fields | Server defines (typed) |
| Performance | Good (HTTP/1.1–2, JSON) | Good (single trip, but heavier parsing) | Excellent (binary, HTTP/2) |
| Caching | Built-in via HTTP cache | Requires custom strategy | Not built-in |
| Tooling & debugging | Universal — curl, browser | GraphiQL, Playground | grpcurl, generated stubs |
They coexist in real systems
No production system of any size uses only one style. A typical architecture might expose a REST API for third-party integrations (clear versioned contracts), a GraphQL endpoint for its own mobile and web clients (they can request exactly what they need), and gRPC between internal services (maximum throughput, strict contracts). The boundary between the styles becomes an intentional design decision rather than an accident.
A classic interview question: "Name the main web API architectural styles and give a use-case for each." Structure your answer around the three families: REST (public API, partner integrations), GraphQL (mobile or rich-client apps that need tailored payloads), gRPC (high-throughput internal service mesh). Mentioning that the styles often coexist in one system shows senior thinking.
Treating the styles as competing — "REST vs GraphQL vs gRPC" implies only one winner. In practice the question is always "which style fits this interface best?" Picking gRPC for your public developer API, or REST for millisecond-critical internal streaming, is a mistake not because the technology is bad but because the trade-offs are mismatched to the problem.
Do decide on style per interface boundary based on who the client is and what their constraints are. Don't pick a single style organisation-wide and force every use-case into it — you will eventually build workarounds that are worse than using the right style.
Worked example: the same operation in three styles
To make the difference concrete: fetching the name and email of user 42 in all three styles.
# REST — client asks for the whole user resource
GET /users/42
# Response contains every field the server decided to include
{ "id": 42, "name": "Riya", "email": "riya@ex.com", "plan": "pro", ... }
# GraphQL — client declares exactly the two fields it needs
POST /graphql
{ "query": "{ user(id: 42) { name email } }" }
# Response shaped to the query
{ "data": { "user": { "name": "Riya", "email": "riya@ex.com" } } }
# gRPC — call a typed method; proto defines the message
# rpc GetUser (UserRequest) returns (UserResponse)
request: UserRequest { id: 42 }
response: UserResponse { name: "Riya", email: "riya@ex.com" }
# wire bytes are binary protobuf, not JSON
Under the hood: the same task in three styles
Seeing all three styles side by side on an abstract table is useful, but the real intuition comes from tracing actual bytes on the wire. Let's use a concrete operation — retrieve user 42 and their last two orders — and follow each style from request through response, noting exactly what the network sees.
REST — two round-trips
REST models each entity as a separate resource at its own URL. Fetching a user and their orders means two independent HTTP requests. Some REST APIs provide an ?include=orders convenience parameter, but that is a custom extension, not part of the style itself — you cannot count on it.
# Round-trip 1: fetch the user
GET /users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_live_abc123
→ 200 OK
{
"id": 42,
"name": "Riya Sharma",
"email": "riya@example.com",
"plan": "pro",
"created_at": "2023-01-15T09:00:00Z",
"avatar_url": "https://cdn.example.com/avatars/42.jpg"
}
# Round-trip 2: fetch the orders separately
GET /users/42/orders?limit=2&sort=newest HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_live_abc123
→ 200 OK
{
"data": [
{ "id": "ord_9A1", "total": 129.00, "status": "shipped",
"items": [{ "sku": "HDPHN-BLK", "qty": 1 }] },
{ "id": "ord_8B7", "total": 49.00, "status": "delivered",
"items": [{ "sku": "CBL-USBC", "qty": 2 }] }
],
"total": 47,
"next_cursor": "cursor_ord_7C3"
}
Over-fetch: the first response delivers avatar_url, plan, and created_at even though the UI only needs the name and email. Extra round-trip: two sequential network requests are required before the screen can render — each adds at least one RTT of latency. On a mobile network at 150 ms RTT, that is 300 ms of pure waiting before the first byte of order data arrives.
GraphQL — one round-trip, exact fields
GraphQL uses a single endpoint for every query. The client ships a document that names precisely the fields it wants; the server resolves only those fields and returns a response that mirrors the query shape exactly.
POST /graphql HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_live_abc123
Content-Type: application/json
{
"query": "{ user(id: \"42\") { name email orders(last: 2) { id total status } } }"
}
→ 200 OK
{
"data": {
"user": {
"name": "Riya Sharma",
"email": "riya@example.com",
"orders": [
{ "id": "ord_9A1", "total": 129.00, "status": "shipped" },
{ "id": "ord_8B7", "total": 49.00, "status": "delivered" }
]
}
}
}
Zero over-fetch: the response contains exactly the six fields the query requested — no avatar_url, no created_at, no next_cursor. One round-trip: user and orders arrive together. The trade-off is that all requests are POST, which means standard HTTP caches (CDN, browser) cannot cache them by URL alone. A caching strategy must be built on top — typically persisted queries or a client-side normalised cache.
gRPC — one RPC, binary wire
gRPC replaces ad-hoc JSON with a typed schema (protobuf) that both client and server compile into native code. There is no URL negotiation — you call a method by name. The data on the wire is binary protobuf, not JSON, and the transport is HTTP/2.
// Proto definition (shared contract, compiled by both sides)
rpc GetUserWithOrders (UserOrdersRequest) returns (UserOrdersResponse);
message UserOrdersRequest {
string user_id = 1;
int32 order_limit = 2;
}
message UserOrdersResponse {
User user = 1;
repeated Order orders = 2;
}
// What the caller sends (as structured values — actual wire is binary)
UserOrdersRequest { user_id: "42", order_limit: 2 }
// What arrives back
UserOrdersResponse {
user: User { id: "42", name: "Riya Sharma", email: "riya@example.com" }
orders: [
Order { id: "ord_9A1", total: 129.00, status: "SHIPPED" },
Order { id: "ord_8B7", total: 49.00, status: "DELIVERED" }
]
}
// On the wire: HTTP/2 DATA frame carrying binary protobuf
// ~4x smaller than the equivalent JSON, no field-name strings repeated
No over-fetch: the proto schema defines which fields exist; there is nowhere for extra data to hide. One RPC call: user and orders travel together. Binary encoding means the payload is typically 40–70 % smaller than JSON and faster to parse. The constraints: the response body is unreadable in a browser's Network tab, browser support requires a gRPC-Web proxy (Envoy or grpc-gateway), and any schema change requires a proto recompile on both sides.
You can determine which style an unknown API uses purely by observing network traffic. Multiple GET requests to paths like /users/42 and /orders?user=42 is REST — each resource lives at its own URL. A stream of POST requests all going to a single /graphql path, each with a JSON body containing a "query" key, is GraphQL. POST requests to paths like /acme.UserService/GetUser with Content-Type: application/grpc and an unreadable binary body is gRPC-Web. This pattern recognition is the first step in any debugging session involving an API you did not build yourself.
How to debug & inspect it
The fastest way to identify which API style you are looking at is to read the traffic — the URL pattern, the HTTP method, and the first few bytes of the response body each tell a distinct story. Here is how to reproduce those signals with curl and grpcurl, and what to look for in the browser Network tab.
In the browser Network tab, the tell-tale signatures:
| What you see in Network | Style | Why |
|---|---|---|
Multiple GET requests, each to a different URL like /users/42, /orders?user=42 |
REST | Each resource has its own URL; separate reads require separate GETs |
All requests go to POST /graphql; response body always has a "data" key |
GraphQL | Single endpoint model; all operations are POSTs to /graphql |
POST to /acme.Service/MethodName with Content-Type: application/grpc; binary body (unreadable in the preview pane) |
gRPC-Web | gRPC method names use the package.Service/Method convention; body is binary protobuf |
HTTP status always 200, but the response body contains an "errors" array |
GraphQL | GraphQL encodes errors in the response body, not the HTTP status code |
Symptom to cause to fix:
| Symptom | Cause | Fix |
|---|---|---|
| REST: page requires 10+ network calls to load | Under-fetching — server resources are too granular for the screen's data needs | Add a ?include= or ?expand= parameter; or introduce a BFF aggregating the calls server-side |
| REST: responses are large but the UI only uses 3 fields | Over-fetching — server defines a fixed shape that includes everything | Add sparse fieldsets (?fields=id,name,email); or evaluate whether GraphQL fits this client better |
| GraphQL: HTTP 200 but UI shows error | Field-level error in the "errors" array — HTTP monitoring missed it |
Parse the response body for the "errors" key in addition to checking HTTP status |
| gRPC: connection refused on port 50051 | No gRPC-Web proxy in front of the gRPC server (browsers cannot speak raw HTTP/2 gRPC) | Add Envoy or grpc-gateway as a proxy; or use the grpc-web transpiled client library |
GraphQL: 200 OK with no "data" key at all |
Top-level parse error — malformed query string or wrong Content-Type |
Ensure Content-Type: application/json and that the "query" value is a valid string |
Debug checklist:
- Open the Network tab; note the URL pattern and HTTP method — does it look like
/resource/id(REST),POST /graphql(GraphQL), orPOST /pkg.Service/Methodwith a binary body (gRPC-Web)? - For REST: check status codes. A 200 response with an error message in the body is a REST design mistake — status codes should carry semantic meaning.
- For GraphQL: always inspect the response body for the
"errors"key even when HTTP status is 200. Alerting and monitoring that only watches HTTP status codes will silently miss GraphQL errors. - For gRPC-Web: use
grpcurlwith the-plaintextflag for local services, or enable proto reflection on the server; binary bodies in the Network tab preview pane are not human-readable. - Over-fetch check: count the fields in the response versus the fields your UI actually uses. If the ratio is higher than 3:1, that is a signal to add sparse fieldsets or to consider moving the client to GraphQL.
🧠 Quick check
1. Which style lets the client choose which fields appear in the response?
GraphQL's defining feature is that the client writes a query that lists exactly the fields it needs — over-fetching and under-fetching both disappear. In REST and gRPC the server defines the response shape.
2. Which style encodes messages as binary and runs over HTTP/2 by default?
gRPC uses Protocol Buffers (binary encoding) over HTTP/2, which is what gives it its performance advantage over JSON-over-HTTP/1.1.
3. REST gets built-in caching "for free" primarily because of:
REST maps operations onto HTTP methods. GET requests are idempotent and cacheable by the HTTP spec; proxies, CDNs, and browsers cache them without extra work. GraphQL POSTs and gRPC calls don't benefit from this by default.
4. A company wants a high-throughput, strongly-typed link between two backend services with no browser clients. Which style fits best?
gRPC excels at internal service-to-service communication: binary encoding is fast, the proto schema enforces types on both sides, and HTTP/2 supports multiplexing. Browser support limitations don't matter if there are no browser clients.
✍️ Exercise: classify an API landscape (try before opening)
You join a team with three interfaces: (a) a payments API consumed by third-party fintech apps; (b) a BFF (backend-for-frontend) consumed only by the company's own iOS and Android apps; (c) a notifications service that emits events to an analytics pipeline inside the same data-centre. Recommend a style for each and justify it in one sentence each.
Model answer:
- (a) Payments API → REST. External partners need a stable, well-documented, versioned contract with predictable URLs and HTTP semantics — REST's loose coupling and ubiquitous tooling make it easiest to adopt.
- (b) Mobile BFF → GraphQL. iOS and Android screens need different field subsets from the same data; GraphQL lets each client declare exactly what it needs without multiple round-trips or server endpoint proliferation.
- (c) Notifications → gRPC. Internal, latency-sensitive, high-frequency messages with a machine-readable schema; binary encoding and HTTP/2 multiplexing minimise overhead on the private network.
Rubric: ✓ correct style named for each ✓ justification references at least one axis (coupling / field flexibility / performance) ✓ no style recommended for the wrong reason (e.g. "GraphQL is faster" or "REST is newer").
Key takeaways
- Three main web API families: REST (resources + HTTP), GraphQL (client-declared queries), gRPC (typed binary RPC).
- Key axes: coupling, response flexibility, performance, caching, tooling.
- REST caches easily via HTTP; GraphQL gives clients field precision; gRPC gives maximum throughput and type-safety.
- Real production systems regularly use all three — the choice is per interface boundary, not per organisation.