Skip to content
Laviyx Learn

Try

System Design course menu

API Design for System Design Interviews: REST, GraphQL and gRPC

Design clear APIs: REST conventions, GraphQL and gRPC compared, pagination with cursors, idempotency keys, versioning, and real-time options such as WebSockets.

Intermediate5 min readUpdated
On this page

An API is the contract between parts of a system. Once others depend on it, it is hard to change, so it is worth thinking about early.

Choosing a style#

StyleHow it worksGood for
RESTResources and HTTP methodsPublic APIs, web apps
GraphQLClient asks for exact fieldsApps with varied data needs
gRPCBinary messages, defined by a schemaFast calls between internal services

REST basics#

Name resources, not actions, and use HTTP methods for the action:

Text
GET    /users/42/orders        list a user's orders
POST   /orders                 create an order
GET    /orders/1001            read one
PATCH  /orders/1001            change part of it
DELETE /orders/1001            delete it

Use status codes correctly: 200 OK, 201 created, 400 bad request, 401 not signed in, 403 not allowed, 404 not found, 429 too many requests, 500 server error, and 503 unavailable.

See Building a REST API for a working example.

Pagination#

Never return an unlimited list. There are two common methods:

Text
Offset:  GET /posts?limit=20&offset=4000
Cursor:  GET /posts?limit=20&after=9f3a
OffsetCursor
Jump to page 50YesNo
Speed on deep pagesSlows downStays fast
New items addedCan repeat or skipStable

Offset fails when data changes. Here a new post arrives between pages:

Python
posts = [f"post{i}" for i in range(10, 0, -1)]


def by_offset(data, offset, limit=3):
    return data[offset:offset + limit]


def by_cursor(data, after, limit=3):
    start = data.index(after) + 1 if after else 0
    return data[start:start + limit]


page1 = by_offset(posts, 0)
page1c = by_cursor(posts, None)
posts.insert(0, "post11")

print("Offset page 2:", by_offset(posts, 3))
print("Cursor page 2:", by_cursor(posts, page1c[-1]))
print("Page 1 ended at:", page1[-1])
Output
Offset page 2: ['post8', 'post7', 'post6']
Cursor page 2: ['post7', 'post6', 'post5']
Page 1 ended at: post8

The offset version shows post8 twice. Feeds and timelines therefore use cursors.

Idempotency#

An operation is idempotent if doing it twice has the same effect as once. GET, PUT and DELETE should be, but POST is not, by default. Networks fail, so clients retry, and without protection, a retry can charge a card twice.

The fix is an idempotency key: the client sends a unique ID, and the server remembers the result:

Python
results = {}
charges = []


def charge(key, amount):
    if key in results:
        return results[key]
    charges.append(amount)
    result = {"status": "paid", "amount": amount}
    results[key] = result
    return result


print(charge("k-123", 500))
print(charge("k-123", 500))
print("Times charged:", len(charges))
Output
{'status': 'paid', 'amount': 500}
{'status': 'paid', 'amount': 500}
Times charged: 1

Versioning#

You will need to change an API without breaking clients:

Text
/v1/orders       in the URL: simple, and the most common
Accept: application/vnd.shop.v2+json      in a header

Adding a field is usually safe. Removing or renaming one needs a new version, and a period of notice.

Real-time communication#

MethodHowGood for
Short pollingClient asks every few secondsSimple, rare updates
Long pollingServer holds the request until there is newsWhen others are unavailable
Server-sent eventsServer streams over one connectionNotifications, live scores, AI text
WebSocketTwo-way open connectionChat, games, collaboration

Good API habits#

  • Validate all input.
  • Use consistent names and error shapes.
  • Require authentication, and rate limit.
  • Return only what is needed, and paginate lists.
  • Document with OpenAPI.

Common mistakes#

Practice#

Practice exercise

Design the endpoints for a simple bookmarks service: add, list with pagination, and delete.

Show solution
Text
POST   /bookmarks                  { url, title }     --> 201
GET    /bookmarks?limit=20&after=abc                  --> 200, next cursor
DELETE /bookmarks/{id}                                --> 204

The user comes from their authentication token, not from the URL.

Key takeaway#

Next: Rate Limiting.

Finished this lesson?

Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.