REST API Best Practices Every Developer Should Know

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.

Resource-oriented URLs #

Model your API around resources, not actions. A resource is a noun, and collections are plural.

code

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.

Use HTTP methods for what they mean #

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.

Return meaningful status codes #

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.

Consistent JSON and naming #

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.

json.json

{
"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.

Structured error responses #

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.

json.json

{
"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.

Filtering, sorting, and pagination on collections #

Any collection that can grow needs these controls. Use query parameters and never return an unbounded list.

code

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.

HATEOAS, in moderation #

Hypermedia links let a response tell the client what it can do next, which reduces hardcoded URL assembly.

json.json

{ "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.

Versioning #

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 with ETag and Cache-Control #

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.

code

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.

Security basics #

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.

Checklist #

URLs are plural nouns with shallow, ownership-based hierarchy

HTTP methods match their semantics, and idempotent methods are safe to retry

Status codes are specific, including 201, 204, 409, 422, and 429

JSON casing is consistent and timestamps are ISO 8601 UTC

Errors return a structured code, message, and field

Collections support filtering, sorting, and cursor pagination

Versioning strategy is decided and additive changes never break clients

ETag and Cache-Control are set on cacheable responses

HTTPS everywhere, tokens over static keys, authorization on every request

The call we'd make #

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.