A practical guide to designing REST APIs that stay predictable, easy to consume, and safe as your service grows.
A REST API is a contract. Once other teams build against it, every inconsistency becomes a support ticket and every breaking change becomes a migration. The good news is that most of what makes an API pleasant to use comes down to a handful of conventions applied consistently. Here are the ones worth getting right from day one.
Model your API around resources, not actions. A resource is a noun, and collections are plural.
GET /orders # list orders
GET /orders/42 # a single order
GET /orders/42/items # items belonging to order 42
Avoid verbs in paths. /getOrders or /createOrder are RPC habits leaking into REST. The HTTP method already carries the verb, so let it. Keep hierarchy shallow. Nesting is useful for ownership (/orders/42/items), but past two levels it gets awkward, and you can usually flatten with query parameters instead.
Each method has defined semantics, and clients, proxies, and caches rely on them.
GET: read a resource, never causes side effects, safe and idempotent. POST: create a resource or trigger a process, not idempotent. PUT: replace a resource fully, idempotent. PATCH: update part of a resource, not guaranteed idempotent. DELETE: remove a resource, idempotent.
Idempotency matters more than it sounds. If a client retries a PUT or DELETE after a timeout, the result should be the same as sending it once. That property is what makes safe retries possible on flaky networks. For non-idempotent POST operations where duplicates are costly, support an Idempotency-Key header so the server can detect and collapse retries.
The status code is the first thing a client checks. Use it honestly.
200 OK: successful read or update.
201 Created: a new resource exists, include a Location header pointing to it.
204 No Content: success with nothing to return, common for DELETE.
400 Bad Request: the request is malformed or syntactically wrong.
401 Unauthorized: no valid credentials, the caller must authenticate.
403 Forbidden: authenticated, but not allowed to do this.
404 Not Found: the resource does not exist.
409 Conflict: the request clashes with current state, like a duplicate or a version mismatch.
422 Unprocessable Entity: syntax is fine but the data fails validation.
429 Too Many Requests: rate limit hit, include a Retry-After header.
The distinction between 400 and 422 trips people up. Use 400 when you cannot parse the request at all, and 422 when you parsed it fine but a field violates a business rule. That difference tells the client whether to fix its serialization or fix its data.
Pick one casing and hold the line everywhere. snake_case or camelCase both work, but mixing them guarantees bugs on the consumer side. Use ISO 8601 for every timestamp, always in UTC, so there is no ambiguity about timezones or format.
{
"id": 42,
"status": "shipped",
"total_amount": 79.90,
"currency": "EUR",
"created_at": "2026-08-08T14:03:00Z"
}
Keep field names stable. Renaming a field is a breaking change even if the data behind it is identical.
Errors deserve the same care as success responses. A raw stack trace or a bare string helps nobody. Return a predictable shape with a machine-readable code, a human-readable message, and, for validation failures, the offending field.
{
"error": {
"code": "validation_failed",
"message": "The request contains invalid fields.",
"details": [
{
"field": "email",
"code": "invalid_format",
"message": "Must be a valid email address."
}
]
}
}
The code lets clients branch programmatically, the message goes in logs and UIs, and field lets a frontend highlight exactly what to fix. Keep the codes documented and stable, because clients will switch on them.
Any collection that can grow needs these controls. Use query parameters and never return an unbounded list.
GET /orders?status=shipped&sort=-created_at&limit=25&cursor=eyJpZCI6NDJ9
Cursor-based pagination holds up better than offset pagination under changing data, since offsets shift when rows are inserted or deleted. Return the next cursor and a total where it is cheap to compute. For a deeper look at the tradeoffs, see API pagination.
Hypermedia links let a response tell the client what it can do next, which reduces hardcoded URL assembly.
{ "id": 42, "status": "shipped",
"_links": { "cancel": { "href": "/orders/42/cancel" } } }
Full HATEOAS is rarely worth the ceremony for internal APIs. A few relevant links on key resources give most of the benefit without forcing every client to become a hypermedia parser.
Decide on versioning before you ship, not after the first breaking change. A version in the path (/v1/orders) is the most visible and cache-friendly option, and header-based versioning is cleaner but easier to overlook. Whatever you choose, additive changes like a new optional field should never require a version bump. Reserve new versions for changes that genuinely break existing clients.
Caching cuts load and latency when you let it. Send Cache-Control to state how long a response stays fresh, and an ETag so clients can revalidate cheaply.
GET /orders/42
< ETag: "a1b2c3"
< Cache-Control: private, max-age=60
GET /orders/42
> If-None-Match: "a1b2c3"
< 304 Not Modified
A 304 skips the body entirely, which is a real bandwidth win on large or frequently polled resources.
Serve everything over HTTPS, with no plaintext fallback. Authenticate with short-lived tokens (OAuth 2.0 bearer tokens or signed JWTs) rather than long-lived API keys where you can, and always authorize on the server per request. Never trust the client to enforce access. Rate-limit to protect against abuse and runaway clients, and validate every input server-side. Security is broad enough to warrant its own read, so see API security best practices for the full picture.
Get the conventions locked before the first client integrates, because consistency is far cheaper to establish than to retrofit. Start with resource URLs, honest status codes, and a structured error shape, since those three shape every other decision. Layer in pagination, caching, and versioning as the API grows. For the wider strategy behind all of this, our pillar on API design best practices ties the pieces together.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A developer-friendly walk through DNS resolution, record types, TTL, and the caching quirks that cause real production bugs.
A practical guide to choosing between GraphQL and REST based on how your clients fetch data, cache, and evolve over time.
Explore more articles in this category
A practitioner's tour of where WebAssembly earns its keep in 2026, from browser apps to edge compute, plus the places it still doesn't fit.
A practical look at why Go usually outruns Python at runtime, where Python holds its own, and how to pick per workload.
A grounded look at WebAssembly, the portable binary format that runs code at near-native speed inside a secure sandbox.
Evergreen posts worth revisiting.