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 opaquenextcursor 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.