API Design

Design endpoints the way interviewers expect: REST resources, cursor pagination, idempotency keys, versioning, errors, and when RPC or events fit.

16 minLesson 5 of 6
On this page
  1. Resources and methods
  2. Idempotency keys
  3. Pagination
  4. Errors and versioning
  5. When REST is the wrong shape
  6. In the interview
  7. Common mistakes
  8. What to practice next

A diner taps "Book" for 4 people at 19:30. The server books a table, but the phone loses signal before the reply arrives. The app sees a timeout and retries, and the server books a second table. The restaurant keeps one empty all evening. If 0.5% of bookings succeed but lose their reply and get retried, the reservation system from requirements and scope, with 1 million bookings a day, makes 1,000,000 × 0.005 = 5,000 duplicate bookings a day.

That is an API design bug. An API is a contract with clients you cannot update all at once, so settle the parts that are expensive to change first: resources, paging, retries, errors and versions.

Resources and methods

Start from the functional requirements: each one becomes an endpoint. In REST, URLs name things (nouns) and HTTP methods say what you do to them. Spend about 3 minutes on this in an interview, as in the framework.

Reservation API (version 1)
GET    /v1/restaurants/{id}/availability?date=2026-12-04&party_size=4
         200  { "slots": [ { "time": "19:00", "tables_free": 2 }, ... ] }
 
POST   /v1/reservations          Idempotency-Key: 6f1c...e2
         body { "restaurant_id": 42, "time": "...", "party_size": 4 }
         201  { "id": "r-5521", "status": "confirmed" }
         409  slot_taken     422  invalid_party_size     429  rate_limited
 
GET    /v1/reservations/{id}                  200 | 404
DELETE /v1/reservations/{id}                  204 | 404
GET    /v1/users/me/reservations?limit=20&cursor=...
         200  { "items": [ ... ], "next_cursor": "eyJ0IjoiMjAyNi0..." }

The client asks for a time and party size; the server picks a free table that fits. That is why a blind retry books a second table instead of failing.

Method semantics matter for retries. GET changes nothing, so it is always safe to repeat. PUT and DELETE are idempotent by definition: doing them twice leaves the same state as once (the second DELETE may return 404, but nothing new happens). POST creates something new each time, so repeating it is the dangerous case.

Idempotency keys

The fix for the double booking: the client generates a unique key per user action (a random UUID when the diner taps "Book") and sends the same key with every retry of that action. The server remembers each key with the response it produced, and answers a repeat with the stored response instead of booking again.

A booking, a lost reply, a safe retryUse the arrows or ← → keys
  1. A booking, a lost reply, a safe retry, step 1Diner's phone to Reservation API (POST /reservations); Reservation API to Reservations DB (one transaction).Diner's phoneReservation APIReservations DBPOST /reservationsone transaction
    key
    6f1c
    bookings
    0

    1The diner taps Book. The app creates key 6f1c for this action and sends it in the Idempotency-Key header.

  2. A booking, a lost reply, a safe retry, step 2Diner's phone to Reservation API (POST /reservations); Reservation API to Reservations DB (one transaction).Diner's phoneReservation APIReservations DB6f1c → r-5521POST /reservationsone transaction
    key
    6f1c
    bookings
    1

    2In one transaction the API inserts the key (only if absent), books table 7 and stores the response it will send. One transaction means a crash leaves either all three or none.

  3. A booking, a lost reply, a safe retry, step 3Diner's phone to Reservation API (POST /reservations); Reservation API to Reservations DB (one transaction).Diner's phoneReservation APIReservations DBPOST /reservationsone transaction
    key
    6f1c
    bookings
    1

    3The reply is lost on the way back. The app only sees a timeout: it cannot know whether the booking happened.

  4. A booking, a lost reply, a safe retry, step 4Diner's phone to Reservation API (POST /reservations); Reservation API to Reservations DB (one transaction).Diner's phoneReservation APIReservations DBPOST /reservationsone transaction
    key
    6f1c
    bookings
    1

    4The app retries with the same key and the same body. A new key here would book a second table.

  5. A booking, a lost reply, a safe retry, step 5Diner's phone to Reservation API (POST /reservations); Reservation API to Reservations DB (one transaction).Diner's phoneReservation APIReservations DB6f1c seenPOST /reservationsone transaction
    key
    6f1c
    bookings
    1

    5The key insert finds 6f1c already there with a stored response. Nothing is booked.

  6. A booking, a lost reply, a safe retry, step 6Diner's phone to Reservation API (POST /reservations); Reservation API to Reservations DB (one transaction).Diner's phoneReservation APIReservations DBPOST /reservationsone transaction
    key
    6f1c
    bookings
    1
    status
    201

    6The API replays the stored 201 with r-5521. The diner sees one booking; the restaurant holds one table.

1 / 6

Details interviewers check. A repeat that arrives while the first request is still running must not run in parallel: the key row is claimed first, so the second request waits or gets a "still in progress" error to retry. The same key with a different body is a client bug: reject it with 422. Keys are kept for a window longer than any client retries (24 hours is a typical choice) and then deleted.

Be precise about what this buys. The network can still deliver the request twice; the key makes the effect happen once. That only holds if the key and the booking commit together. More in idempotency and retries.

Pagination

A diner's reservation history is a list that grows. Never return it all; return pages of, say, 20. There are two ways to say "the next page".

Offset: ?offset=20&limit=20, which the database runs as "sort, skip 20, take 20". Two problems. If a new reservation is added while the diner pages, everything shifts down by one: the last item of page 1 appears again at the top of page 2. And the database still reads every skipped row, so offset=100000 reads 100,020 rows to return 20.

Cursor: the response includes next_cursor, an opaque token encoding the sort key of the last item returned, for example its (created_at, id). The next query asks for items after that point, which an index answers directly, and inserts elsewhere in the list do not shift anything.

Two ways to page a list
OptionGood atBad atPick it when
Offset and limitSimple. Lets a user jump to page 37.Items repeat or vanish when the list changes. Deep pages get slower, because skipped rows are still read.Small, rarely changing lists, or an admin table that needs page numbers.
Cursor Usual defaultStable while the list changes. Every page costs the same, through an index.No jumping to page N. The sort order must have a unique tiebreaker such as the id.Feeds, histories, any list that grows. The usual interview answer.

Errors and versioning

Use the status code for the category and a body for the detail: {"error": {"code": "slot_taken", "message": "Table 7 was just booked", "retryable": false}}. A machine-readable code lets the app react (show other times), and retryable tells clients which failures to retry. 4xx means the client must change something; 5xx and 429 may succeed later, with backoff.

Adding a field is safe; clients ignore what they do not know. Removing or renaming a field, or changing what one means, breaks clients that may live on phones for years. Put a major version in the path (/v1/) for a public API: it is visible in logs and easy to route. Run old and new side by side until traffic on the old one fades.

When REST is the wrong shape

Request styles
OptionGood atBad atPick it when
REST over HTTP Usual defaultUnderstood by every client and tool. GETs can be cached by browsers and CDNs.Loose contracts unless you add a schema. Chatty for complex reads.Public and mobile APIs. The default.
RPC (gRPC, for example)Typed, generated client code and compact binary messages.Poor fit for browsers without a proxy. Harder to inspect by hand.Internal service-to-service calls with many methods and high volume.
Events on a queueThe sender does not wait and does not know who listens. Consumers can be added freely.No direct reply. Ordering and duplicates must be handled by consumers.Something happened that others react to: a booking triggers an email and analytics.

For pushing updates to a client (a table frees up), none of these is enough on its own; see realtime communication.

In the interview

Write the 3 to 5 endpoints for the core flows, with error cases, then argue the one decision that matters most for this problem. Phrases that show judgment:

  • "Booking is a POST that clients will retry on bad networks, so it takes an idempotency key."
  • "History grows and changes while you scroll, so I'll page it with a cursor."
  • "Public clients get REST; the booking service tells the email service through an event, because it should not wait for an email."
Defending the booking endpointStrong answer
Interviewer

Why not just check for a duplicate reservation before inserting?

You

The original and a retry can both check, both find nothing, and both insert. And "same diner, same time" is not always a duplicate: someone might book for two groups. The key says exactly which request is a retry. It's stored in the same transaction as the booking, so there is no gap.

Follow-ups you should expect:

  • "Who generates the key?" The client, once per user action, before the first attempt. A key generated by the server cannot recognize a retry.
  • "How do you rate limit this API?" Per user or API key, returning 429 with a Retry-After header. See rate limiting.
  • "Would you use GraphQL?" When many client screens need different shapes of the same data. For a few fixed flows, REST is simpler to cache and secure.

Common mistakes

Quick check

A history list is paged with offset, 20 per page. A diner reads page 1, then a new reservation is added at the top, then they load page 2. What do they see?

Which request is safe for a client to retry with no extra mechanism?

A list is sorted by an indexed column. With offset=100000&limit=20, about how many rows does the database read?

What to practice next

The endpoints decide the queries; Data Modeling turns those queries into tables and keys. For an API where idempotency is the whole game, see Design a Payment System.