Skip to content

Designing a REST API That Ages Well: Naming, Versioning, Pagination and Errors

Engineering7 min readBy the Nexzem team

Practical REST API design rules that keep clients working for years: resource naming, HTTP semantics, status codes, Problem Details errors, versioning and pagination.

In this article
  1. 01Why API design decisions last
  2. 02Name resources, not actions
  3. 03Respect HTTP method semantics
  4. 04Use status codes precisely
  5. 05Return errors in a standard format
  6. 06Plan versioning and deprecation from day one
  7. 07Paginate with cursors for changing data
  8. 08Small conventions that prevent big bugs
  9. 09Authentication, rate limits and webhooks
  10. 10Filtering, sorting and partial responses
  11. 11Document the contract and test it

Why API design decisions last

An API is a contract. Once mobile apps, partners and other services depend on it, every inconsistency becomes permanent, because changing it breaks someone. Old mobile app versions in particular can stay in use for years. The goal of good design is not elegance; it is making the API predictable for clients and leaving room to evolve without breaking them.

This guide covers the decisions that matter most for a REST API. If you are still choosing a style, our REST vs GraphQL comparison explains when each fits.

Name resources, not actions

Use plural nouns for collections and IDs for items: /orders and /orders/{orderId}. Nest only one level for clear ownership, such as /orders/{orderId}/items, and keep deeper relationships as top-level resources with filters. Pick one casing convention for paths and fields and use it everywhere.

Some operations are not simple create, read, update or delete. Model them as sub-resources or state changes, such as POST /orders/{orderId}/cancellation, rather than verbs in random places. Consistency matters more than which convention you choose.

Respect HTTP method semantics

Clients, proxies and caches rely on HTTP semantics, so follow them:

  • GET reads and never changes state, so it can be cached and retried safely
  • POST creates a resource or triggers a process; return 201 Created with a Location header for new resources
  • PUT replaces a resource and is idempotent: sending it twice has the same effect as once
  • PATCH updates part of a resource
  • DELETE removes a resource and is idempotent
  • For POST requests with side effects, such as payments, accept an Idempotency-Key header so client retries do not create duplicates

Use status codes precisely

Status codes let clients react without parsing messages. Use 200 for success with a body, 201 for creation, 202 when work is accepted for later processing and 204 for success without a body. On the client error side, 400 means a malformed request, 401 means not authenticated, 403 means authenticated but not allowed, 404 means not found, 409 signals a conflict with current state, 412 means a precondition such as If-Match failed, 422 means valid syntax but invalid content and 429 means rate limited, ideally with a Retry-After header.

Use 500 for unexpected server errors and 503 when a service is temporarily unavailable. Never return 200 with an error in the body. Our HTTP status codes reference lists the full set with explanations.

Return errors in a standard format

RFC 9457, Problem Details for HTTP APIs, defines a standard JSON error body with the media type application/problem+json. Its fields are type (a URI identifying the error kind), title (a short summary), status, detail (a human-readable explanation for this occurrence) and instance (an identifier for this occurrence). You can add extension fields, such as an errors array listing each invalid field and the reason.

Using one error shape everywhere lets clients write a single error handler. Keep messages useful but never leak stack traces, SQL or internal hostnames.

Plan versioning and deprecation from day one

The best versioning strategy is to need it rarely. Additive changes, such as new optional fields, new endpoints and new enum values clients are told to expect, are not breaking. Removing or renaming fields, changing types, making optional inputs required and changing error codes are breaking.

When a breaking change is unavoidable, a version in the path, such as /v2/orders, is the simplest for clients to understand and route. Header-based versioning is cleaner in theory but harder to debug. Announce deprecations in documentation and in responses using the Deprecation header and the Sunset header (RFC 8594), and track which clients still call old versions before switching them off.

Paginate with cursors for changing data

Offset pagination (?limit=20&offset=40) is easy but slows down on large tables and skips or repeats items when data changes between pages. Cursor pagination returns an opaque next cursor that encodes the position, such as the last item's creation time and ID, and the client passes it back. It stays fast and stable as data grows.

  • Always cap the page size, for example a default of 20 and a maximum of 100
  • Return the next cursor in the body or in a Link header with rel="next"
  • Keep cursors opaque so you can change their internal format later
  • Support filtering and sorting with documented query parameters

Small conventions that prevent big bugs

Use ISO 8601 timestamps in UTC. Represent IDs as strings, even if they are numbers today. Represent money as an integer in the smallest unit or as a decimal string, always with a currency code, never as a floating point number. To prevent lost updates, return an ETag with resources and accept If-Match on updates, responding 412 when the resource has changed since the client read it.

Authentication, rate limits and webhooks

Send credentials in the Authorization header, for example OAuth 2.0 bearer tokens, never API keys in query strings, because URLs end up in logs and browser history. Return 401 with a WWW-Authenticate header when credentials are missing or invalid. Rate limit per client and respond with 429 and Retry-After. If your API sends webhooks, sign each payload with an HMAC over the body and a timestamp, retry failed deliveries with backoff and include an event ID so receivers can ignore duplicates.

Filtering, sorting and partial responses

Keep query parameters predictable. Filters use field names, such as ?status=open&created_after=2026-01-01. Sorting often uses a sort parameter with a minus prefix for descending order, such as sort=-created_at. A fields parameter lets clients request only the fields they need, which helps mobile apps on slow networks. Document the allowed values for each parameter, and decide once whether unknown parameters are rejected with 400 or ignored, then apply that rule everywhere.

Document the contract and test it

Describe the API in an OpenAPI document and generate reference docs and client SDKs from it. Validate requests and responses against the spec in tests, so the documentation cannot drift from reality. An API gateway can then handle authentication, rate limiting and logging consistently. Nexzem's backend and API development work follows these conventions.

Planning something similar?

Get a straight answer on scope, cost and timeline.

Talk to the team

Tell us what you're building.

A solutions consultant replies within one business day with next steps, a rough estimate and a suggested team.