What REST actually is
The original REST dissertation describes an architectural style with six constraints: client-server, stateless, cacheable, uniform interface, layered system, and code-on-demand (optional). Most APIs that call themselves RESTful satisfy four of those: client-server, stateless, cacheable, and a uniform interface. They mostly ignore HATEOAS — the "hypermedia as the engine of application state" part — because it adds cost without adding benefit for the kinds of clients APIs actually have.
What's left, in practice, is a convention: model your domain as resources, address each resource with a URL, use HTTP methods to operate on them, and use HTTP status codes to communicate outcomes. That's the working definition that this page assumes.
Resources, not endpoints
The first design decision is how to break your domain into resources. A resource is a noun the client cares about — an order, a user, a payment — that has a stable identity over time. Resources are not the same as database tables. A database table called orders with a join to order_items can be exposed as a single /orders/{id} resource that includes the items inline; or as /orders/{id} plus /orders/{id}/items as a sub-resource. Both are reasonable. Which one fits depends on whether clients usually need the items together with the order.
Two questions help with the choice. First: what does the client want to do in one call? If most clients always need the items, fold them in. If only some do, expose a sub-resource and let the client opt in. Second: how often do the parts change independently? If items get updated without the order itself changing, separate sub-resources let clients PATCH them without touching the parent.
Naming
The conventions that have settled out:
- Plural nouns for collections.
/orders, not/order. Singular for single-instance resources that have no collection (/me,/settings). - Lowercase, hyphen-separated.
/shipping-addresses, not/ShippingAddressesor/shipping_addresses. URLs are case-sensitive in the path; consistency matters. - No verbs in the path. The HTTP method is the verb.
POST /orders, notPOST /createOrder. The exception is for actions that don't fit CRUD — see "Actions that aren't CRUD" below. - IDs are opaque to clients. Whether your IDs are integers, UUIDs, or prefixed strings is your business; clients should treat them as strings and not parse them.
HTTP methods, properly
Five methods cover almost every case, and each has a contract that clients depend on:
- GET — safe and idempotent. Reads only. Must not have side effects. Cacheable by default.
- POST — neither safe nor idempotent. The general "create or do something" verb. Use for resource creation when the server assigns the ID, and for operations that don't fit the other methods.
- PUT — idempotent. Replace a resource at a known URL with the body provided. Doing it twice has the same effect as doing it once.
- PATCH — not necessarily idempotent. Partial update. The body describes the change, not the new state.
- DELETE — idempotent. Remove the resource. Subsequent DELETEs return 404 (or 204 if you treat "already deleted" as success).
The idempotency property of PUT and DELETE is what makes them safe for retries when the response was lost. POST is not idempotent by default, which is why idempotency keys exist as a separate convention.
PATCH: JSON Merge Patch vs JSON Patch
Two standards. JSON Merge Patch (RFC 7396) sends a partial object — fields present in the body replace those fields, fields absent are left alone, and fields explicitly set to null are deleted. JSON Patch (RFC 6902) sends an array of operations like {"op": "replace", "path": "/email", "value": "..."}. Merge Patch is simpler and covers 90% of cases. JSON Patch handles array element changes and conditional updates that Merge Patch can't express, but most APIs don't need it. Pick one and stick with it; supporting both is rarely worth the complexity.
Status codes — the working set
You don't need 30 status codes. You need about a dozen, used consistently:
- 200 OK — success with a body.
- 201 Created — POST that created a resource. Include a
Locationheader pointing at the new resource. - 202 Accepted — async work was queued. Body should describe how to check status.
- 204 No Content — success with no body. Typically DELETE, sometimes PUT.
- 301 / 302 / 307 / 308 — redirects. 308 preserves the method on retry; 301 doesn't. Use 308 if you mean it.
- 400 Bad Request — request was malformed.
- 401 Unauthorized — credentials missing or invalid (really "Unauthenticated").
- 403 Forbidden — credentials valid, action not allowed.
- 404 Not Found — resource doesn't exist (or caller can't see it).
- 409 Conflict — request conflicts with current state.
- 422 Unprocessable Entity — validation failed. Use this rather than overloading 400.
- 429 Too Many Requests — rate-limited. Pair with
Retry-After. - 500 / 502 / 503 / 504 — server errors. 5xx is retryable; 4xx is not.
For the body shape that goes with these — error envelopes, problem-details, field-level validation — see API Error Handling Conventions.
Actions that aren't CRUD
Some operations don't map cleanly to GET/POST/PUT/PATCH/DELETE: archiving a record, retrying a failed job, generating a one-time token, sending a verification email. The pattern that has held up:
- POST to a sub-resource named after the action:
POST /orders/{id}/cancel,POST /tokens/{id}/revoke. The verb is in the URL, not the method. This is the closest thing to a violation of "no verbs in the path" that REST tolerates well. - Don't use PUT to flip a state field as a stand-in for an action.
PUT /orders/{id}with{"status": "cancelled"}looks RESTful but bakes business logic into a generic update. Cancellation has effects (refunds, notifications); model it as an action so you can test and audit it explicitly.
Filtering, sorting, paginating
Filters
Use query parameters for filters. The simple convention is ?status=open&customer_id=cust_123 — one parameter per field, equality only. This handles 80% of cases. For ranges, use suffixes: ?created_after=2026-01-01&amount_gt=100. Avoid embedding query languages (SQL fragments, GraphQL-style filter objects) in REST URLs; if you need that expressiveness, you've outgrown REST for that endpoint and should expose GraphQL instead.
Sorting
The convention is ?sort=created_at for ascending and ?sort=-created_at for descending, with comma-separation for multiple keys: ?sort=-created_at,id. Document which fields are sortable; arbitrary sort on un-indexed columns is a denial-of-service primitive.
Pagination
Three patterns: offset, cursor, keyset. For public APIs over an active dataset, cursor is the right default. The full comparison — including the failure modes of offset on growing tables — is in API Pagination Patterns.
Versioning
The decision to make is not how to version — it's whether you can avoid breaking changes in the first place. Adding fields to a response is non-breaking if clients are tolerant readers; adding optional request fields is non-breaking; changing the meaning of an existing field is breaking; removing a field is breaking. Most "version 2" releases turn out to be a half-dozen breaking changes that could have been avoided.
When you do need versioning, three options in order of preference:
- Path-based:
/v1/orders,/v2/orders. Visible, debuggable, easy to route. The default unless you have a specific reason to choose otherwise. - Header-based:
API-Version: 2. Cleaner URLs but harder to debug — you can't paste a URL into a browser and get the right version. Worth it for APIs with many small versioned endpoints. - Media-type:
Accept: application/vnd.example.v2+json. Most "RESTful" but most awkward in practice. Few teams pull it off well.
Whatever you choose, document the deprecation policy. "Old versions supported for 12 months after a new version ships" is a reasonable default. Returning a Sunset header (RFC 8594) on deprecated endpoints lets clients programmatically detect impending removal.
Caching
HTTP caching is the most underused feature of REST. Done right, it cuts load on your API and latency for clients. Two mechanisms:
- Expiration model. Set
Cache-Control: public, max-age=300on responses that don't change often. Clients and intermediaries cache the response for 300 seconds without checking back. Best for read-only resources whose contents are not user-specific. - Validation model. Return an
ETagheader with each response. Clients sendIf-None-Matchon subsequent requests; if the ETag still matches, return 304 Not Modified with no body. Best for resources that change but where you want to avoid resending unchanged data.
For mutations, ETags also enable optimistic concurrency: a client sends If-Match: "etag-value" on a PUT, and the server rejects with 412 Precondition Failed if someone else has changed the resource since the client read it. This is the cleanest way to avoid lost-update bugs without locking.
Rate limiting
Every public REST API needs rate limits. The headers conventions are:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1714838400
Retry-After: 30 (only on 429 responses)
Send these on every response, not just on rejections, so clients can self-throttle. The choice of algorithm — fixed window, sliding window, token bucket, leaky bucket — shapes burst tolerance and fairness. The full comparison is in API Rate Limiting Strategies.
Idempotency for non-idempotent methods
POST is not idempotent by default. For operations with real-world side effects — payments, order creation, message sends — clients need a way to retry safely after a network failure. The convention is the Idempotency-Key header. The full pattern, including how to store keys, how long to keep them, and the corner cases, is in Idempotency Keys for APIs.
Authentication
Two patterns dominate REST APIs: API keys for server-to-server, OAuth 2.0 for delegated user access. Send credentials in the Authorization header (Authorization: Bearer ...), not in query parameters — query parameters end up in logs and referrer headers. The full reference is in API authentication.
Webhooks: the inbound side
REST is request-driven. When the client needs to be notified of a server-side event, the standard solution is a webhook — the server makes an outbound POST to a URL the client registered. The full design — signing, retries, replay protection, idempotency on the receiver — is in Webhook Design and Delivery.
Common mistakes
- Returning 200 with an error in the body. The status code is the contract. Never use 200 for failure.
- Inconsistent error shapes across endpoints. One envelope across the entire API. Pick a shape; enforce it.
- Verbs in resource paths.
/getOrdersis RPC, not REST. Use the HTTP method. - Exposing internal IDs. If your IDs leak information (sequential integers reveal volume; database row IDs leak schema), use opaque IDs.
- No pagination on collection endpoints. Today's small dataset is tomorrow's load issue. Paginate from day one.
- Treating PATCH like PUT. Sending the full object on PATCH defeats its purpose and creates conflicts where there shouldn't be any.
- Caching POST responses. By default they're not cacheable for a reason; if you genuinely want caching on a POST that's safe-by-design, set explicit
Cache-Controlrather than relying on accidents. - Returning everything in one response. The response is also a contract; once clients depend on a field, removing it is breaking. Return what's useful, not what's available.
Where to go next
For when REST is the wrong tool — when clients need to query nested data with one round trip — see GraphQL. For real-time delivery, see WebSockets. For the broader design patterns that don't fit on one page, see the API design best practices long-form.