Backend
Lesson 3 of 8About 2 min readSuggest an edit

Designing REST APIs

An API is a contract with people you may never meet. Good design makes it predictable: once a developer has used one endpoint, they can guess how the others work.

Resources and URLs

Model the API around nouns (resources) and let HTTP methods supply the verbs.

GET    /orders           list orders
POST   /orders           create an order
GET    /orders/9001      read one order
PATCH  /orders/9001      change some fields
DELETE /orders/9001      delete it
GET    /orders/9001/items   items that belong to the order

Use plural nouns, lowercase, and hyphens for multi-word paths (/shipping-addresses). Avoid verbs in URLs such as /getOrder or /createOrder. For actions that aren’t simple field changes, model them as a sub-resource: POST /orders/9001/cancellation.

Status codes and errors

Return the status code that matches what happened (see the HTTP fundamentals lesson), and give errors a consistent, machine-readable body:

{
  "error": {
    "code": "insufficient_stock",
    "message": "Only 2 units of book-42 are left.",
    "field": "quantity"
  }
}

Clients branch on code, show message to people, and use field to highlight the form input. Never leak stack traces or SQL errors; log them server-side instead.

Pagination

Never return an unbounded list. Two common styles:

  • Offset: GET /orders?limit=50&offset=100. Simple, supports jumping to page 7, but slow on large tables and unstable when rows are inserted while you page.
  • Cursor: GET /orders?limit=50&after=eyJpZCI6OTAwMX0. The server returns an opaque next cursor encoding the last item’s sort key. It is fast at any depth and stable under inserts, but can’t jump to an arbitrary page.

Prefer cursors for feeds and large collections. Always sort by a unique, stable key (for example created_at, id), or pages can repeat or skip items.

Idempotency keys

POST is not idempotent, and networks drop responses. If a payment request times out, the client can’t tell whether it succeeded. The fix is an idempotency key:

POST /payments
Idempotency-Key: 5f1c9a2e-7c1b-4a8e-9f2d-3b6a0c1e8d44

The server stores the key with the result of the first request. A retry with the same key returns the stored result instead of charging twice.

Filtering, sorting and partial responses

Use query parameters: GET /orders?status=shipped&sort=-created_at. Keep the vocabulary consistent across endpoints. If clients need only a few fields of large resources, a fields parameter saves bandwidth.

Evolving without breaking clients

Adding things is safe: new endpoints, new optional fields, new optional parameters. Clients must ignore fields they don’t know. Breaking changes include removing or renaming a field, changing its type or meaning, making an optional parameter required, and changing error codes.

When you must break, version the API (/v2/orders or a version header), run both versions, announce a deprecation date, and measure who still calls the old one before turning it off.

Document it as you build

Describe the API in OpenAPI, generate reference docs from it, and include a runnable example for every endpoint. An API without documentation gets reverse-engineered, and whatever people guess becomes a contract you can’t change.

Next: Caching

Where caches live, cache-aside and write-through, invalidation, stampedes and data you must never cache.