Everyone Can Learn Program
中文 Start learning
Specialty · BackendLesson 6 of 6Free to read

API design and pagination · Backend Development

What makes an API easy to use and safe to retry?

Course contents

Why 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.

RequestMeans
GET /orderslist orders
POST /orderscreate an order
GET /orders/42read order 42
PUT /orders/42replace order 42
PATCH /orders/42change some fields
DELETE /orders/42delete 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=20 skips 40 rows. Simple, but rows shift when new ones are added.
  • Cursor: ?after=140&limit=20 means "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 yourself

page 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))
Output
{'items': [1, 2, 3], 'page': 1, 'has_more': True}
{'items': [7], 'page': 3, 'has_more': False}
Step by step
  1. L4Pages count from 1, so page 1 starts at 0.
  2. L6Tell the client whether to ask for another page.
  3. L9Page 3 starts at 6: only item 7 is left.

An idempotency key

Try it yourself

A 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)
Output
(201, {'charge': 1, 'amount': 30})
(201, {'charge': 1, 'amount': 30})
(201, {'charge': 2, 'amount': 30})
[30, 30]
Step by step
  1. L5Seen this key before? Return the stored answer.
  2. L13A retry after a timeout: same key, same answer, no second charge.
  3. 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?

  1. APOST /orders
  2. BDELETE /orders/7
  3. CPUT /orders/7
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?

  1. AGET /users/7/orders
  2. BGET /getOrdersForUser?id=7
  3. CPOST /users/7/listOrders
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.

  1. PredictPage through

    Type exactly what this prints.

  2. ModifyMake DELETE idempotent

    Deleting an order twice should be harmless: the second call should answer 204 too (nothing left to delete, same final state). Right now it crashes with a KeyError. Fix delete_order so every call returns (204, None).

  3. WriteCursor pagination

    Write page_after(items, after, limit). items are dicts sorted by "id". Return a dict:

    • "items": the ids of up to limit items whose id is greater than after (after is None for the first page)
    • "next": the last returned id if there might be more items after it, otherwise None
    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

TermChineseDefinition
RESTRESTURLs 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.