REST API Design Guidelines for Production Systems

2 min readIntermediate
API DesignRESTJavaBackend

A well-designed API is boring in the best way: consistent, predictable, and unambiguous. After building and consuming a fair share of them, these are the rules I would not ship without.

Resources over actions

Model your URLs around nouns and let HTTP verbs express intent. An action that does not fit a noun is a sign you are missing a resource, not a verb.

GET    /v1/orders          list
POST   /v1/orders          create
GET    /v1/orders/{id}     fetch
PATCH  /v1/orders/{id}     partial update
POST   /v1/orders/{id}/cancel   ← deliberate action, not CRUD

Use PATCH for partial updates, not PUT with full replacement semantics that nobody honors. Keep action endpoints a deliberate exception, not the default.

Versioning is a public promise

Changing a contract that clients rely on breaks them silently. Version in the URL and keep old versions alive on a schedule:

# URL versioning is ungraceful but unmistakable.
/v1/orders
/v2/orders

Add deprecation headers early and document the sunset date. Your future self will thank your past self for versioning before the first external consumer exists.

Errors are a contract too

A 500 with an HTML page is not an API. Use a consistent error envelope and fill it with information a client can act on:

{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "Order 12345 does not exist",
    "status": 404,
    "traceId": "a1b2c3d4-e5f6-..."
  }
}

A stable machine-readable code is worth more than the message. Clients should switch on code, never on strings scraped from message. Include traceId so a support ticket can find the request in your logs in seconds.

Pagination, sorting, and stable defaults

For list endpoints, decide the default and keep it stable:

  • Cursor pagination for append-heavy feeds; offset for admin-style lists.
  • Return limit, has_more, and the next cursor in the response, not just in your head.
  • Make ordering explicit: ?sort=created_at&order=desc. Never rely on DB default ordering — it changes.
{
  "items": [ { "id": "…" } ],
  "pagination": { "next": "eyJjcmVhdGVkX2F0IjogIjIwMjYvMDYvMTQifQ", "has_more": true }
}

Idempotency for anything that costs money

Creating an order twice is expensive; creating a payment twice is a disaster. Let clients send an Idempotency-Key header:

POST /v1/payments
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7

Store the key, return the cached response on replay. It is a small feature with a disproportionate payout in retry safety.

The takeaway

The status codes and resource shapes are table stakes. What separates production APIs is versioning discipline, an error contract clients can rely on, predictable list semantics, and idempotency on the operations that matter. Consistency beats cleverness every time.

← Back to technical articles