System designCore7 min

APIs: REST, gRPC & GraphQL

Three ways to let other programs talk to yours, shown on one music app: what each is good at, what each costs, and the two habits, paging and versioning, that keep any API usable as it grows.

1 · The idea

An API is a promise to everyone who calls you

A music streaming service has a phone app, a website, a smart-speaker app, and dozens of its own services behind the scenes. They all need to ask the same questions: what is in this playlist, add this song, play the next track. The API is the agreed way to ask.

Phone appWebsiteSmart speakerMusic APIServices behind itPhone appWebsiteSmart speakerMusic APIServices behind it

Every caller builds on the API. Change it carelessly, and apps you do not control stop working.

That is why an API is a promise: once people depend on it, you can add to it, but taking things away breaks them. There are three popular styles for making that promise. Each is shown here on the same app.

2 · REST

REST: things with addresses, and HTTP methods to act on them

REST treats everything as a resource with its own URL (the nouns) and uses the HTTP methods as the verbs:

The playlist, as REST

http
GET    /playlists/77              read the playlist
GET    /playlists/77/songs        list its songs
POST   /playlists/77/songs        add a song
DELETE /playlists/77/songs/912    remove one

It is simple, every language and tool speaks it, and responses to GET requests can be cached anywhere along the way. Its weakness shows on a screen that needs several things at once. The playlist screen needs the playlist, its songs, and each song's artist:

Three round trips before the screen can draw, each perhaps 100 ms on a mobile network. And each song arrives with every field, when the screen shows only three.

Those two problems have names: under-fetching, needing several calls for one screen, and over-fetching, getting far more data than you use. On a phone with a slow connection, both hurt.

3 · GraphQL

GraphQL: the client asks for exactly the shape it needs

GraphQL turns it around. There is one endpoint, and the client sends a query describing precisely the data it wants, nested however it likes:

The whole playlist screen, in one request

graphql
query {
  playlist(id: 77) {
    name
    songs {
      title
      duration
      artist { name }
    }
  }
}

One round trip, and nothing the screen does not show. It shines when many different screens and apps need different slices of the same data. It has costs:

  • Caching is harder. Queries usually go to one URL, often as POST, so the ordinary HTTP caches along the way cannot reuse the answers.
  • The server must defend itself. A client can write a query that asks for everything, nested ten levels deep. Servers limit query depth and cost.
  • More work in the server. It has to understand arbitrary queries and gather the data for each.

4 · gRPC

gRPC: calling another service as if it were a function

Behind the scenes, the music app's own services talk to each other constantly. For that, gRPC is popular. You describe the calls in a contract file, and tools generate the code for both sides, so calling another service looks like calling a function:

The contract both services share

protobuf
service Playlists {
  rpc GetSongs (PlaylistId) returns (SongList);
  rpc AddSong (AddSongRequest) returns (Song);
}

message PlaylistId { int64 id = 1; }

Messages travel in a compact binary format over HTTP/2, so they are small and fast, and the contract means a mistyped field is caught when the code is built, not in production. It can also stream: send or receive a sequence of messages over one call. The costs: you cannot read the messages by eye, and browsers cannot call gRPC directly without a translating proxy. It is at its best between services you control.

5 · Choosing

Pick the style by who is calling

Style

Best when

Watch out for

REST

Anyone may call you, including other companies; responses should be cacheable

Several round trips per screen; responses with more than you need

GraphQL

Many apps and screens need different slices of the same data

Harder caching; queries that ask for too much

gRPC

Your own services calling each other, often and fast

Not readable by eye; browsers need a proxy

They are not rivals. A common layout uses more than one:

Apps and websitePublic APIREST or GraphQLPlaylist serviceSong serviceUser serviceREST or GraphQLgRPCgRPCgRPCApps and websitePublic APIREST or GraphQLPlaylist serviceSong serviceUser serviceREST or GraphQLgRPCgRPCgRPC

An easy, widely understood style at the edge for callers you do not control; a fast, strict one inside, where you control both ends.

6 · Living with it

Paging and versioning keep an API usable as it grows

Two problems appear in every API sooner or later, whatever its style. The first: a popular playlist has 10,000 songs. Returning them all in one response is slow for everyone, so you return them a page at a time, say 50 per page.

There are two ways to ask for the next page:

  • Offset: "skip 100, give me 50". Simple, but if a song is added at the top while you read, every song shifts down one and you see one twice. And the store must count past every skipped item, so page 200 is slow.
  • Cursor: "give me 50 after song 912". The answer does not shift when songs are added, and it is fast at any depth. You lose the ability to jump straight to page 200, which people rarely need.

Aspect

Offset

Cursor

The request

Skip 100, give me 50

Give me 50 after song 912

A song is added at the top

Everything shifts down one; you see one song twice

Nothing shifts

Page 200

Slow: the store counts past 9,950 skipped songs

As fast as page 1

Jump straight to page 200

Yes

No

The second problem: the API has to change, and the apps already installed on millions of phones do not change with it. The rules that keep them working:

  • Adding is safe. A new field in a response does no harm; old apps ignore what they do not know.
  • Removing or renaming is not. An app that reads the old field breaks the day it disappears.
  • For a breaking change, add a new version alongside the old one, for example /v2/playlists, and keep the old version running until its callers have moved.

Check yourself

5 questions

1. A phone screen needs a playlist, its songs and their artists. With a plain REST API it takes three requests. What is this problem called, and which style addresses it directly?
2. Your company's own services call each other thousands of times a second. Which style fits best?
3. Why is GraphQL harder to cache than REST?
4. Users paging through a playlist with "skip 100, take 50" sometimes see a song twice. Why, and what fixes it?
5. You want to rename the field "title" to "name" in a public API. What is the safe way?

Takeaways

Remember this

  • An API is a promise: adding to it is safe, taking away breaks the people who depend on it.
  • REST: resources with URLs and HTTP methods; simple and cacheable, but screens may need several calls.
  • GraphQL: the client asks for exactly the nested data it needs in one request; caching and query costs need care.
  • gRPC: compact, typed calls between services you control; not for browsers directly.
  • Page long lists with cursors, and make breaking changes as a new version while the old one keeps running.