LESSON 4.2
REST APIs done well
Resources, methods and status codes, plus the three details that separate a sketch from a real API: idempotency, pagination and versioning.
7 min read
Most system design interviews include a few minutes on the API, and most real systems expose one. REST over HTTP is the default choice for public and web-facing APIs because every client, proxy and tool understands it. The basics are quick to learn. The details are where designs go wrong.
Resources and methods
REST models your system as resources, nouns with URLs, and uses HTTP methods as the verbs.
| Method | Meaning | Safe to retry? |
|---|---|---|
GET /links/abc | Read a resource | Yes, it changes nothing |
POST /links | Create a resource or run an action | Not by default |
PUT /links/abc | Replace a resource with this full version | Yes, the result is the same every time |
PATCH /links/abc | Change some fields | Depends on the change |
DELETE /links/abc | Remove it | Yes, deleting twice leaves it deleted |
Name resources as plural nouns and nest them only when the child cannot exist without the parent: GET /events/42/seats reads well; /users/7/orders/9/items/3/reviews is a maze. Put verbs in the method, not the URL: POST /orders/9/cancel is acceptable for an action that isn't a simple field change, but GET /getOrder?id=9 is not REST.
Status codes that carry meaning
Clients make decisions from status codes, so use the specific ones.
- 200 OK, 201 Created (with the new resource's location), 202 Accepted (work queued, not done yet), 204 No Content.
- 400 malformed input, 401 not authenticated, 403 authenticated but not allowed, 404 not found, 409 conflict with current state, 410 gone for good, 422 valid syntax but failed validation, 429 rate limited (with
Retry-After). - 500 a bug, 502/504 an upstream failed or timed out, 503 temporarily unavailable.
Idempotency: making retries safe
Networks fail in the worst place: after the server did the work but before the client heard back. The client must retry, and the server must not do the work twice.
GET, PUT and DELETE are naturally idempotent. For POST, let the client send an idempotency key, a unique ID it generates per logical operation:
- The client sends
POST /paymentswithIdempotency-Key: 7f3a…. - The server records the key with the request inside the same transaction that does the work, then stores the response.
- A retry with the same key finds the record and returns the stored response, without charging again.
Keys expire after a day or so. If the same key arrives with a different request body, return an error: it's a client bug.
Pagination
Any list that can grow needs pagination from day one.
Offset: ?offset=1000&limit=50
- Simple
- Can jump to page N
- Slow for large offsets: the database still reads and skips the rows
- Inserts shift pages, causing duplicates and gaps
Pick it when: Small, stable lists such as admin tables.
Cursor: ?after=abc&limit=50
- Fast at any depth, using an index
- Stable while new items arrive
- No jumping to page N
- The cursor must encode a unique sort order
Pick it when: Feeds, timelines, anything large or changing. The default.
A cursor is usually an opaque, encoded form of the last item's sort key, for example (created_at, id). The id breaks ties so no two items share a position.
Versioning and evolution
Clients you don't control will keep calling your API for years. Prefer additive changes: new optional fields and new endpoints never break anyone. When you must break something, version it, most often in the path (/v2/orders) or a header, and run both versions until old clients are gone.
Security basics worth saying
Use HTTPS everywhere. Authenticate with tokens (OAuth 2.0 access tokens or signed session tokens) sent in the Authorization header, never in URLs where they end up in logs. Authorize every request against the resource's owner, because a valid token for user 7 must not read user 8's orders. Rate-limit per client.
Try it
Webhook Delivery HLD
Design webhook delivery: retries, idempotency and status codes from the other side of the wire.