API design and pagination · Backend Development
What makes an API easy to use and safe to retry?
Course contentsWhy you need it
An API is a promise to everyone who builds on it, and it's hard to change once people depend on it. A few conventions make it predictable: clear resource URLs, methods that mean what they say, results that come in pages, and requests that can be safely sent twice when the network hiccups.
The intuition
Resources are nouns; methods are verbs.
| Request | Means |
|---|---|
GET /orders | list orders |
POST /orders | create an order |
GET /orders/42 | read order 42 |
PUT /orders/42 | replace order 42 |
PATCH /orders/42 | change some fields |
DELETE /orders/42 | delete it |
Not /getOrders or /deleteOrder?id=42.
Idempotent means doing it twice has the same effect as once. GET, PUT and DELETE are idempotent: deleting order 42 twice still leaves it deleted. POST is not: send it twice and you might create two orders. When the network times out, a client can't know whether a POST arrived, so payment APIs accept an Idempotency-Key header: the server remembers the key and returns the same result for a repeat.
Pagination. Never return a million rows at once.
- Offset:
?page=3&size=20skips 40 rows. Simple, but rows shift when new ones are added. - Cursor:
?after=140&limit=20means "20 items after id 140". Stable and fast; the response says where to continue:"next": 160.
Definition
REST
A style of API where URLs name resources and HTTP methods say what to do with them.
Idempotent
Repeating the request has the same effect as sending it once. GET, PUT, DELETE are; POST isn't.
Cursor pagination
Asking for items after a known position (often an id), with the response saying where to continue.
Examples
Offset pagination
Try it yourselfpage and size become a slice.
ITEMS = [{"id": i, "name": f"item{i}"} for i in range(1, 8)]
def page_of(items, page, size):
start = (page - 1) * size
chunk = items[start:start + size]
return {"items": [it["id"] for it in chunk], "page": page, "has_more": start + size < len(items)}
print(page_of(ITEMS, 1, 3))
print(page_of(ITEMS, 3, 3)){'items': [1, 2, 3], 'page': 1, 'has_more': True}
{'items': [7], 'page': 3, 'has_more': False}
- L4Pages count from 1, so page 1 starts at 0.
- L6Tell the client whether to ask for another page.
- L9Page 3 starts at 6: only item 7 is left.
An idempotency key
Try it yourselfA repeated payment with the same key is answered from memory, not charged again.
CHARGES = []
SEEN = {}
def pay(key, amount):
if key in SEEN:
return SEEN[key]
CHARGES.append(amount)
result = (201, {"charge": len(CHARGES), "amount": amount})
SEEN[key] = result
return result
print(pay("k-1", 30))
print(pay("k-1", 30))
print(pay("k-2", 30))
print(CHARGES)(201, {'charge': 1, 'amount': 30})
(201, {'charge': 1, 'amount': 30})
(201, {'charge': 2, 'amount': 30})
[30, 30]
- L5Seen this key before? Return the stored answer.
- L13A retry after a timeout: same key, same answer, no second charge.
- L15Two real charges: one per key.
[30, 30]
Common mistakes
Verbs in URLs
POST /createUser, GET /deleteUser?id=3 hide what's happening and break caching. Use POST /users and DELETE /users/3.
Changing data with GET
Browsers, crawlers and link previews send GET requests freely. A GET that deletes or pays will be triggered by accident.
Retrying POST blindly
After a timeout the first request may have succeeded. Without an idempotency key, the retry can create a duplicate order or charge.
Quick quiz
1. Which request is NOT idempotent?
Show the answer
Answer: A
Each POST may create a new order; repeating DELETE or PUT leaves the same final state.
2. Which URL design fits REST?
Show the answer
Answer: A
Nouns in the path, the verb in the method.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictPage through
Type exactly what this prints.
- ModifyMake DELETE idempotent
Deleting an order twice should be harmless: the second call should answer
204too (nothing left to delete, same final state). Right now it crashes with a KeyError. Fixdelete_orderso every call returns(204, None). - WriteCursor pagination
Write
page_after(items, after, limit).itemsare dicts sorted by"id". Return a dict:"items": the ids of up tolimititems whose id is greater thanafter(afterisNonefor the first page)"next": the last returned id if there might be more items after it, otherwiseNone
page_after(ITEMS, None, 3) # {'items': [1, 2, 3], 'next': 3} page_after(ITEMS, 3, 3) # {'items': [4, 5, 6], 'next': 6} page_after(ITEMS, 6, 3) # {'items': [7], 'next': None}
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| REST | REST | URLs name resources; HTTP methods say what to do. |
| Idempotent | 幂等 | Doing it twice has the same effect as once. |
| Pagination | 分页 | Returning a big list in pages. |