4.5.6 · D2Software Engineering

Visual walkthrough — REST API design — resources, HTTP methods, status codes, versioning, pagination

1,928 words9 min readBack to topic

We will only assume you know what a list of items is (like rows in a spreadsheet). Everything else — index, window, offset, ceiling — we define as it appears.


Step 1 — What is the raw thing we are cutting up?

WHAT. Imagine your database table is just a tall stack of items in a fixed order. We draw them as boxes stacked top to bottom.

WHY. Before we can talk about "page 3" we must agree on what a page is made of. It's made of items sitting at positions. So first, positions.

PICTURE. In the figure, each box is one item (a user). To its left is a number called the index — the item's position counted from zero. The first item is at index , the next at index , and so on. We count from (not ) because almost every programming language and database slices lists that way; matching it avoids off-by-one bugs.

Figure — REST API design — resources, HTTP methods, status codes, versioning, pagination

Step 2 — What is a "page"? A window of fixed height

WHAT. A page is a contiguous block of items — a window — of a fixed height we call (the page size / "limit"). Here we pick so it fits on screen.

WHY. We paginate because sending 50 million items at once is slow and wasteful; the client only wants one screenful. So we hand back one window at a time. The window has a chosen height so the client (and our server memory) always knows the maximum work per request.

PICTURE. The coloured bracket in the figure is one window of height . Slide that same-height bracket down the stack and each position gives you a different page.

Figure — REST API design — resources, HTTP methods, status codes, versioning, pagination

Step 3 — Where does page start? Discovering the offset

WHAT. Label pages the way humans do: page , page , page (1-indexed — humans start counting at one, even though item indices start at zero). We want: for page number , which item index does the window's top sit on? Call that number the offset — the count of items we skip before the window begins.

WHY. The database can't act on "page 3" — it only understands "skip this many rows, then take ." So we must translate the human page number into a machine skip-count.

PICTURE. Look at the three windows in the figure with :

  • Page skips items → starts at index .
  • Page skips items → starts at index .
  • Page skips items → starts at index .

Each new page skips one more full window than the last. Page skips windows, page skips window, page skips windows. Each skipped window is items tall, so:

The is doing the "convert human 1-based page to zero-based count of pages above me" job; multiplying by turns "pages above" into "items above."

Figure — REST API design — resources, HTTP methods, status codes, versioning, pagination

Step 4 — Which exact indices does page contain?

WHAT. The window starts at index and is tall, so it covers the indices from there up to but not including .

WHY. We use a half-open interval — square bracket includes the left end, round bracket excludes the right end. This is the standard slicing convention: the end of one page is exactly the start of the next, with no gap and no overlap. If we included both ends, page 1 and page 2 would share a row (duplicate). If we excluded both, one row would fall through the crack.

PICTURE. The figure shows pages with : page 1 = indices , page 2 = , page 3 = . The boundary line is the top of page 2 and not part of page 1 — that's the round bracket at work.

Figure — REST API design — resources, HTTP methods, status codes, versioning, pagination

Step 5 — How many pages total? The ceiling appears

WHAT. Given total items and page size , how many pages cover them all? We want the smallest number of windows of height that together reach every item.

WHY. If we divide and just chop off the decimals (that's the floor, written , "round down"), we throw away the last partial page. We must round up — that operation is the ceiling, written . Ceiling answers exactly the question "how many full-or-partial windows do I need to cover items?"

PICTURE. Take , . The figure stacks 23 items. Two full windows of 10 cover indices . That leaves 3 leftover items (indices ) still uncovered — they demand a third page even though it's only partly full.

  • → would silently lose the last 3 items. ✗
  • → covers everyone. ✅
Figure — REST API design — resources, HTTP methods, status codes, versioning, pagination

The integer trick. Real code often has no ceiling for integers, so we rewrite ceiling using floor: Adding before flooring nudges any nonzero remainder up into the next whole page, while a perfectly-divisible (remainder ) is not pushed over. Check : ✅. Check : ✅ (exactly divisible, no extra page).


Step 6 — Edge & degenerate cases (never surprise the reader)

WHAT. We now walk every corner case so no input breaks the formulas.

WHY. Contracts fail at boundaries: empty tables, a last page that isn't full, asking for a page past the end, and moving data. Each deserves an explicit answer.

PICTURE. The figure shows four mini-scenarios side by side.

  1. Empty table, . pages. A GET still succeeds with an empty list and 200 OKnot 404 (the collection exists; it's just empty).
  2. Last page not full. , page 3 is indices but only exist. The window is "up to ", so you return 3 items, not 10. Range stays .
  3. Offset past the end. Ask page 5 when only 3 pages exist: offset . The slice is empty → return an empty list with 200 OK, not an error. There simply are no rows there.
  4. Moving data. Someone inserts a new item at index between your page-1 and page-2 requests. Everything shifts down by one, so the old "index 9" becomes "index 10" and you see it twice (once on page 1, again on page 2). Offset pagination is not stable under writes — for live feeds switch to cursor pagination (see Database indexing for why WHERE id > cursor stays fast).
Figure — REST API design — resources, HTTP methods, status codes, versioning, pagination

The one-picture summary

The whole derivation on one axis: the stack of indexed items, the fixed-height window sliding down by each page, the offset arrow , the half-open ranges, and the ragged last page that forces the ceiling.

Figure — REST API design — resources, HTTP methods, status codes, versioning, pagination
Recall Feynman retelling — say it in plain words

Picture users stacked in a tower, numbered from zero. A page is just a window of a fixed height that I slide down the tower. To get to page , I step over all the whole windows sitting above me — there are of them, each tall — so I skip items; that skip-count is the offset. My window then grabs the indices from where I stopped up to (not including) the next window's start, so pages tile the tower perfectly with no gaps or overlaps. To count how many pages the whole tower needs, I divide height by window size — but if there's any leftover shorter-than- bit at the bottom, I still need one more window, so I round up (the ceiling), never down. Empty tower → zero pages, and a GET still says 200 with an empty list. Ask for a window past the bottom → also empty, still 200. And if someone slips a new item in at the top while I'm reading, everything shifts and I might see a row twice — that's why live feeds use a cursor instead of an offset.

Recall

Why offset for page with size ? ::: — skip the whole windows above, each tall. Which indices does page hold? ::: The half-open range items, left included, right excluded. Why ceiling, not floor, for total pages? ::: A leftover partial window still needs its own page; floor would drop it. . Integer form of the ceiling? ::: — the pushes any nonzero remainder into the next page. What status code for an empty or out-of-range page? ::: 200 OK with an empty list — the collection exists. When does offset pagination duplicate/skip rows? ::: When the dataset changes between requests; use cursor pagination for feeds.


Related: HTTP protocol · Idempotency and retries · Caching and ETags · API versioning and backward compatibility · OpenAPI / Swagger · GraphQL