Ecommet
Topic 14 of 75

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.

Intermediate 6 min read 4 quiz questions

Jump to the quiz

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):

RequestMeaning
GET /orders?status=paidList orders, filtered
GET /orders/42Read order 42
POST /ordersCreate an order
PATCH /orders/42Change part of order 42
DELETE /orders/42Delete order 42
GET /orders/42/itemsItems 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

gRPC between services orders.proto → Checkout service: generates orders.proto → Order service: generates Checkout service → Order service: CreateOrder() orders.protothe contractCheckout servicegenerated clientOrder servicegenerated servergeneratesgeneratesCreateOrder()
One contract file generates typed clients and servers; calls travel as compact binary messages over HTTP/2.
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)

Java
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 }
          }
        }
        """;
}
Python
# 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

RESTgRPCGraphQL
ShapeResources + HTTP methodsFunctions + typed messagesOne endpoint + query language
FormatUsually JSONProtocol Buffers (binary)JSON
CachingExcellent (HTTP caching)Do it yourselfHarder; needs client or persisted-query caching
Best forPublic APIs, simple CRUD, anything browsers callService-to-service, low latency, streamingApps 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

Java
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 50
Python
import 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 50

Version 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

1. Which is a resource-oriented REST URL?
Show the answer

Answer: /orders/42. REST URLs name resources; the HTTP method says what to do with them.

2. What encoding does gRPC use by default?
Show the answer

Answer: Protocol Buffers. Protocol Buffers: a compact binary format generated from .proto files.

3. What is a common server-side problem with naive GraphQL resolvers?
Show the answer

Answer: The N+1 query problem. Fetching a list and then one query per item; batching (for example DataLoader) fixes it.

4. Why is cursor pagination preferred for large lists?
Show the answer

Answer: Fast at any depth and stable when rows are added. It seeks directly to the last item seen using an index.

Further reading

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.

Next topic