API Styles: REST, RPC and gRPC, GraphQL
How services expose their functions: REST around resources, RPC and gRPC around actions with strict contracts, and GraphQL letting clients ask for exactly the fields they need. Plus pagination and versioning that keep APIs usable as they grow.
In a nutshell
- REST: resources at URLs, standard HTTP methods; simple, cacheable, universal. Best default for public APIs.
- RPC / gRPC: call functions on another service with typed contracts; fast and efficient between your own services.
- GraphQL: one endpoint, the client chooses fields; great for varied clients, harder to cache and protect.
- Whatever the style: paginate lists with cursors and version changes so old clients keep working.
REST: resources and verbs
REST (Representational State Transfer) models an API as resources (nouns) at URLs, acted on with standard HTTP methods (verbs):
| Request | Meaning |
|---|---|
GET /orders?status=paid | List orders, filtered |
GET /orders/42 | Read order 42 |
POST /orders | Create an order |
PATCH /orders/42 | Change part of order 42 |
DELETE /orders/42 | Delete order 42 |
GET /orders/42/items | Items of order 42 (a sub-resource) |
REST's strength is that it reuses HTTP: status codes, caching headers, authentication, tooling, CDNs and proxies all work out of the box, and any language can call it. Its weaknesses are over-fetching (the endpoint returns fields you do not need) and under-fetching (you need several calls to build one screen).
RPC and gRPC: calling functions remotely
Remote procedure call (RPC) styles model an API as functions: CreateOrder, GetUser, ChargeCard. The client calls them almost as if they were local.
gRPC, created at Google and now an open-source project, is the most common modern RPC framework. You describe services and messages in a .proto file; code generators produce typed clients and servers in many languages. By default messages are encoded with Protocol Buffers, a compact binary format, and sent over HTTP/2, which allows streaming in both directions.
gRPC between services
Diagram as text
- orders.proto → Checkout service: generates
- orders.proto → Order service: generates
- Checkout service → Order service: CreateOrder()
gRPC shines between your own services: strict contracts catch mismatches at build time, binary messages are small and fast, and deadlines and streaming are built in. It is less convenient for browsers (which need a proxy layer such as gRPC-Web) and harder to inspect by hand than JSON.
GraphQL: the client asks for what it needs
GraphQL, created at Facebook and open-sourced in 2015, exposes one endpoint and a typed schema. The client sends a query describing exactly the data it wants, possibly across several related objects, and gets exactly that back in one round trip.
How a client might fetch an order page with REST, and with GraphQL (shown as plain strings, in both languages)
class OrderPage {
// REST: three calls, each returning whole objects.
static final String[] REST_CALLS = {
"GET /orders/42",
"GET /orders/42/items",
"GET /customers/7"
};
// GraphQL: one call, only the fields the page shows.
static final String GRAPHQL_QUERY = """
query {
order(id: 42) {
status
total
items { name quantity }
customer { name }
}
}
""";
}# REST: three calls, each returning whole objects.
REST_CALLS = [
"GET /orders/42",
"GET /orders/42/items",
"GET /customers/7",
]
# GraphQL: one call, only the fields the page shows.
GRAPHQL_QUERY = """
query {
order(id: 42) {
status
total
items { name quantity }
customer { name }
}
}
"""- Good: no over- or under-fetching, one round trip per screen, a self-documenting schema; ideal when many different clients (web, iOS, Android) need different shapes of data.
- Costs: HTTP caching is harder (everything is a POST to one URL), a single query can ask for a huge amount of work (limit query depth and cost), and naive resolvers cause the N+1 query problem (one database query per item); batching tools such as DataLoader fix it.
Choosing a style
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Shape | Resources + HTTP methods | Functions + typed messages | One endpoint + query language |
| Format | Usually JSON | Protocol Buffers (binary) | JSON |
| Caching | Excellent (HTTP caching) | Do it yourself | Harder; needs client or persisted-query caching |
| Best for | Public APIs, simple CRUD, anything browsers call | Service-to-service, low latency, streaming | Apps with many screens and several client types |
Pagination and versioning
Two habits keep any API healthy as it grows.
Paginate with cursors
Never return an unbounded list. Offset pagination (?page=500) is simple but slow on large tables (the database still walks past every skipped row) and skips or repeats items when rows are added between pages. Cursor pagination returns an opaque token pointing at the last item seen; the next request asks for items after it, which uses an index and stays fast and stable.
An opaque cursor: encode the last id seen, decode it on the next request
import java.nio.charset.StandardCharsets;
import java.util.Base64;
class Cursor {
static String encode(long lastId) {
String raw = "id:" + lastId;
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(raw.getBytes(StandardCharsets.UTF_8));
}
static long decode(String cursor) {
String raw = new String(Base64.getUrlDecoder().decode(cursor), StandardCharsets.UTF_8);
if (!raw.startsWith("id:")) {
throw new IllegalArgumentException("bad cursor");
}
return Long.parseLong(raw.substring(3));
}
}
// SELECT * FROM orders WHERE id > :lastId ORDER BY id LIMIT 50import base64
def encode_cursor(last_id):
raw = f"id:{last_id}".encode()
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
def decode_cursor(cursor):
padded = cursor + "=" * (-len(cursor) % 4)
raw = base64.urlsafe_b64decode(padded).decode()
if not raw.startswith("id:"):
raise ValueError("bad cursor")
return int(raw[3:])
# SELECT * FROM orders WHERE id > :last_id ORDER BY id LIMIT 50Version without breaking clients
Mobile apps stay installed for years, so old clients keep calling old shapes. Add fields freely, but never remove or rename them in place. For breaking changes, publish a new version (/v2/orders, a version header, or a new GraphQL field) and retire the old one only when traffic to it is gone.
Common mistakes
- Designing REST URLs around actions (
/createOrder,/getUserById) instead of resources. - Returning unbounded lists that work in development and time out in production.
- Exposing GraphQL publicly without query depth and cost limits.
- Making breaking changes to an API that installed mobile apps still call.
Interview questions
Try answering out loud before you open each one.
When would you choose gRPC over REST?
For internal service-to-service calls where you control both sides and want low latency, small payloads, strict typed contracts and streaming. REST stays better for public APIs and browsers, where universality, readability and HTTP caching matter more.
What problem does GraphQL solve, and what does it cost?
It lets each client fetch exactly the fields it needs in one round trip, solving over-fetching and multiple calls per screen. The costs are harder HTTP caching, the risk of expensive queries (needing depth and cost limits), and N+1 queries on the server unless resolvers batch.
Why prefer cursor pagination over offsets?
Offsets make the database skip rows (slow deep pages) and shift when data changes (skipped or duplicated items). A cursor such as "after id 1050" uses an index, stays fast at any depth and is stable while new rows are inserted.
Check yourself
Related topics
Further reading
- gRPC introduction grpc.io
- Introduction to GraphQL graphql.org
- Architectural Styles and the Design of Network-based Software Architectures (REST, chapter 5) Roy Fielding, 2000
Your notes
Saved in this browser as you type.
Finished this topic?
Mark it read to track your progress, then carry on with the next one.