REST API Design Principles

Resource naming, versioning strategies, cursor vs offset pagination, filtering/sorting, the RFC 9457 problem+json error format, and HATEOAS basics.

A good REST API aims for predictability — you can guess what to do next without reading the docs deeply. That comes from using HTTP's vocabulary (methods, status codes, headers) consistently and representing resources by clear rules. This guide walks the design axes you decide over and over in practice.

Resource naming

The heart of REST is expressing 'resources' (nouns) as URLs and using the HTTP method to say what to do to them — not baking the verb into the path. Verb-in-path routes like GET /getUsers or POST /createUser are an anti-pattern.

Versioning strategies

APIs eventually need breaking changes, so you need versioning to protect existing clients. Three approaches dominate. URL path versioning (/v1/users) is the most common, highly visible, and easy to cache and route. Header versioning (Accept: application/vnd.example.v2+json) keeps URLs clean but is less discoverable and fiddlier to test. Query-parameter versioning (/users?version=2) is simple but complicates cache keys. Whichever you pick, the core principle is to preserve backward compatibility as far as possible (adding fields is non-breaking) and to batch breaking changes into a deliberate version bump.

Pagination: offset vs cursor

For large lists, page rather than return everything. Offset pagination (?limit=20&offset=40) is intuitive and lets you jump to any page, but has two weaknesses — deeper pages get slower as the database counts the preceding rows, and if items are inserted between page loads, rows shift, causing duplicates or gaps. Cursor pagination (?limit=20&cursor=eyJpZCI6NDB9) passes an opaque cursor for 'the last item seen' and resumes after it, giving stable performance at scale and consistency even as data changes live. The trade-off is you can't jump to an arbitrary page number. Prefer cursors for infinite scroll and big data; consider offset for UIs like admin tables that need page numbers.

GET /v1/orders?status=paid&sort=-created_at&limit=20&cursor=eyJpZCI6MTAwfQ HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": [ { "id": 101, "status": "paid" } ],
  "page": { "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
}

Filtering and sorting

Layer filtering, sorting, and search onto collection endpoints via query parameters. Filters use the field name directly (?status=paid&role=admin), and sorting conventionally prefixes a field with - for descending (?sort=-created_at,name = created descending, name ascending). Range filters use a suffix convention like ?price_gte=1000&price_lte=5000 or a dedicated grammar. Again the point is consistency and documentation: settle the rules once and apply them uniformly so clients reuse their knowledge across collections.

Error format: RFC 9457 problem+json

If every endpoint shapes errors differently, client error handling becomes a nightmare. The standard fix is RFC 9457 (formerly RFC 7807, application/problem+json). It defines the fields type (a URI identifying the error kind), title (a human-readable summary), status (the HTTP status code), detail (a specific explanation for this occurrence), and instance (the resource where it happened), plus optional extension members. The status code is still the primary signal; problem+json adds machine-readable structured detail on top.

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "email must be a valid address",
  "instance": "/v1/users",
  "errors": [ { "field": "email", "code": "format" } ]
}

HATEOAS basics

HATEOAS (Hypermedia as the Engine of Application State) is REST's original ideal of embedding 'links to the actions you can take next' in the response, so the client follows server-provided links instead of hardcoding URLs. For instance, putting _links: { "cancel": "/v1/orders/101/cancel", "self": "/v1/orders/101" } in an order response lets the server signal, per state, whether cancellation is possible and where. Full HATEOAS is rare in practice, but the partial adoption of including related-resource links (self, next, related items) in responses has real practical value: it lowers coupling and makes the API self-describing.