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.
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#
REST basics#
Name resources, not actions, and use HTTP methods for the action:
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 itUse 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:
Offset: GET /posts?limit=20&offset=4000
Cursor: GET /posts?limit=20&after=9f3aOffset fails when data changes. Here a new post arrives between pages:
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])Offset page 2: ['post8', 'post7', 'post6']
Cursor page 2: ['post7', 'post6', 'post5']
Page 1 ended at: post8The 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:
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)){'status': 'paid', 'amount': 500}
{'status': 'paid', 'amount': 500}
Times charged: 1Versioning#
You will need to change an API without breaking clients:
/v1/orders in the URL: simple, and the most common
Accept: application/vnd.shop.v2+json in a headerAdding a field is usually safe. Removing or renaming one needs a new version, and a period of notice.
Real-time communication#
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
POST /bookmarks { url, title } --> 201
GET /bookmarks?limit=20&after=abc --> 200, next cursor
DELETE /bookmarks/{id} --> 204The user comes from their authentication token, not from the URL.
Key takeaway#
Next: Rate Limiting.
Finished this lesson?
Related lessons
Recommended System Design resources
- System Design Interview by Alex Xu
Step-by-step walkthroughs of common design questions, for interview preparation.
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.