4.5.6 · D5Software Engineering

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

1,655 words8 min readBack to topic

Before we start, a tiny glossary so no word here is used before it's defined:

  • Resource = a "thing" your API exposes (a user, an order) that lives at a fixed address.
  • URI = that address, e.g. /users/42 — think of it as the resource's house number.
  • HTTP method = the verb (GET, POST, PUT, PATCH, DELETE) that says what you want to do to the resource.
  • Status code = the 3-digit number the server sends back describing what happened.
  • Idempotent = doing the same request twice leaves the server in the same final state as doing it once.

True or false — justify

TRUE or FALSE: GET is guaranteed to be safe, so a GET request can never change anything on the server.
TRUE in spec: GET is defined as safe (read-only). The trap is that a badly built API might mutate state on GET — that's a bug, not permission; caches and crawlers assume GET is safe and may replay it.
TRUE or FALSE: Every idempotent method is also safe.
FALSE. DELETE and PUT are idempotent (repeat → same final state) but not safe, because they do change server state. Safe ⊂ Idempotent, not the other way around.
TRUE or FALSE: POST is idempotent because retrying a failed create just makes the same user again.
FALSE. POST is not idempotent — each successful call creates a new resource, so a retry after a silent success can create a duplicate. See Idempotency and retries for how idempotency keys fix this.
TRUE or FALSE: A 200 OK with {"error": "invalid password"} in the body is an acceptable way to report a failed login.
FALSE. HTTP status is the machine-readable outcome that caches, proxies and client libraries branch on; a failed login must be 401 so tooling can tell failure from success without parsing the body.
TRUE or FALSE: Returning 404 when a user tries to access a resource they aren't allowed to see is always wrong.
FALSE — it's sometimes deliberate. 403 Forbidden is technically correct, but some APIs return 404 on purpose so an attacker can't even learn the resource exists (information hiding). Both are defensible; 200 is not.
TRUE or FALSE: Adding a new optional field to a JSON response is a breaking change and needs a new API version.
FALSE. Additive changes (new optional field, new endpoint) are normally non-breaking — old clients ignore the new field. See API versioning and backward compatibility.
TRUE or FALSE: Cursor pagination is strictly better than offset pagination, so you should always use it.
FALSE. Cursor pagination is stable and fast at any depth but cannot jump to an arbitrary page ("go to page 137"). Offset pagination allows random-access jumps; the right choice depends on whether users need to jump around.
TRUE or FALSE: PUT /users/42 and PATCH /users/42 with body {"email":"x"} produce the same result.
FALSE. PUT is a full replace, so omitted fields like name get wiped/defaulted; PATCH merges, so omitted fields survive.
TRUE or FALSE: URI versioning (/v2/users) keeps your caches simpler than header versioning.
TRUE in practice. The version is baked into the URL, so each version is a distinct cache key with no extra logic; header versioning makes the same URL mean two things, complicating Caching and ETags.

Spot the error

Endpoint POST /users/42/delete to remove a user — what's wrong?
The verb lives in the URL (/delete) instead of the HTTP method. REST wants nouns in the URL and verbs in the method: this should be DELETE /users/42.
DELETE /users/42 returns 200 OK with a body {"deleted": true} — is this an error?
Not strictly an error, but the conventional choice is 204 No Content (success, nothing to return). 200 with a confirmation body is acceptable; the real mistake would be 404 on a successful delete.
A "create payment" endpoint uses POST and, on a client timeout, the app automatically retries the identical request. What can go wrong?
POST is not idempotent, so the first request may have succeeded silently before timing out — the retry charges the customer twice. Fix with an idempotency key or a client-supplied id via PUT.
An API returns 500 Internal Server Error when a client sends malformed JSON. What's the misclassification?
5xx means the server's fault; malformed input is the client's fault, so this should be 400 Bad Request (or 422 if the JSON parses but is semantically invalid).
A login with a wrong password returns 403 Forbidden. Correct code?
No — the user hasn't proven who they are yet, so it's 401 Unauthorized (unauthenticated). 403 means "I know who you are, and you're still not allowed." See Authentication vs Authorization.
GET /users?limit=20&offset=1000000 on a 50-million-row table is slow. Where's the design flaw?
Deep offsets force the database to scan and discard the skipped rows before returning any — an cost. Cursor/keyset pagination (WHERE id > :cursor) uses an index and stays fast at any depth; see Database indexing.
Response to POST /users is 201 Created but includes no Location header. What's missing and why does it matter?
The client just made the server assign a new id and now doesn't know the new resource's URI, so it can't fetch or update it later. 201 should carry Location: /users/42.

Why questions

Why does PUT require the client to already know the id, while POST does not?
PUT targets a specific URI (/users/42) that must be knowable so a retry lands on the same resource, keeping it idempotent; POST posts to a collection (/users) and lets the server mint the id.
Why is a failed request that returns the wrong status class more dangerous than one that returns a wrong message?
Automated infrastructure — retry logic, circuit breakers, monitoring, caches — branches only on the status code, not the message. A wrong class can make a monitor report "all healthy" during an outage or make a client cache an error.
Why does offset pagination duplicate or skip items on a live feed?
Offset counts positions, not identities. If a row is inserted at the top between two page requests, every later row shifts down one slot, so an item you already saw reappears (or one gets skipped). Cursor pagination anchors on the last item's key, so it's immune.
Why do we round up when computing total pages, ?
The final partial page still holds real rows that must be shown; rounding down () would silently drop the leftover items on the last incomplete page.
Why version an API at all instead of just fixing the response shape when needed?
Once thousands of deployed clients (especially mobile apps) depend on the current shape, you can't update them all at once; versioning lets new clients use v2 while old ones keep the v1 contract alive.
Why is 409 Conflict the right code for a duplicate-email signup, rather than 400?
400 means the request itself is malformed; here the request is perfectly well-formed but clashes with current server state (the email already exists), which is exactly what 409 describes.

Edge cases

What status should DELETE /users/42 return the second time, after the user is already gone?
By convention either 404 Not Found (it's no longer there) or 204 No Content (the desired end-state "gone" is already true). Crucially, it must never delete something else — that's what makes DELETE idempotent.
What should GET /users?limit=20&offset=999999 return when there are only 30 users?
A successful 200 OK with an empty list, not a 404. The collection exists; the requested window is simply past the end, which is valid.
If a PATCH body is {} (empty), what should happen?
It's a valid no-op partial update: merge nothing, change nothing, and return 200 OK with the unchanged resource. Treat it as success, not 400.
A client requests /v3/users but your API only has v1 and v2. What code?
404 Not Found — that versioned URI simply doesn't exist. It's not 400 (the request is well-formed) and not 501 unless you specifically model "version not implemented."
What should GET /users/42 return if user 42 was soft-deleted (flagged deleted but still in the DB)?
From the client's view the resource is gone, so 404 Not Found is usually correct — the soft-delete flag is an internal implementation detail and shouldn't leak the record back to callers.
Under cursor pagination, what does the server return when the cursor points past the newest item (nothing after it)?
200 OK with an empty result list and typically a null/absent next cursor, signalling "you've reached the end." No error — running out of pages is a normal terminal state.

Recall One-line self-test

The three properties that decide correct method choice ::: safe (no state change), idempotent (repeat = once), and who assigns the id (server → POST, client-known → PUT).