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.
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
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 oneIt 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
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
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:
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
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.